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

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

按引用传递的参数算是C#与众多任何语言相比较的一大特色,想要深刻掌握这一定义应该说不是一件不难的事,再把值类型和引用类型给参杂进来的话就变得更为令人头晕了。
时常看看有人把按引用传递和引用类型混为一谈,让本身有点不吐非常的慢。再增进前两日遇到的三个有趣的题材,让本人进一步觉得应该整理整理有关ref和out的始最终。

一 、什么是按引用传递

ref和out用起来照旧卓殊简单的,正是在普通的按值传递的参数前加个ref或许out就行,方法定义和调用的时候都得加。
ref和out都以意味按引用传递,CL索罗德也完全不区分ref依旧out,所以下文就径直以ref为例来开始展览求证。

我们都了然,按值传递的参数在章程内部不管怎么改变,方法外的变量都不会受到震慑,那从学C语言时候就听老师说过的了。
在C语言里想要写贰个Swap方法该如何是好?用指针咯。
那么在C#里该如何做?尽管也能够用指针,可是更平凡也更安全的做法就是用ref咯。

说到那里,有少数亟需掌握,按值传递的参数到底会不会被转移。
设若传的是int参数,方法外的变量肯定是完完全全不变的嘞,然则要是传的是个List呢?方法内部对那一个List的具备增加和删除改都会展现到艺术外头,方法外查一下Count就能看出来了是啊。
那么传List的那个境况,也象征了全体引用类型参数的事态,方法外的变量到底变没变?
绝不听信某个论调说什么样“引用类型就是传引用”,不用ref的动静下引用类型参数照旧传的是“值”,所以艺术外的变量依然是不变的。

以上海市总括起来就是一句话:
按值传递参数的艺术永远不容许改变方法外的变量,要求改变方法外的变量就不能够不按引用传递参数。

PS:不是经过传参的办法传入的变量当然是能够被改成的,本文不对那种景色做商讨。

② 、参数字传送递的是哪些

按值传参传的正是值咯,按引用传参传的正是引用咯,这么不难的题材还有啥可研商的啊。
只是想一想,值类型变量和引用类型变量组合上按值传参和按引用传参,一共八种境况,有些景况下“值”和“引用”只怕指的是同四个事物。

先简单地从变量说起吧,一个变量总是和内部存款和储蓄器中的2个目的相关联。
对于值类型的变量,能够认为它总是包涵三个音信,一是引用,二是目的的值。前者就是指向后者的引用。
对此引用类型的变量,能够认为它也包罗七个消息,一是援引,二是另三个引用。前者依然是指向后者的引用,而后者则指向堆中的对象。

所谓的按值传递,正是传递的“二”;按引用传递,正是传递的“一”。
也正是说,在按值传递3个引用类型的时候,传递的值的始末是二个引用。

约莫景况相近于这样:

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,不然编写翻译器没办法区分调用的是哪3个重载版本。
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了3个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版本是用了取地址运算符(&)来标记,而指针版本用的是直接寻址运算符(*),含义也都很显明,前者传入的是三个变量的地点(即引用),后者传入的是3个指针类型。
更遗闻务是那样的:

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”正是指向的“这一个”对象。

相关文章