本文要探讨的是引用类型的引用传递,道理是很浅显的(对于基础比较扎实的请飘过,免得浪费时间;而对于实参到形参传递还存有疑惑的,可以继续看下去)。但是笔者之前对此一直不了解,因此这次通过查了园子里多篇相关文章(其中之一为《全面解析C#中参数传递》,并实际测试得到以下结果:
当一个方法的参数为引用类型,那么它的实参到形参的传递一定是引用传递,方法内部形参的改变会使得调用方传入的实参的改变。
下面先说说背景吧。之前用C# WinForm写了一个小程序,其中一个父窗体是入库操作,这里面有一个按钮是增加入库商品,一个DataGridView是显示本次已经选择的要入库的商品,最后点击入库按钮将选择的商品一并入库。DataGridView的数据源用的是DataTable,而选择入库商品我放到了另一个窗体里,也就是子窗体(模态窗体),在子窗体里有个下拉列表框用于选择某个商品,然后点击确定按钮,下拉列表框中当前选定的商品就被增添到要入库的商品中。
接下来我说一下之前我是如何实现的。首先在父窗体中实例化一个DataTable类型变量,变量名假设为dtProduct。然后在增加入库商品按钮的点击事件中实例化子窗体,并以模态窗体方式显示出来。而实例化子窗体时使用子窗体含有一个DataTable类型参数的构造函数。最后我把父窗体实例化的dtProduct作为参数传入子窗体的构造函数中。而点击子窗体的确定按钮时,我希望将用户选择的商品添加到传入的dtProduct,这时父窗体里的实参也会相应改变,于是乎我就在构造函数的参数前加了一个ref修饰符。当时圆满实现需求,并未多想什么。
最近突然想到引用类型的赋值,不是将堆里的值赋给新的变量,而是把栈里对堆的引用赋值给新的变量。于是乎就想到了方法传参这个问题,引用类型传参实际也是将实参给形参赋值,那么我用的DataTable类型本身就是引用类型,传递的就应该是引用,而不是值,为什么还要加ref修饰符呢?带着这个问题查阅了一些关于方法实参和形参的博客文章,并通过实际测试得到前面的结果。
博客文章大家可以看前面提到的那篇,我这里说说我的测试例子:
1 using System; 2 using System.Collections.Generic; 3 using System.Linq; 4 using System.Text; 5 using System.Data; 6 7 namespace console131031 8 { 9 class Program 10 { 11 static void Main(string[] args) 12 { 13 Program p = new Program(); 14 DataTable originalDt = new DataTable(); 15 originalDt.Columns.Add("ID", typeof(int)); 16 originalDt.Columns.Add("Name", typeof(string)); 17 originalDt.Columns.Add("Gender", typeof(bool)); 18 DataRow dr = originalDt.NewRow(); 19 dr[0] = 1; 20 dr[1] = "Jack"; 21 dr[2] = false; 22 originalDt.Rows.Add(); 23 Console.WriteLine("未执行方法前的行数:{0}",originalDt.Rows.Count); 24 DataTable newDt = p.Test(originalDt); 25 Console.WriteLine("执行完方法后的行数:{0}", originalDt.Rows.Count); 26 Console.WriteLine("方法返回值的行数:{0}", newDt.Rows.Count); 27 Console.ReadKey(); 28 } 29 DataTable Test(DataTable dt) 30 { 31 DataRow dr = dt.NewRow(); 32 dr[0] = 2; 33 dr[1] = "Lucy"; 34 dr[2] = true; 35 dt.Rows.Add(dr); 36 Console.WriteLine("方法内部形参的行数:{0}", dt.Rows.Count); 37 return dt; 38 } 39 } 40 }
运行后发现:“执行完方法后实参的行数”为2;而“方法内部形参的行数”也为2。也就是不需要加ref修饰符,引用类型的引用传递也是会通过改变形参而改变实参。因为传递的是引用(可以看成是地址),改变的是引用的堆里面的值。前面推荐的博客里提到4种传递,其中引用类型一般肯定是引用传递(因为引用类型不在栈里存值,而是存对于堆的引用),但是有个例外,就是字符串,字符串是引用类型,但是由于字符串具有不可变性,所以字符串是值传递。而值类型有两种,一种是值传递,另一种是引用传递。如果值类型要实现引用传递,就要加ref修饰符。
到这里算是比较了解方法的实参到形参的传递了。如果上面有说的不对的地方,欢迎指正,谢谢!
关于方法的参数传递补遗:
最近学习了《C#本质论》这本书,受益匪浅。并且对之前提出的“方法的参数传递”这个问题有了更深了了解,在这里补充说明一下。首先,方法的参数传递都是值传递(注意不是值类型,写上面的文章时并没有很清晰地理解值传递和值类型两个概念),如果方法的参数传递想成为引用传递,就必须加修饰符ref或out。而参数又有数据类型,数据类型分为值类型和引用类型。值类型/引用类型与值传递/引用传递需要分开理解,下面举个例子:
int i=3; int j=i;//这是值类型的赋值,由于值类型存放在栈上,所以变量i和变量j是在不同的栈地址上,且每个地址中存放的值均为3
DataTable dt=new DataTable(); DataTable dtNew=dt;//这是引用类型的赋值,由于引用类型存放在堆(托管堆)上,所以DataTable的一个实例/对象是在堆中,而变量dt是在一个栈地址上存放对堆中对象的地址的引用,变量dtNew也是对堆中对象的地址的引用
这是如果更改变量i的值,是不会影响变量j中的值,而更改变量dtNew(DataTable对象)的值时,更改的是堆中的对象,因此引用同一个对象的变量dt也会显示出新的值。(不知道这么说,是否说明白了)
方法有实参和形参一说,可以将它理解成两个变量,在调用方法时,实际上是将实参的值赋值给形参。当不带ref或out修饰符时,方法的参数传递为值传递,因此可以将实参赋值给形参的过程想象成值类型的赋值过程(注意,这里两个过程并不一样,只是用于理解)。只需要记住一点,方法调用时是将实参的栈地址中的值赋值给形参的栈地址的值。如果方法参数类型为值类型,比如:
void DemoMethod(int paramValue){}
static void Main()
{
int v=3;
DemoMethod(v);
}
这里内存中存在两个变量,一个是v,一个是paramValue,v的栈地址中存放的就是3,然后将3复制一个副本,存放到paramValue的栈地址中,所以当在方法中更改形参的值,方法外实参的值是不会受到影响。如果方法参数类型不幸为引用类型,比如:
void DemoMethod(DataTable paramValue){}
static void Main()
{
DataTable dt=new DataTable();
DemoMethod(dt);
}
这里内存中存在两个变量和一个对象,对象存放在堆中,且其堆地址假设为0x1304c,那么dt的栈地址中存放的就是0x1304c值,然后将这个值复制一个副本,存放到paramValue的栈地址中。这时若在方法中更改对象中的某个属性,比如增加一个DataRow,方法外的实参引用的是同一个对象,对应的DataRow.Count的值也会增加1。这看起来跟引用类型的赋值很像嘛,那为什么叫值传递呢?为了理解这个问题,再举个例子:
DataTable dt=new DataTable(); DataTable dtNew=dt; dt=new DataTable();//这时更改dtNew中的某个属性,比如增加一个DataRow,那么dt中的DataRow.Count会是1吗?当然显然不是。因为堆中已经有两个对象了,而dtNew和dt的栈地址中所引用的对象地址不相同。好,回到之前的问题,方法调用之所以叫值传递,就是因为当在方法中对形参赋值一个新对象时,实参引用的仍然是旧对象,这是在方法中对形参做任何修改,都不会影响到实参,因为实参的栈地址中存放的对象引用地址与形参的栈地址中不相同了。这里不举例子了,不知道这么说是否清楚。
我们在写方法时有一种需求就是,方法中对形参的修改要反应到调用方传入的实参的值,所以这里出现引用传递。当通过引用传递方式传递一个值类型数据,那么当方法中将值由3改为5,那么方法外的实参也被改为5,这个很好理解。如果通过引用传递方式传递一个引用类型数据呢?那么即使方法中将形参重新实例化一个对象,方法外的实参也随之指向方法中实例化的新对象。
最后总结一下,就是值传递传递的是栈地址中的值,而引用传递传递的是栈地址的引用。(补充完毕,2014年5月30日)