c#引用(ref)的用法和应用实例

无论是浅拷贝与深拷贝,C#都将源对象中的所有字段复制到新的对象中。不过,对于值类型字段,引用类型字段以及字符串类型字段的处理,两种拷贝方式存在一定的区别(见下表)。

1. 一般对C#中传值调用和传引用调用的理解

  • 如果传递的参数是基元类型(int,float等)或结构体(struct),那么就是传值调用。
  • 如果传递的参数是类(class)那么就是传引用调用。
  • 如果传递的参数前有ref或者out关键字,那么就是传引用调用。

验证示例的代码如下:

public class ArgsByRefOrValue
    {
        public static void Main(string[] args)
        {
            // 实验1. 传值调用--基元类型
            int i = 10;
            Console.WriteLine("before call ChangeByInt: i = " + i.ToString());
            ChangeByInt(i);
            Console.WriteLine("after call ChangeByInt: i = " + i.ToString());
            Console.WriteLine("==============================================");
            // 实验2. 传值调用--结构体
            Person_val p_val = new Person_val();
            p_val.name = "old val name";
            Console.WriteLine("before call ChangeByStruct: p_val.name = " + p_val.name);
            ChangeByStruct(p_val);
            Console.WriteLine("after call ChangeByStruct: p_val.name = " + p_val.name);
            Console.WriteLine("==============================================");
            // 实验3. 传引用调用--类
            Person_ref p_ref = new Person_ref();
            p_ref.name = "old ref name";
            Console.WriteLine("before call ChangeByClass: p_ref.name = " + p_ref.name);
            ChangeByClass(p_ref);
            Console.WriteLine("after call ChangeByClass: p_ref.name = " + p_ref.name);
            Console.WriteLine("==============================================");
            // 实验4. 传引用调用--利用ref
            Person_ref p = new Person_ref();
            p.name = "old ref name";
            Console.WriteLine("before call ChangeByClassRef: p.name = " + p.name);
            ChangeByClassRef(ref p);
            Console.WriteLine("after call ChangeByClassRef: p.name = " + p.name);
            Console.ReadKey(true);
        }
        static void ChangeByInt(int i)
        {
            i = i + 10;
            Console.WriteLine("when calling ChangeByInt: i = " + i.ToString());
        }
        static void ChangeByStruct(Person_val p_val)
        {
            p_val.name = "new val name";
            Console.WriteLine("when calling ChangeByStruct: p_val.name = " + p_val.name);
        }
        static void ChangeByClass(Person_ref p_ref)
        {
            p_ref.name = "new ref name";
            Console.WriteLine("when calling ChangeByClass: p_ref.name = " + p_ref.name);
        }
        static void ChangeByClassRef(ref Person_ref p)
        {
            p.name = "new ref name";
            Console.WriteLine("when calling ChangeByClassRef: p.name = " + p.name);
        }
    }
    public struct Person_val
    {
        public string name;
    }
    public class Person_ref
    {
        public string name;
    }

运行结果如下:

看起来似乎上面代码中实验3实验4是一样的,即对于类(class)来说,不管加不加ref或out,都是传引用调用。

其实,这只是表面的现象,只要稍微改一下代码,结果就不一样了。

