【游戏技术群】959392658  【游戏出海群】12067810
游戏蛮牛 手机端
开启辅助访问
 找回密码
 注册帐号

扫一扫,访问微社区

开发者专栏

关注:2395

当前位置:游戏蛮牛 技术专区 开发者专栏

__________________________________________________________________________________
开发者干货区版块规则:

  1、文章必须是图文形式。(至少2幅图)
      2、文章字数必须保持在1500字节以上。(编辑器右下角有字数检查)
      3、本版块只支持在游戏蛮牛原创首发,不支持转载。
      4、本版块回复不得无意义,如:顶、呵呵、不错......【真的会扣分的哦】
      5、......
__________________________________________________________________________________
查看: 1212|回复: 15

[士郎] C#中的Attribute和元数据

[复制链接]  [移动端链接]
排名
1
昨日变化

7201

主题

7731

帖子

2万

积分

Rank: 16

UID
1231
好友
185
蛮牛币
11748
威望
30
注册时间
2013-7-29
在线时间
3692 小时
最后登录
2019-1-18

社区QQ达人活力之星原创精华达人突出贡献奖财富之证游戏蛮牛QQ群会员蛮牛妹VIP

发表于 2019-1-10 11:14:35 | 显示全部楼层 |阅读模式

马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。

您需要 登录 才可以下载或查看,没有帐号?注册帐号

x
所谓元数据,就是描述数据的数据(Metadata (CLI) - Wikipedia)。

运转的系统就像一个盒子,而元数据就像是这个盒子的规格说明书。系统可接受的输入,内部的结构和处理的方式,最终得到的输出,这些要素之间的内在逻辑关系对用户来说通常是复杂而难以理解的。规格说明书,是在不影响原有系统功能前提下,提供给用户认识系统的重要资料。


对于XML语言来说,描述数据域和数据域类型的XSL文件可以看作是XML文件的元数据。而JSON则是自包含的,元数据和用户数据是混合存储的。对一种编程语言的词法和语法的描述,也可以看作是元数据。由于元数据本身的存储也有格式,比如XSL也采用了XML格式,描述元数据的数据也是元数据。
C#支持反射,其基础就是元数据。其中,Attribute是C#元数据的一个重要组成部分,赋予了用户元编程的能力。


一个简单的Attribute示例
元数据都由编译器自动生成。按照定义的方式,Attribute可以分为两种,一种是原生Attribute,即语言和框架内置的。另一种是自定义Attribute,是用户根据需要自己定义的(需要继承自System.Attribute)。原生Attribute由CLR(Common Language Runtime)使用,自定义Attribute必须由用户自己写处理逻辑。
我们直接给出一个同时使用了原生Attribute和自定义Attribute的示例。其中Obsolete是一个原生Attribute,而TestAttribute是一个自定义Attribute。Attribute标记“[]”用于告诉编译器生成元数据。Attribute的基本语法略去不提,有大量资料可参考。


[AppleScript] 纯文本查看 复制代码
using System;

namespace Test
{
    class TestAttribute : System.Attribute{}

    class Program
    {
        [Obsolete("do not call me", false)]
        void f(){}

        [Test]
        void g(){}

        static void Main(string[] args)
        {
            Console.WriteLine("Hello World!");
        }
    }
}

