[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)
        .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 关键字引用类的当前实例

这里的“当前实例”指的是内存中的对象,也就是下图中的“值”或“引用类型对象”:

如果对值类型的this进行赋值,那么“值”被修改,“当前实例”仍然是原来实例对象,只是内容变了。
而如果对引用类型的this进行复制,那么“B引用”被修改,出现了类似于这个图的情况,现在的“当前实例”已经不是原来的实例对象了,this关键字的含义就不再明确。所以引用类型中的this应该是只读的,确保“this”就是指向的“这个”对象。

最后也没想到有啥可多说的,那就到此为止吧~

时间: 2024-10-14 18:02:53

[C#]浅析ref、out参数的相关文章

out参数,ref参数,params参数数组

params参数数组 params关键字可以为方法指定数目可变的参数.params关键字修饰的参数,可以传入任意数目的同类型参数,甚至可以不传入参数. 不过params修饰的参数必须是方法的最后一个参数,并且一个方法只能有一个params修饰的参数. 示例 public class MyClass { public static void UseParams(params int[] list) { for (int i = 0; i < list.Length; i++) { Console.

ref引用类型,数组型参数,out输出参数

ref和out的相同点和不同点 共同点:都是引用传递不同点:ref的参数在调用之前一定要赋值,在方法调用的过程中可以不要赋值.    out的参数在调用之前可以不赋值,在方法调用的过程中一定要赋值. //方法的参数    class Program    {        static void Main(string[] args)        {            //值传递            //int num = 5;            //Change(num);    

C#参数知识盘点,形/实参、值/引用传递、ref、out、in、params、可选、命名等

引言 参数,也叫参变量,是一个变量.在方法签名中随处可见,实现了不同方法间的数据传递,基本上充斥在代码的各个角落里.实参是通过方法调用传递到方法的值的名称,形参是方法期望接收的值.在方法签名或者原型中,方法名称后的括号包含方法的参数及其类型的完整列表.参数声明指定参数中存储的值的类型.大小和标识符.然而小小参数的背后其实也是有着大学问的,所以本篇博文,您可以和博主一起把C#里面各式各样的参数复习一遍.我们先简单回顾一下各种各样的参数概念,对不同类型参数的使用场景有一个了解,再慢慢深入探讨参数的传

c#基础学习(0724)之可变参数、ref和out

params可变参数,无论有几个参数,必须出现在参数列表的最后,可以为可变参数直接传递一个对应类型的数组 #region 可变参数 //1.如果方法有多个参数,可变参数可以作为最后一个参数 //2.可变参数可以传递参数也可以不传递参数,如果不传递参数,则args数组为一个长度为0的数组 //3.可变参数可以直接传递一个数组进来 static void Test(string msg,params int[] args) { //如果可变参数传值为null时,需要加上判断 if(args!=nul

ref、out 修饰符

ref参数和out参数类似,除了: 1.ref参数要求在传入函数之前赋值,而out参数不用 2.out参数必须在函数结束之前被赋值,而ref参数不用 ref传递参数 若int x;则报错 1 class Program 2 { 3 static void Main(string[] args) 4 { 5 int x=0; 6 Foo(ref x); 7 Console.WriteLine(x);//1 8 Console.ReadKey(); 9 } 10 static void Foo(re

Ref与Out的区别

Ref与Out的区别 ref和out都是C#中的关键字,所实现的功能也差不多,都是指定一个参数按照引用传递. 对于编译后的程序而言,它们之间没有任何区别,也就是说它们只有语法区别. 总结起来,他们有如下语法区别: 1.ref传进去的参数必须在调用前初始化,out不必,即: int i; SomeMethod( ref i );//语法错误 SomeMethod( out i );//通过 2.ref传进去的参数在函数内部可以直接使用,而out不可: public void SomeMethod(

关于c#中”ref”和”out”关键字的一些理解

一. 综述(本文内容大部分来自网络,经本人整理而成,仅供学习参考,不免理解错误,欢迎批评指正) 在c#中,方法的参数传递有四种类型: (1) 传值参数(by value) 传值参数无需额外的修饰符.传值参数在方法调用过程中,如果改变了参数的值,那么传入方法的参数在方法调用完成以后并不因此而改变,而是保持原来传入的值.实际 上,传值参数传递的是调用参数的一份拷贝,因此在调用方法的过程中,即使改变了参数的值,也不会影响到实际传入的参数值,详见例程: (2) 传址参数(by reference) 传址

浅析C#值传递与引用值传递

先上一段代码. using UnityEngine; using System.Collections; public class TypePassing : MonoBehaviour { public Dog dog = new Dog (1, "xiaobai") ; //调用默认构造函数给struct里所有字段一个缺省值,对dog来说是null public Cat cat = new Cat ();//貌似在这里不能直接赋值dog属性 int i = 0; void Star

C#基础复习——参数

ref,out,params ref与out1:使用ref型参数时,传入的参数必须先被初始化,对out而言,必须在方法中对其完成初始化.2:使用ref和out时,在方法的参数和执行方法时,都要加ref或out关键字,以满足匹配.3:out适用在需要return多个返回值的地方,而ref则用在需要被调用的方法修改调用者的时候引用. 下面依次来介绍: ref 1:代码参考 2,ref用法:当传递的参数类型是值类型,使用ref参数可以保留修改 out 1:代码如下 2,out参数的用法:就是当一个方法