修改上面代码,再增加两个实验。

 public class ArgsByRefOrValue
    {
        public static void Main(string[] args)
        {
            // 实验1. 传值调用--基元类型
            int i = 10;
            Console.WriteLine("before call ChangeByInt: i = " + i.ToString());
            ChangeByInt(i);
            Console.WriteLine("after call ChangeByInt: i = " + i.ToString());
            Console.WriteLine("==============================================");
            // 实验2. 传值调用--结构体
            Person_val p_val = new Person_val();
            p_val.name = "old val name";
            Console.WriteLine("before call ChangeByStruct: p_val.name = " + p_val.name);
            ChangeByStruct(p_val);
            Console.WriteLine("after call ChangeByStruct: p_val.name = " + p_val.name);
            Console.WriteLine("==============================================");
            // 实验3. 传引用调用--类
            Person_ref p_ref = new Person_ref();
            p_ref.name = "old ref name";
            Console.WriteLine("before call ChangeByClass: p_ref.name = " + p_ref.name);
            ChangeByClass(p_ref);
            Console.WriteLine("after call ChangeByClass: p_ref.name = " + p_ref.name);
            Console.WriteLine("==============================================");
            // 实验4. 传引用调用--利用ref
            Person_ref p = new Person_ref();
            p.name = "old ref name";
            Console.WriteLine("before call ChangeByClassRef: p.name = " + p.name);
            ChangeByClassRef(ref p);
            Console.WriteLine("after call ChangeByClassRef: p.name = " + p.name);
            Console.WriteLine("==============================================");
            // 实验5. 传引用调用--类 在调用的函数重新new一个对象
            Person_ref p_ref_new = new Person_ref();
            p_ref_new.name = "old new ref name";
            Console.WriteLine("before call ChangeByClassNew: p_ref_new.name = " + p_ref_new.name);
            ChangeByClassNew(p_ref_new);    //注意区别这里
            Console.WriteLine("after call ChangeByClassNew: p_ref_new.name = " + p_ref_new.name);
            Console.WriteLine("==============================================");
            // 实验6. 传引用调用--利用ref 在调用的函数重新new一个对象
            Person_ref p_new = new Person_ref();
            p_new.name = "old new ref name";
            Console.WriteLine("before call ChangeByClassRefNew: p_new.name = " + p_new.name);
            ChangeByClassRefNew(ref p_new);
            Console.WriteLine("after call ChangeByClassRefNew: p_new.name = " + p_new.name);
            Console.ReadKey(true);
        }
        static void ChangeByInt(int i)
        {
            i = i + 10;
            Console.WriteLine("when calling ChangeByInt: i = " + i.ToString());
        }
        static void ChangeByStruct(Person_val p_val)
        {
            p_val.name = "new val name";
            Console.WriteLine("when calling ChangeByStruct: p_val.name = " + p_val.name);
        }
        static void ChangeByClass(Person_ref p_ref)
        {
            p_ref.name = "new ref name";
            Console.WriteLine("when calling ChangeByClass: p_ref.name = " + p_ref.name);
        }
        static void ChangeByClassRef(ref Person_ref p)
        {
            p.name = "new ref name";
            Console.WriteLine("when calling ChangeByClassRef: p.name = " + p.name);
        }
        static void ChangeByClassNew(Person_ref p_ref_new)
        {
            p_ref_new = new Person_ref();   //这样不改变 p_ref_newp_ref_new
            p_ref_new.name = "new ref name";
            Console.WriteLine("when calling ChangeByClassNew: p_ref_new.name = " + p_ref_new.name);
        }
        static void ChangeByClassRefNew(ref Person_ref p_new)
        {
            p_new = new Person_ref();
            p_new.name = "new ref name";
            Console.WriteLine("when calling ChangeByClassRefNew: p_new.name = " + p_new.name);
        }
    }
    public struct Person_val
    {
        public string name;
    }
    public class Person_ref
    {
        public string name;
    }

则运行结果为:

实验5的运行结果似乎说明即使参数是类(class),只要不加ref,也是传值调用。

下面就引出了我的理解。

2. 没有ref时,即使参数为引用类型(class)时,也可算是一种传值调用

参数为引用类型时,传递的是该引用类型的地址的一份拷贝,“该引用类型的地址的一份拷贝”即为传值调用的“值”。

注意这里说传递的是该引用类型的地址的一份拷贝,而不是引用类型的地址。

下面将用图的形式来说明以上实验3,实验5和实验6中内存的情况。

2.1 首先是实验3

实验3的内存图如下,实参是函数ChangeByClass外的Person_ref对象,形参是函数ChangeByClass内的Person_ref对象。