元数据的格式和存储方式
编译器将C#源码编译为托管PE文件(exe, dll),其中包括四个部分:PE头、CLR头、元数据和IL代码(https://blog.csdn.net/xushaozhang/article/details/54428471)。其中PE头是windows要求的标准信息,CLR头是托管模块特有的,包括CLR版本号,模块的入口方法,数字签名,模块内部元数据表的大小和偏移。IL代码是编译器生成的中间语言代码。元数据描述了类型、方法、属性,也包括了程序引用的其他类型和方法等。
为了分析元数据的具体内容及其格式,我们可以对生成的托管PE文件反汇编。使用的工具是MSIL 反汇编程序 Ildasm.exe(https://stackoverflow.com/questions/8861065/what-is-metadata-in-net)。如下列出了对上一节中的极简Attribute示例程序反汇编得到的元数据。
[AppleScript] 纯文本查看 复制代码
……
TypeDef #1 (02000002)
-------------------------------------------------------
        TypDefName: Test.TestAttribute  (02000002)
        Flags     : [NotPublic] [AutoLayout] [Class] [AnsiClass] [BeforeFieldInit]  (00100000)
        Extends   : 0100000C [TypeRef] System.Attribute
        Method #1 (06000001) 
        -------------------------------------------------------
                MethodName: .ctor (06000001)
                Flags     : [Public] [HideBySig] [ReuseSlot] [SpecialName] [RTSpecialName] [.ctor]  (00001886)
                RVA       : 0x00002050
                ImplFlags : [IL] [Managed]  (00000000)
                CallCnvntn: [DEFAULT]
                hasThis 
                ReturnType: Void
                No arguments.


TypeDef #2 (02000003)
-------------------------------------------------------
        TypDefName: Test.Program  (02000003)
        Flags     : [NotPublic] [AutoLayout] [Class] [AnsiClass] [BeforeFieldInit]  (00100000)
        Extends   : 0100000D [TypeRef] System.Object
        Method #1 (06000002) 
        -------------------------------------------------------
                MethodName: f (06000002)
                Flags     : [Private] [HideBySig] [ReuseSlot]  (00000081)
                RVA       : 0x00002059
                ImplFlags : [IL] [Managed]  (00000000)
                CallCnvntn: [DEFAULT]
                hasThis 
                ReturnType: Void
                No arguments.
                CustomAttribute #1 (0c00000b)
                -------------------------------------------------------
                        CustomAttribute Type: 0a00000b
                        CustomAttributeName: System.ObsoleteAttribute :: instance void .ctor(class System.String,bool)
                        Length: 20
                        Value : 01 00 0e 64 6f 20 6e 6f  74 20 63 61 6c 6c 20 6d >   do not call m<
                              : 65 00 00 00                                      >e               <
                        ctor args: ("do not call me",  <can not decode> )


        Method #2 (06000003) 
        -------------------------------------------------------
                MethodName: g (06000003)
                Flags     : [Private] [HideBySig] [ReuseSlot]  (00000081)
                RVA       : 0x0000205c
                ImplFlags : [IL] [Managed]  (00000000)
                CallCnvntn: [DEFAULT]
                hasThis 
                ReturnType: Void
                No arguments.
                CustomAttribute #1 (0c00000c)
                -------------------------------------------------------
                        CustomAttribute Type: 06000001
                        CustomAttributeName: Test.TestAttribute :: instance void .ctor()
                        Length: 4
                        Value : 01 00 00 00                                      >                <
                        ctor args: ()


        Method #3 (06000004) [ENTRYPOINT]
        -------------------------------------------------------
                MethodName: Main (06000004)
                Flags     : [Private] [Static] [HideBySig] [ReuseSlot]  (00000091)
                RVA       : 0x0000205f
                ImplFlags : [IL] [Managed]  (00000000)
                CallCnvntn: [DEFAULT]
                ReturnType: Void
                1 Arguments
                        Argument #1:  SZArray String
                1 Parameters
                        (1) ParamToken : (08000001) Name : args flags: [none] (00000000)

        Method #4 (06000005) 
        -------------------------------------------------------
                MethodName: .ctor (06000005)
                Flags     : [Public] [HideBySig] [ReuseSlot] [SpecialName] [RTSpecialName] [.ctor]  (00001886)
                RVA       : 0x0000206d
                ImplFlags : [IL] [Managed]  (00000000)
                CallCnvntn: [DEFAULT]
                hasThis 
                ReturnType: Void
                No arguments.
……
TypeRef #14 (0100000e)
-------------------------------------------------------
Token:             0x0100000e
ResolutionScope:   0x23000001
TypeRefName:       System.ObsoleteAttribute
        MemberRef #1 (0a00000b)
        -------------------------------------------------------
                Member: (0a00000b) .ctor: 
                CallCnvntn: [DEFAULT]
                hasThis 
                ReturnType: Void
                2 Arguments
                        Argument #1:  String
                        Argument #2:  Boolean
……
User Strings
-------------------------------------------------------
70000001 : (12) L"Hello World!"
……

容易发现,元数据由三种元数据表构成:定义表,引用表和清单表。


1)定义表,包含了类型、属性、方法的定义。对应于上面示例中TypeDef/Field/Method等。
2)引用表,主要就是对引用的程序集内部的类型、属性、方法的描述。对应于上面示例中TypeRef/MemberRef等。
3)清单表,主要就是对程序集组成的那部分文件的信息。


自定义Attribute继承自System.Attribute,除此之外,在定义方式上,和一般的类型并没有本质不同。都存在于类型定义中,有自己的属性和方法。
原生Attribute(如Obsolete)在System库中定义(可查看引用表),自定义Attribute(如TestAttribute)在用户代码中定义。除此之外,两种Attribute在元数据表中的地位是一样的。引用的方式,在元数据表中存储的方式,都是一样的。


