C语言[C#]浅析ref、out参数

 转载:http://www.cnblogs.com/vd630/p/4601919.html\#top

据引用传递的参数算是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:不是由此传参的方式传入的变量当然是足以被改动之,本文不针对这种情况举行讨论。

亚、参数传递的凡啊

按值传参传的即是值咯,按引用传参传的便是引用咯,这么简单的题材还有啥可讨论的啊。
可想同一纪念,值类型变量和援类型变量组合及遵循值传参和准引用传参,一共季栽状态,某些情况下“值”和“引用”可能乘的凡和一个事物。

事先简单地于变量说打吧,一个变量总是与内存中之一个靶相关联。
于值类型的变量,可以当它们总是噙两单消息,一凡援引,二凡目标的值。前者即凡是依赖于后者的援。
于引用类型的变量,可以当它为含有两只信息,一凡是援引,二凡另一个援。前者还是是指为后者的援,而后者则指向堆中之对象。

所谓的按值传递,就是传递的“二”;按引用传递,就是传递的“一”。
也就是说,在按值传递一个援类型的早晚,传递的价值的情节是一个援。

约莫情形好像于这样:

C语言 1

论值传递时便像是这么:

C语言 2

得视,不管方法中针对“值”和“B引用”作什么修改,两独变量包含的音是无见面生出其他变动之。
然也堪看,方法中是足以经过“B引用”对“引用类型对象”进行改动的,这就是出现了前文所说之生在List上的气象。
倘按引用传递时即便比如是这么:

C语言 3

得视,这个上方法中是得透过“引用”和“A引用”直接修改变量的音讯之,甚至可能有如此的状:

C语言 4

其一上的不二法门实现可能是这般的:

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: 带ref的方法里多了一条指令“ldind.ref”,关于这条指令MSDN的解释是这样的:stfld int32 CsConsole.Class::i
        IL_0008: ret
    } // end of method Class::Method
} // end of class CsConsole.Class

带ref的办法里大多了相同修指令“ldind.ref”,关于这长长的指令MSDN的解释是这般的:

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

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: cal

别甚清楚,前者由局部变量表取“值”,后者于一些变量表取“引用”。

季、引用和指针

说了这般久引用,再来拘禁无异圈同样可以据此来写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)
        .maxstack 8

        IL_0000: ldarg.1
        IL_0001: initobj CsConsole.Struct
        IL_0007: ret
    } // end of method Struct::Method

    .method private hidebysig 
        instance void Method (
            valuetype CsConsole.Struct* 'struct'
        ) cil managed 
    {
        // Method begins at RVA 0x2059
        // Code size 8 (0x8)
        .maxstack 8

        IL_0000: ldarg.1
        IL_0001: initobj CsConsole.Struct
        IL_0007: ret
    } // end of method Struct::Method

} // end of class CsConsole.Struct

少独方法体的IL是同等型一样的!可以推论引用的庐山真面目到底是呀了咔嚓~?

五、this和引用

是妙不可言的题目是前面片天才发现及的,以前从没有写了类似这样的代码:

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

    public void Test()
    {
        Method(ref this);
    }
}

面就段代码是可由此编译的,但是只要如下这样描绘就充分了:

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

    void Test()
    {
        // 无法将“<this>”作为 ref 或 out 参数传递,因为它是只读的
        Method(ref this);
    }
}

红字部分代码会报生而注所陈述之谬误。两段落代码唯一的差距在前者是struct(值类型)而后者是class(引用类型)。
前方已经说了,ref标记的参数在方式中的修改会潜移默化到方式外的变量值,所以用ref标记this传入方法可能致this的价为移。
有趣的是,为什么struct里的this允许给更改,而class里之this不允许被改变呢?

望生之始末跟ref其实没啥太好关系了,但是关乎到值和援,所以还是延续写吧:D

MSDN本着“this”关键字之说是如此的:

this 关键字引用类的当前实例

这边的“当前实例”指的是外存中的目标,也即是产图备受之“值”或“引用类型对象”:

C语言 5

假设对值类型的this进行赋值,那么“值”被改,“当前实例”仍然是本来实例对象,只是情变了。
万一如果对援类型的this进行复制,那么“B引用”被改动,出现了近似于这个图的景象,现在之“当前实例”已经休是本的实例对象了,this关键字之义就是不再明显。所以引用类型中之this应该是就读之,确保“this”就是依为的“这个”对象。