捕获

从图中我们可以看出实参new出来之后就在托管堆上分配了内存,并且在栈上保存了对象的指针。

调用函数ChangeByClass后,由于没有ref参数,所以将栈上的实参p_val拷贝了一份作为形参,注意这里p_val(实参)p_val(形参)是指向托管堆上的同一地址。

所以说没有ref时,即使参数为引用类型(class)时,也可算是一种传值调用,这里的值就是托管堆中对象的地址(0x1000)。

调用函数ChangeByClass后,通过p_val(形参)修改了name属性的值,由于p_val(实参)p_val(形参)是指向托管堆上的同一地址,所以函数外的p_val(实参)的name属性也被修改了。

捕获

2.2 然后是实验5

上面的实验3从执行结果来看似乎是传引用调用,因为形参的改变导致了实参的改变。

下面的实验5就可以看出,p_val(形参)p_val(实参)并不是同一个变量,而是p_val(实参)的一个拷贝。

捕获

从图中可以看出第一步还是和实验3一样,但是在调用函数ChangeByClassNew后,就不一样了。

函数ChangeByClassNew中,对p_val(形参)重新分配了内存(new操作),使其指向了新的地址(0x1100),如下图:

捕获

所以p_val(形参)的name属性改了时候,p_val(实参)的name属性还是没变。

2.3 最后是实验6

我觉得实验6是真正的传引用调用。不废话了,直接上第一个图。

捕获

参数中加了ref关键字之后,其实传递的不是托管堆中对象的地址(0x1000),而是栈上p_val(实参)的地址(0x0001)。

所以这里实参和形参都是栈上的同一个东西,没有什么区别了。我觉得这才是真正的传引用调用。

然后调用了函数ChangeByClassRefNew,函数中对p_val(形参)重新分配了内存(new操作),使其指向了新的地址(0x1100)。

捕获

由于p_val(形参)就是p_val(实参),所以p_val(形参)的name属性改变后,函数ChangeByClassRefNew外的p_val(实参)的name属性也被改变了。

而原先分配的对象(地址0x1000)其实已经没有被引用了,随时会被GC回收。

3. 结论

  • 如果传递的参数是基元类型(int,float等)或结构体(struct),那么就是传值调用。
  • 如果传递的参数前有ref或者out关键字,那么就是传引用调用。
  • 如果传递的参数是类(class)并且没有ref或out关键字:
  1. 如果调用的函数中对参数重新进行了地址分配(new操作),那么执行结果类似传值调用
  2. 如果调用的函数中没有对参数重新进行了地址分配,直接就是使用了传递的参数,那么执行结果类似传引用调用

 

  • 6
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
C#中,ref和out都是用来指定参数按引用传递的关键字。它们的区别在于以下几点: 1. 使用方式:使用ref关键字时,方法的定义和调用方法都必须显式使用ref关键字。而使用out关键字时,只需要在方法的定义中使用out关键字,调用方法时不需要。 2. 初始值:使用ref关键字时,在调用方法之前,传递的参数必须被初始化。而使用out关键字时,可以不对参数进行初始化,因为在方法内部必须将其赋值。 3. 输出参数:使用out关键字时,传递给方法的参数必须在方法内部被重新赋值,即方法必须通过输出参数返回值。而使用ref关键字时,传递给方法的参数可以在方法内部被修改,也可以不被修改。 总结来说,ref关键字用于传递参数,并且方法内部对参数的更改将反映在该变量中,而out关键字一般用于传递输出参数,并且方法必须通过输出参数返回值。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* [C#中out与ref的区别实例解析](https://download.csdn.net/download/weixin_38659812/13992002)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_1"}}] [.reference_item style="max-width: 50%"] - *2* *3* [C#——ref和out的区别](https://blog.csdn.net/Erenlui/article/details/123886739)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_1"}}] [.reference_item style="max-width: 50%"] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值