前一篇我们探索了类型的第一种成员:字段。字段在IL编译时,会生成MdToken和偏移量,因为对于类型来说,一个类型在编译时就已经确定了字段的个数,所以偏移量对于编译器来说是已知的,字段和偏移量分别由元数据表(Field和ClassLayout)来记录。
本篇我们来讨论类型中的另一种成员:方法.在本系列的第一篇探索CLR原理系列(1):类型 中我们说到类型中只有两种成员:字段和方法.字段是用来描述类型的状态,而方法则提供了类型所具备的功能.首先我们来看看方法在IL中是如何描述的.先来定义一个类型.
{
private string name= " xulei ";
private int age= 10;
public static int Add(int a,ref int b) // 静态
{
return a + b;
}
private static int Sub(out int a,out int b) // 私有静态
{
a = 10;
b = 5;
return a - b;
}
public void WriteName() // 公有
{
Console.Write(name);
}
protected int GetAge() // 受保护
{
return age;
}
private void WriteNameAndAge() // 私有
{
Console.Write(name + age.ToString());
}
这个类型中我们定义了2个不同访问级别静态方法,3个不同访问级别的实例方法.下面我们来看看这些方法的元数据。
-------------------------------------------------------
TypDefName: TestDemo1.Type.MethodClass ( 02000003) //类型标识
Method # 1 ( 06000003) //方法标识
-------------------------------------------------------
MethodName: Add (06000003)
Flags : [ P ublic] [ Static]
RVA : 0x00002064
ReturnType: I4
2 Arguments //参数列表
Argument # 1 : I4 //值传递
Argument # 2 : ByRef I4 //引用传递
2 Parameters
( 1) ParamToken : ( 08000001) Name : a flags: [none]
( 2) ParamToken : ( 08000002) Name : b flags: [none]
Method # 2 ( 06000004)
-------------------------------------------------------
MethodName: Sub ( 06000004)
Flags : [ Private] [Static]
RVA : 0x0000207c
ReturnType: I4 这部分黄色的就是方法签名
2 Arguments
Argument #1:ByRef I4
Argument #2:ByRef I4
2 Parameters 这部分是参数元数据表的内容
(1) ParamToken : (08000003) Name : a flags: [out]
(2) ParamToken : (08000004) Name : b flags: [out]
Method #3 (06000005)
-------------------------------------------------------
MethodName: WriteName (06000005)
Flags : [Public]
RVA : 0x00002091
hasThis 方法签名
ReturnType: Void
No arguments.
Method #4 (06000006)
-------------------------------------------------------
MethodName: GetAge (06000006)
Flags : [Family]
RVA : 0x000020a0
hasThis 方法签名
ReturnType: I4
No arguments.
Method #5 (06000007)
-------------------------------------------------------
MethodName: WriteNameAndAge (06000007)
Flags : [Private]
RVA : 0x000020b8
hasThis 方法签名
ReturnType: Void
No arguments.
可以看到在方法的原数据中包括了对方法的完整的描述,代码中高亮部分有二部分内容,方法返回值,方法参数.这二部分内容称之为方法签名,大家注意静态方法和实例方法有什么区别?仔细看一下,多了一个HasThis,关于原因我们在后面讨论.我们前一篇说过字段在Dll中有一个字段元数据表来描述所有的字段,那么方法也同样有一个方法的元数据表.来看看它是什么样子的.
方法的元数据表的Token是以0X06开头的,Token是IL中的标识数据,例如静态方法Add的标识为06000003,flags中标注了可见性,以及抽象,虚,静态,密封等方法的标记.Signature就是方法的签名,由返回值和参数组成。RVA是方法体在Dll文件中的相对地址,也就是方法的代码所在的相对地址。
除此之外,还有方法参数的起始位置.在类型那一篇中,我们讨论过类型中包含了字段起始位置,和方法的起始位置,那么这里的参数起始位置和前面二者的原理是相同的,用来查找方法的参数.来看看方法参数的元数据表吧
在上面的IL代码中Add方法的参数分别为一个值传递,一个引用传递,那么他们在IL中被标示为Byref,并且Out参数的Flags被标注为out,而Ref则与值传递参数的Flags没有任何区别,都为None,那么CLR在运行时如何判断呢?关键在于IL代码中,我们可以看到一个类似于C++中的去地址操作符&.
int32 & b)
这就是引用传递和值传递的区别了,Out参数也一样,只不过在参数原数据表中的Flags中标记了一个Out。那么参数原数据表中Flags的另一个标记Opt是什么意思呢??呵呵,大家可以自己试验一下,定义一个可变参数 params int a 看看。
Sequence是什么意思?他代表参数的位置,也就是第几个参数。所以Sequence的值不能大于方法的参数个数。
也许大家会问,那么参数的类型呢,怎么不在参数元数据表中?这个秘密就是在方法的签名中。。。懂了吗?
总结
我们了解了方法是由元数据和IL代码组成。元数据中包括了方法的签名,方法的参数,以及参数的描述,IL代码中包含了方法的执行逻辑 。每个方法在方法元数据表中都有一条记录,该记录包括方法在编译时的标识Token,以及方法的可见性,契约的特征Flags,还有方法的参数和返回值组成的签名,乃至引用了方法的每一个参数的特征,由参数元数据表来记录这些信息。
下一篇我们来聊聊方法在类型继承中的情况。。。