深入理解参数传递

C# 引用类型传递探究
想想看下面的结果是什么,并分析原因:
namespace ReferenceParameter
{
    
class MyClass
    
{
        
private string msgInfo;
        
public string MsgInfo
        
{
            
get return msgInfo; }
            
set { msgInfo = value; }
        }


        
public MyClass(string param)
        
{
            
this.msgInfo = param;
        }

        
public MyClass()
        
{
            
// 默认构造函数
        }

    }

    
class Program
    
{
        
static void Main(string[] args)
        
{
            MyClass mc 
= new MyClass("A new message.");
            Console.WriteLine(mc.MsgInfo);
            ChangeMsgA(mc);
            Console.WriteLine(mc.MsgInfo);
            ChangeMsgB(mc);
            Console.WriteLine(mc.MsgInfo);
            Console.ReadKey();
        }

        
static void ChangeMsgA(MyClass mc)
        
{
            mc.MsgInfo 
= "Message has been Changed by ChangeMsgA().";
        }

        
static void ChangeMsgB(MyClass mc)
        
{
            mc 
= new MyClass();
            mc.MsgInfo 
= "Message has been Changed by ChangeMsgB().";
        }

    }

}

答案:
new message.
Message has been Changed by ChangeMsgA().
Message has been Changed by ChangeMsgA().

从这样一个基础性的题目来看,MyClass(引用类型)参数的传递应该和值类型的参数传递性质是一样的,都是值传递,那么可以得出下面2个结论:

1)引用类型的按值传递
2)值类型的按值传递

这样说起来可能会觉得有点怪,因为一般我们都认为引用类型变量作为参数传递的是引用。

从上面的示例也可以看出,当调用了ChangeMsgA()方法后,的确是改变了原参数变量内部的MsgInfo的值,但是如果调用ChangeMsgB()却不会影响到原变量本身,基于对象作用域来考虑,的确是这个意思,当一个对象超出了它的作用范围,就会失效,随后被GC回收。

那么根据示例来看,对于引用类型来说一般也是按值传递(传递的是变量内容的一个副本),只不过这个值,是原变量对托管堆中内容的引用。看图说明:
                    
由图很明显可以看出,第一次调用ChangeMsgA(),其内部改变的是mc所指向的托管堆中的值
而第二次调用ChangeMsgB(),由于其内部构造了一个新的MyClass,此时副本将指向新的引用(右图),所以对其内容的操作并不会影响到外部的MyClass mc。

那么如何改变外部的值?
1)值类型的按引用传递
2)引用类型的按引用传递


使用ref关键字,那么这时传递的参数mc将不再是引用,而是引用的引用(类似于指针),或许这样说起来很拗口,那么可以这么认为:

当使用ref关键字后:
static void ChangeMsgB(ref MyClass mc)
{
      mc 
= new MyClass();
      mc.MsgInfo 
= "Message has been Changed by ChangeMsgB().";
}

这里的MyClass mc仅仅指的是外部的mc的一个别名,并不会在堆栈中再分配一个变量副本,而这个别名本身也恰恰就是外部的mc本身,所以使用了ref关键字后,在函数内部的操作,就等于直接操作外部的变量。

一个补充的小问题:

使用ref关键字和out关键字,本质上传递的都是引用的引用,而ref关键字要求参数必须在外部初始化,out关键字则必须在函数体内重新初始化,另外CLR不支持仅仅是ref和out不同的函数重载,例如下面这种方式:
static void ChangeMsgB(ref MyClass mc) // ref关键字
{
      mc 
= new MyClass();
      mc.MsgInfo 
= "Message has been Changed by ChangeMsgB().";
}


static void ChangeMsgB(out MyClass mc) // out关键字
{
      mc 
= new MyClass();
      mc.MsgInfo 
= "Message has been Changed by ChangeMsgB().";
}

实际上面这样两段代码在一起编译时就不能通过,那么我们来看一下,翻译后的IL代码,就知道为什么了:
MyClass mc = new MyClass();
ChangeMsgB(
ref mc);

MyClass mc 
= new MyClass();
ChangeMsgB(
out mc);

上面这两部分都会被编译为下面的IL代码:
IL_0009:  call      void dotNetNecessary.Program::ChangeMsgB(class dotNetNecessary.MyClass&)

可以看出来,当call一个ChangeMsgB()的静态方法时,参数后都会被标记为&(类似于C中的传址),所以并不能通过仅仅是ref和out关键字的不同来进行重载,因为生成的IL代码都一样,JIT编译时将会无法区分到底是该调用哪个版本的重载。

Plus:最近读Anytao的《你必须知道的.NET》一书的时候,才试着学习通过IL去寻找本质上的区别,再根据自己的理解,用语言和图片的形式表达出来,题目虽然基础但是如果我们能更深入一点,那么印象才会更加深刻,理解得也会更透彻:)

转载于:https://www.cnblogs.com/cnxcfeng/archive/2008/05/21/1204088.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值