首页 > 学院 > 开发设计 > 正文

[C#]浅析ref、out参数

2019-11-17 02:18:03
字体:
来源:转载
供稿:网友

[C#]浅析ref、out参数

按引用传递的参数算是C#与很多其他语言相比的一大特色,想要深入理解这一概念应该说不是一件容易的事,再把值类型和引用类型给参杂进来的话就变得更加让人头晕了。经常看到有人把按引用传递和引用类型混为一谈,让我有点不吐不快。再加上前两天碰到的一个有意思的问题,让我更加觉得应该整理整理关于ref和out的内容了。

一、什么是按引用传递

ref和out用起来还是非常简单的,就是在普通的按值传递的参数前加个ref或者out就行,方法定义和调用的时候都得加。ref和out都是表示按引用传递,CLR也完全不区分ref还是out,所以下文就直接以ref为例来进行说明。

大家都知道,按值传递的参数在方法内部不管怎么改变,方法外的变量都不会受到影响,这从学C语言时候就听老师说过的了。在C语言里想要写一个Swap方法该怎么做?用指针咯。那么在C#里该怎么做?虽然也可以用指针,但是更通常也更安全的做法就是用ref咯。

说到这里,有一点需要明确,按值传递的参数到底会不会被改变。如果传的是int参数,方法外的变量肯定是完完全全不变的咯,可是如果传的是个List呢?方法内部对这个List的所有增删改都会反映到方法外头,方法外查一下Count就能看出来了是吧。那么传List的这个情况,也代表了所有引用类型参数的情况,方法外的变量到底变没变?不要听信某些论调说什么“引用类型就是传引用”,不用ref的情况下引用类型参数仍然传的是“值”,所以方法外的变量仍然是不变的。

以上总结起来就是一句话:按值传递参数的方法永远不可能改变方法外的变量,需要改变方法外的变量就必须按引用传递参数。

PS:不是通过传参的方式传入的变量当然是可以被改变的,本文不对这种情况做讨论。

二、参数传递的是什么

按值传参传的就是值咯,按引用传参传的就是引用咯,这么简单的问题还有啥可讨论的呢。可是想一想,值类型变量和引用类型变量组合上按值传参和按引用传参,一共四种情况,某些情况下“值”和“引用”可能指的是同一个东西。

先简单地从变量说起吧,一个变量总是和内存中的一个对象相关联。对于值类型的变量,可以认为它总是包含两个信息,一是引用,二是对象的值。前者即是指向后者的引用。对于引用类型的变量,可以认为它也包含两个信息,一是引用,二是另一个引用。前者仍然是指向后者的引用,而后者则指向堆中的对象。

所谓的按值传递,就是传递的“二”;按引用传递,就是传递的“一”。也就是说,在按值传递一个引用类型的时候,传递的值的内容是一个引用。

大概情况类似于这样:

按值传递时就像是这样:

可以看到,不管方法内部对“值”和“B引用”作什么修改,两个变量包含的信息是不会有任何变化的。但是也可以看到,方法内部是可以通过“B引用”对“引用类型对象”进行修改的,这就出现了前文所说的发生在List上的现象。而按引用传递时就像是这样:

可以看到,这个时候方法内部是可以通过“引用”和“A引用”直接修改变量的信息的,甚至可能发生这样的情况:

这个时候的方法实现可能是这样的:

void SampleMethod(ref object obj){    //.....    obj = new object();    //.....}

三、从IL来看差异

接下来看一看IL是怎么对待按值或者按引用传递的参数。比如这一段C#代码:

class Class{    void Method(Class @class) { }    void Method(ref Class @class) { }    // void Method(out Class @class) { }}

这一段代码是可以正常通过编译的,但是取消注释就不行了,原因前面也提到了,IL是不区分ref和out的。也正是因为这一种重载的可能性,所以在调用方也必须写明ref或out,不然编译器没法区分调用的是哪一个重载版本。Class类的IL是这样的:

.class PRivate auto ansi beforefieldinit CsConsole.Class    extends [mscorlib]System.Object{    // Methods    .method private hidebysig static         void Method (            class CsConsole.Class 'class'        ) cil managed     {        // Method begins at RVA 0x20b4        // Code size 1 (0x1)        .maxstack 8        IL_0000: ret    } // end of method Class::Method    .method private hidebysig static         void Method (            class CsConsole.Class& 'class'        ) cil managed     {        // Method begins at RVA 0x20b6        // Code size 1 (0x1)        .maxstack 8        IL_0000: ret    } // end of method Class::Method} // end of class CsConsole.Class

为了阅读方便,我把原有的默认无参构造函数去掉了。可以看到两个方法的IL仅仅只有一个&符号的差别,这一个符号的差别也是两个方法可以同名的原因,因为它们的参数类型是不一样的。out和ref参数的类型则是一样的。现在给代码里加一点内容,让差别变得更明显一些:

class Class{    int i;    void Method(Class @class)    {        @class.i = 1;    }    void Method(ref Class @class)    {        @class.i = 1;    }}

现在的IL是这样的:

.class private auto ansi beforefieldinit CsConsole.Class    extends [mscorlib]System.Object{    // Fields    .field private int32 i    // Methods    .method private hidebysig         instance void Method (            class CsConsole.Class 'class'        ) cil managed     {        // Method begins at RVA 0x20b4        // Code size 8 (0x8)        .maxstack 8        IL_0000: ldarg.1        IL_0001: ldc.i4.1        IL_0002: stfld int32 CsConsole.Class::i        IL_0007: ret    } // end of method Class::Method    .method private hidebysig         instance void Method (            class CsConsole.Class& 'class'        ) cil managed     {        // Method begins at RVA 0x20bd        // Code size 9 (0x9)        .maxstack 8        IL_0000: ldarg.1        IL_0001: ldind.ref        IL_0002: ldc.i4.1        IL_0003: stfld int32 CsConsole.Class::i        IL_0008: ret    } // end of method Class::Method} // end of class CsConsole.Class

带ref的方法里多了一条指令“ldind.ref”,关于这条指令MSDN的解释是这样的:

将对象引用作为 O(对象引用)类型间接加载到计算堆栈上。

简单来说就是从一个地址取了一个对象引用,这个对象引用与无ref版本的“arg.1”相同的,即按值传入的@class。再来换一个角度看看,把代码改成这样:

class Class{    void Method(Class @class)    {        @class = new Class();    }    void Method(ref Class @class)    {        @class = new Class();    }}

IL是这样的:

.class private auto ansi beforefieldinit CsConsole.Class    extends [mscorlib]System.Object{    // Methods    .method private hidebysig         instance void Method (            class CsConsole.Class 'class'        ) cil managed     {        // Method begins at RVA 0x20b4        // Code size 8 (0x8)        .maxstack 8        IL_0000: newobj instance void CsConsole.Class::.ctor()        IL_0005: starg.s 'class'        IL_0007: ret    } // end of method Class::Method    .method private hidebysig         instance void Method (            class CsConsole.Class& 'class'        ) cil managed     {        // Method begins at RVA 0x20bd        // Code size 8 (0x8)        .maxstack 8        IL_0000: ldarg.1        IL_0001: newobj instance void CsConsole.Class::.ctor()        IL_0006: stind.ref        IL_0007: ret    } // end of method Class::Method} // end of class CsConsole.Class

这一次两方的差别就更大了。无ref版本做的事很简单,new了一个Class对象然后直接赋给了@class。但是有ref版本则是先取了ref引用留着待会用,再new了Class,然后才把这个Class对象赋给ref引用指向的地方。在来看看调用方会有什么差异:

class Class{    void Method(Class @class) { }    void Method(ref Class @class) { }    void Caller()    {        Class @class = new Class();        Method(@class);        Method(ref @class);    }}

.method private hidebysig     instance void Caller () cil managed {    // Method begins at RVA 0x20b8    // Code size 22 (0x16)    .maxstack 2    .locals init (        [0] class CsConsole.Class 'class'    )    IL_0000: newobj instance void CsConsole.Class::.ctor()    IL_0005: stloc.0    IL_0006: ldarg.0    IL_0007: ldloc.0    IL_0008: call instance void CsConsole.Class::Method(class CsConsole.Class)    IL_000d: ldarg.0    IL_000e: ldloca.s 'class'    IL_0010: call instance void CsConsole.Class::Method(class CsConsole.Class&)    IL_0015: ret} // end of method Class::Caller

差别很清晰,前者从局部变量表取“值”,后者从局部变量表取“引用”。

四、引用与指针

说了这么久引用,再来看一看同样可以用来写Swap的指针。很显然,ref参数和指针参数的类型是不一样的,所以这么写是可以通过编译的:

unsafe struct Struct{    void Method(ref Struct @struct) { }    void Method(Struct* @struct) { }}

这两个方法的IL非常有意思:

.class private sequential ansi sealed beforefieldinit CsConsole.Struct    extends [mscorlib]System.ValueType{    .pack 0    .size 1    // Methods    .method private hidebysig         instance void Method (            valuetype CsConsole.Struct& 'struct'        ) cil managed     {        // Method begins at RVA 0x2050        // Code size 1 (0x1)        .maxstack 8        IL_0000: ret    } // end of method Struct::Method    .method private hidebysig         instance void Method (            valuetype CsConsole.Struct* 'struct'        ) cil managed     {        // Method begins at RVA 0x2052        // Code size 1 (0x1)        .maxstack 8        IL_0000: ret    } // end of method Struct::Method} // end of class CsConsole.Struct

ref版本是用了取地址运算符(&)来标记,而指针版本用的是间接寻址运算符(*),含义也都很明显,前者传入的是一个变量的地址(即引用),后者传入的是一个指针类型。更有意思的事情是这样的:

unsafe struct Struct{    void Method(ref Struct @struct)    {        @struct = default(Struct);    }    void Method(Struct* @struct)    {        *@struct = default(Struct);    }}
.class private sequential ansi sealed beforefieldinit CsConsole.Struct    extends [mscorlib]System.ValueType{    .pack 0    .size 1    // Methods    .method private hidebysig         instance void Method (            valuetype CsConsole.Struct& 'struct'        ) cil managed     {        // Method begins at RVA 0x2050        // Code size 8 (0x8)        .maxst
发表评论 共有条评论
用户名: 密码:
验证码: 匿名发表