解析C#中reference与out关键字

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不一致的行为,而由于按值传递,加入析构函数就意味这该对象资源释放,导致按值传递过程中大量的对象资源被意外释放,这样就会造成对象的访问异常。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值