Attribute是修饰类型、属性和方法的。所以Attribute和普通类型的实例化不同,在编译期就确定了构造所需的全部信息(类型和值)。举例来说,上面示例中的ObsoleteAttribute和TestAttribute,每一个用这些Attribute修饰的地方,都会产生一个相应类型的实例。生成实例所需的构造参数,也在编译器就确定了。而一般类型在编译期只能确定类型信息,需要在运行期动态实例化。


C#元数据和可执行代码并存于PE文件,有关类型的信息同类型自身固定在一起,不会遍布很多地方。这有助于解决程序库的版本问题

元数据与反射
原生Attribute在框架和库中使用。自定义的Attribute必须在用户代码中调用才有意义。元数据借助反射发挥作用。以下是一个简单的反射示例。
[AppleScript] 纯文本查看 复制代码
Type program = typeof(Program);
MethodInfo minfo = program.GetMethod("f");
Console.WriteLine("defined? {0}", minfo.IsDefined(typeof(TestAttribute), true));
                                                
var attributes = minfo.GetCustomAttributes(typeof(ObsoleteAttribute), true);
Console.WriteLine("count: {0}", attributes.Length)

反射为系统带来了极大的灵活性,但是调用方法前必须通过字符串查找,效率很低。而且每次GetCustomAttributes()调用都会重新遍历元数据,并返回一个新的对象。底层并没有因为这些数据在运行时不会改变而自动优化。(Attribute操作的性能优化方式 - Jeffrey Zhao - 博客园一次失败的尝试(上):原来GetCustomAttributes方法每次都返回新的实例 - Jeffrey Zhao - 博客园
元数据应用举例:Managed/Native Interoperability
元数据提供了一个探查和使用应用程序,却不需要侵入源代码的方法。特别适合调试器、Profiler、编辑器等各种工具使用。
元数据还为各种不同语言编写的应用互操作打开方便之门。一般情况下,为了提供跨语言的交互,需要包括各种头文件,依赖各种库,不方便且容易出错。但是元数据和代码一同发布,既描述了执行流程,也描述了规格说明,是一个完备的整体。


托管代码(Managed)和非托管代码(UnManaged/Native)的交互存在很多困难。托管代码和非托管代码的数据类型不同,有些数据类型在托管代码中存在,而在非托管代码中未定义。两种代码对同一数据类型的定义也有可能不一样。托管代码通常采用垃圾回收,而非托管代码也许需要自行管理内存。两种代码的异常处理方式(错误处理)通常也不一样。(An Overview of Managed/Unmanaged Code Interoperability
COM是异构系统之间互操作的一种标准。模块只要按照COM规范提供接口,就可以按照规范的方式被其他模块调用。下面这个示例给出了C#使用Attribute实现COM接口的方式:(Example COM Class (C# Programming Guide)
[AppleScript] 纯文本查看 复制代码
using System.Runtime.InteropServices;

namespace project_name
{
    [Guid("EAA4976A-45C3-4BC5-BC0B-E474F4C3C83F")]
    public interface ComClass1Interface
    {
        [DispId(1)]
        void Init(string userid , string password);
        [DispId(2)]
        bool ExecuteSelectCommand(string selCommand);
    }

    [Guid("7BD20046-DF8C-44A6-8F6B-687FAA26FA71"), 
        InterfaceType(ComInterfaceType.InterfaceIsIDispatch)]
    public interface ComClass1Events {}

    [Guid("0D53A3E8-E51A-49C7-944E-E72A2064F938"),
        ClassInterface(ClassInterfaceType.None),
        ComSourceInterfaces(typeof(ComClass1Events))]
    public class ComClass1 : ComClass1Interface
    {
        public void Init(string userid , string password) {}
        public bool ExecuteSelectCommand(string selCommand)
        {
            //...
            return true;
        }
    }
}

其中DispId用于指定方法在vtable中的位置。System.Runtime.InteropServices使用Guid/DispId/InterfaceType/ClassInterface/ComSourceInterfaces等Attribute,最终生成一个符合COM规范的组件。
除了COM,托管代码和非托管代码交互的另一种方式是Flat API,也就是统一到裸C的layout和调用方式。这就是P/Invoke(Platform Invoke)。只要在托管代码中重新声明一下非托管代码中的函数signature,就可以在接下来的代码中使用这个函数。下面是一个P/Invoke的示例。
[AppleScript] 纯文本查看 复制代码
void print_line(const char* str);   //native code

[DllImport("NativeLib.dll", CharSet = CharSet.Unicode)]  //managed code
private static extern void print_line([MarshalAs(UnmanagedType.LPStr)]string str);



托管代码和非托管代码的类型需要转换。比如char*需要转换成string,void*需要转换成IntPtr。有些转换可以直接完成,有些则必须借助Marshal提供的序列化和反序列化服务。这些都是通过Attribute去指定的。


实际上在.net2.0中,Marshal还提供函数指针的转换。托管代码中的delegate和非托管代码中的函数指针,通过下面两个接口实现了互转。(Marshal.GetFunctionPointerForDelegate Method (Delegate)Marshal.GetDelegateForFunctionPointer Method (IntPtr, Type)
[AppleScript] 纯文本查看 复制代码
[SecurityCriticalAttribute]
public static IntPtr GetFunctionPointerForDelegate(
        Delegate d
)

[SecurityCriticalAttribute]
public static Delegate GetDelegateForFunctionPointer(
        IntPtr ptr,
        Type t
)

这样就有两种用法,一种是通过Marshal.GetDelegateForFunctionPointer()从native library得到指针,然后用DynamicInvoke()方法调用native code(也有直接调用的方法)(https://stackoverflow.com/questions/12858340/difference-between-invoke-and-dynamicinvoke )。如下所示:
IntPtr funcPtr = GetProcAddress(library, typeof(T).Name);var func = Marshal.GetDelegateForFunctionPointer(funcPtr, typeof(T));func.DynamicInvoke(pars);



另一种是通过Marshal.GetFunctionPointerForDelegate()将delegate转换为函数指针,然后以回调的方式注册到native code中,由native code进行调用(Delegates as Callbacks Part 1Delegates As Callbacks Part 2)。某些平台还指明了managed callback function必须用MonoPInvokeCallbackAttribute标注才能被调用(https://stackoverflow.com/questions/18288940/is-there-any-possibility-to-invoke-managed-methods-from-c-in-xamarin-ios)。
[AppleScript] 纯文本查看 复制代码
CallBack callback_delegate = new CallBack(CallBackFunction);
IntPtr intptr_delegate = Marshal.GetFunctionPointerForDelegate(callback_delegate);
SetNativeCallBack(intptr_delegate);




需要注意的是,C#中有GC,如果C#中new出来的内存传给native code使用,就要确保使用期间不会被回收。这时通过GCHandle.Alloc/Free控制这块内存不被GC回收就变得特别重要。


[AppleScript] 纯文本查看 复制代码
// Pin "objectToBePinned"
GCHandle handle = GCHandle.Alloc(objectToBePinned, GCHandleType.Pinned);
... // call native funcs
// Unpin "objectToBePinned"
handle.Free();




COM将库的边界统一到COM这套规范中,而DllExport将native库导出成Flat API。如果C#库得到扩展(比如使用自定义Attribute),将managed库也导出成Flat API,那么native code就能无障碍的使用managed code提供的接口(https://sites.google.com/site/robertgiesecke/Home/uploads/unmanagedexports)。否则就需要写一个C++/CLI的Wrapper做类似的事情了。
知乎@realTOM



跟我念“站长妹纸萌萌哒!”我说站长,你说YO!爱你们么么哒~
回复

使用道具 举报

6蛮牛粉丝
1005/1500
排名
2120
昨日变化

0

主题

68

帖子

1005

积分

Rank: 6Rank: 6Rank: 6

UID
65205
好友
0
蛮牛币
3595
威望
0
注册时间
2015-1-2
在线时间
299 小时
最后登录
2019-1-10
发表于 2019-1-10 11:19:45 | 显示全部楼层
非常好的文章,學習了

回复 支持 反对

使用道具 举报

6蛮牛粉丝
1346/1500
排名
2026
昨日变化
1

10

主题

206

帖子

1346

积分

Rank: 6Rank: 6Rank: 6

UID
252607
好友
5
蛮牛币
3496
威望
0
注册时间
2017-11-5
在线时间
462 小时
最后登录
2019-1-18
发表于 2019-1-10 11:26:06 | 显示全部楼层
23333333333333333333333

回复 支持 反对

使用道具 举报

7日久生情
1833/5000
排名
933
昨日变化
1

0

主题

116

帖子

1833

积分

Rank: 7Rank: 7Rank: 7Rank: 7

UID
35660
好友
0
蛮牛币
2688
威望
0
注册时间
2014-7-22
在线时间
619 小时
最后登录
2019-1-14
发表于 2019-1-10 12:10:10 | 显示全部楼层
挺好的文章的,看完挺有收货的

回复 支持 反对

使用道具 举报

5熟悉之中
618/1000
排名
4060
昨日变化
17

0

主题

87

帖子

618

积分

Rank: 5Rank: 5

UID
71644
好友
0
蛮牛币
1025
威望
0
注册时间
2015-1-28
在线时间
175 小时
最后登录
2019-1-18
发表于 2019-1-10 17:02:04 | 显示全部楼层
不错的文档

回复

使用道具 举报

7日久生情
1947/5000
排名
1631
昨日变化
5

0

主题

628

帖子

1947

积分

Rank: 7Rank: 7Rank: 7Rank: 7

UID
135463
好友
0
蛮牛币
95
威望
0
注册时间
2016-1-23
在线时间
541 小时
最后登录
2019-1-20
发表于 2019-1-10 18:09:14 | 显示全部楼层
66666666666666666677777777777777777
[发帖际遇]: wasdml123 乐于助人,奖励 3 蛮牛币. 幸运榜 / 衰神榜

回复 支持 反对

使用道具 举报

5熟悉之中
723/1000
排名
63814
昨日变化
86

0

主题

533

帖子

723

积分

Rank: 5Rank: 5

UID
73320
好友
0
蛮牛币
12
威望
0
注册时间
2015-2-5
在线时间
188 小时
最后登录
2019-1-19
发表于 2019-1-11 08:41:00 | 显示全部楼层
it is very good

回复

使用道具 举报

5熟悉之中
518/1000
排名
16718
昨日变化
5

1

主题

195

帖子

518

积分

Rank: 5Rank: 5

UID
213085
好友
5
蛮牛币
379
威望
0
注册时间
2017-3-20
在线时间
282 小时
最后登录
2019-1-19
发表于 2019-1-11 09:05:03 | 显示全部楼层

回复

使用道具 举报

2初来乍到
137/150
排名
16718
昨日变化
423

2

主题

63

帖子

137

积分

Rank: 2Rank: 2

UID
297575
好友
0
蛮牛币
390
威望
0
注册时间
2018-9-19
在线时间
34 小时
最后登录
2019-1-18
发表于 2019-1-12 21:47:23 | 显示全部楼层
[发帖际遇]: yongchang27 乐于助人,奖励 1 蛮牛币. 幸运榜 / 衰神榜

回复

使用道具 举报

7日久生情
2041/5000
排名
1176
昨日变化
4

1

主题

639

帖子

2041

积分

Rank: 7Rank: 7Rank: 7Rank: 7

UID
206337
好友
3
蛮牛币
2745
威望
0
注册时间
2017-6-5
在线时间
429 小时
最后登录
2019-1-19
发表于 6 天前 | 显示全部楼层

回复

使用道具 举报

2初来乍到
137/150
排名
16718
昨日变化
423

2

主题

63

帖子

137

积分

Rank: 2Rank: 2

UID
297575
好友
0
蛮牛币
390
威望
0
注册时间
2018-9-19
在线时间
34 小时
最后登录
2019-1-18
发表于 5 天前 | 显示全部楼层
感觉挺叼的

回复

使用道具 举报

排名
166
昨日变化

9

主题

419

帖子

3887

积分

Rank: 7Rank: 7Rank: 7Rank: 7

UID
7264
好友
1
蛮牛币
16448
威望
0
注册时间
2013-11-6
在线时间
1201 小时
最后登录
2019-1-18

VIP社区QQ达人活力之星

QQ
发表于 4 天前 | 显示全部楼层
学习了学习了

回复

使用道具 举报

排名
166
昨日变化

9

主题

419

帖子

3887

积分

Rank: 7Rank: 7Rank: 7Rank: 7

UID
7264
好友
1
蛮牛币
16448
威望
0
注册时间
2013-11-6
在线时间
1201 小时
最后登录
2019-1-18

VIP社区QQ达人活力之星

QQ
发表于 4 天前 | 显示全部楼层
学习了学习了

回复

使用道具 举报

2初来乍到
118/150
排名
21857
昨日变化
900

1

主题

77

帖子

118

积分

Rank: 2Rank: 2

UID
310426
好友
0
蛮牛币
172
威望
0
注册时间
2019-1-2
在线时间
18 小时
最后登录
2019-1-18
发表于 4 天前 | 显示全部楼层
{:104:}
[发帖际遇]: ApolloAR 乐于助人,奖励 2 蛮牛币. 幸运榜 / 衰神榜

回复

使用道具 举报

5熟悉之中
500/1000
排名
5073
昨日变化
3

0

主题

87

帖子

500

积分

Rank: 5Rank: 5

UID
282652
好友
1
蛮牛币
2805
威望
0
注册时间
2018-5-24
在线时间
135 小时
最后登录
2019-1-17
发表于 3 天前 | 显示全部楼层

回复

使用道具 举报

您需要登录后才可以回帖 登录 | 注册帐号

本版积分规则

快速回复 返回顶部 返回列表