想想看下面的结果是什么,并分析原因:
答案:
从这样一个基础性的题目来看,MyClass(引用类型)参数的传递应该和值类型的参数传递性质是一样的,都是值传递,那么可以得出下面2个结论:
1)引用类型的按值传递
2)值类型的按值传递
这样说起来可能会觉得有点怪,因为一般我们都认为引用类型变量作为参数传递的是引用。
从上面的示例也可以看出,当调用了ChangeMsgA()方法后,的确是改变了原参数变量内部的MsgInfo的值,但是如果调用ChangeMsgB()却不会影响到原变量本身,基于对象作用域来考虑,的确是这个意思,当一个对象超出了它的作用范围,就会失效,随后被GC回收。
那么根据示例来看,对于引用类型来说一般也是按值传递(传递的是变量内容的一个副本),只不过这个值,是原变量对托管堆中内容的引用。看图说明:
由图很明显可以看出,第一次调用ChangeMsgA(),其内部改变的是mc所指向的托管堆中的值
而第二次调用ChangeMsgB(),由于其内部构造了一个新的MyClass,此时副本将指向新的引用(右图),所以对其内容的操作并不会影响到外部的MyClass mc。
那么如何改变外部的值?
1)值类型的按引用传递
2)引用类型的按引用传递
使用ref关键字,那么这时传递的参数mc将不再是引用,而是引用的引用(类似于指针),或许这样说起来很拗口,那么可以这么认为:
当使用ref关键字后:
这里的MyClass mc仅仅指的是外部的mc的一个别名,并不会在堆栈中再分配一个变量副本,而这个别名本身也恰恰就是外部的mc本身,所以使用了ref关键字后,在函数内部的操作,就等于直接操作外部的变量。
一个补充的小问题:
使用ref关键字和out关键字,本质上传递的都是引用的引用,而ref关键字要求参数必须在外部初始化,out关键字则必须在函数体内重新初始化,另外CLR不支持仅仅是ref和out不同的函数重载,例如下面这种方式:
实际上面这样两段代码在一起编译时就不能通过,那么我们来看一下,翻译后的IL代码,就知道为什么了:
上面这两部分都会被编译为下面的IL代码:
可以看出来,当call一个ChangeMsgB()的静态方法时,参数后都会被标记为&(类似于C中的传址),所以并不能通过仅仅是ref和out关键字的不同来进行重载,因为生成的IL代码都一样,JIT编译时将会无法区分到底是该调用哪个版本的重载。
Plus:最近读Anytao的《你必须知道的.NET》一书的时候,才试着学习通过IL去寻找本质上的区别,再根据自己的理解,用语言和图片的形式表达出来,题目虽然基础但是如果我们能更深入一点,那么印象才会更加深刻,理解得也会更透彻:)
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().";
}
}
}
{
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().";
}
}
}
答案:
A new message.
Message has been Changed by ChangeMsgA().
Message has been Changed by ChangeMsgA().
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().";
}
{
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().";
}
{
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);
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去寻找本质上的区别,再根据自己的理解,用语言和图片的形式表达出来,题目虽然基础但是如果我们能更深入一点,那么印象才会更加深刻,理解得也会更透彻:)