C#中数据类型分为两种:值类型和引用类型,值类型为:基本类型(枚举、数值、布尔)、String、Struct类型,而其他均为引用类型。在引用类型中,reference标注是否对该类型无影响呢?
测试程序如下:
class A {
public A()
{
Console.WriteLine("A::A()");
}
public string val;
};
static void test(ref A name)
{
name.val = "ref helloA";
}
static void test(A name)
{
name.val = "helloA";
}
对于这样的两个方法,生成的IL是不一样的,但执行结果是一样的:
.method private hidebysig static void test(class TestRef.A& name) cil managed
{
// 代码大小 14 (0xe)
.maxstack 8//栈大小
IL_0000: nop
IL_0001: ldarg.0//加载name指针
IL_0002: ldind.ref //以引用方式加载name的值,ind:indirect,ref:reference
IL_0003: ldstr "ref helloA"//加载string对象
IL_0008: stfld string TestRef.A::val//将val赋值为string对象
IL_000d: ret
} // end of method Program::test
.method private hidebysig static void test(class TestRef.A name) cil managed
{
// 代码大小 13 (0xd)
.maxstack 8
IL_0000: nop
IL_0001: ldarg.0
IL_0002: ldstr "helloA"
IL_0007: stfld string TestRef.A::val
IL_000c: ret
} // end of method Program::test
引用参数ref在生成代码是多了一个受限指针类型,也就是C++中的引用类型。所有对象存放在堆中,如不加引用传递的也是name的地址(*name),修改name成员均有效,而引用类型传递的是name地址的地址(也就是指针的指针**name),这样就可以修改name地址,这也就说明了ref与out参数的区别。因所有对象按地址传递故也不存在构造函数生成临时对象的场景。
out参数的含义为该参数未进行初始化,需重新初始化,引用类型按引用传递该关键字含义如何?static void test( A name)
{
name = null;
}
static void test(out A name)
{
name = null;
}
对于这样的两个方法生成的IL如下:
但两个IL执行结果完全不同,out方法产生了空值异常。
.method private hidebysig static void test([out] class TestRef.A& name) cil managed
{
// 代码大小 5 (0x5)
.maxstack 8
IL_0000: nop
IL_0001: ldarg.0//ld代表load
IL_0002: ldnull
IL_0003: stind.ref
IL_0004: ret
} // end of method Program::test
.method private hidebysig static void test(class TestRef.A name) cil managed
{
// 代码大小 5 (0x5)
.maxstack 8
IL_0000: nop
IL_0001: ldnull
IL_0002: starg.s name//将值存入name字段,也就是name赋值为null
IL_0004: ret
} // end of method Program::test
从中间代码中可以看到ref,out关键字涉及间接寻址(就是指令stdind.ref),而引用类型传递的name地址,当然就无法修改name地址。而ref、out关键字在生成函数签名时均为多加了一个取地址符&,这样就说明了如果一个方法仅在参数上只有ref/out区别是无法被编译通过的。
形参不加ref或out如下调用时,对象name是不会修改的:static void test(A name)
{
name = new A();
name.val = "new A";
}
生成的IL如下,可以看出涉及对象name根本不会间接寻址:
.method private hidebysig static void test(class TestRef.A name) cil managed
{
// 代码大小 20 (0x14)
.maxstack 8
IL_0000: nop
IL_0001: newobj instance void TestRef.A::.ctor()
IL_0006: starg.s name将值存入name字段,也就是name赋值为newobj
IL_0008: ldarg.0
IL_0009: ldstr "new A"
IL_000e: stfld string TestRef.A::val
IL_0013: ret
} // end of method Program::test
值类型传递参数时,如不加ref/out关键字,则完全无法修改对象内部成员,不同的是作为值类型对象struct,无法定义默认构造函数和析构函数,按照Essential C#中的说法,原因为struct类型初始化直接清零,允许默认构造函数将会造成CLR不一致的行为,而由于按值传递,加入析构函数就意味这该对象资源释放,导致按值传递过程中大量的对象资源被意外释放,这样就会造成对象的访问异常。