按值传递
c#的函数默认只能有0或者1个返回值,而输入参数可以有任意个。默认情况,参数都是按值传递的,即在方法内部改变,而方法外部看不到任何改变。不过,这要看参数本身是值类型还是引用类型。如果是值类型的话,那么相信大家都知道,方法内部怎么改,外面都看不见,因为传进去的是一个值的副本。而如果参数本身是引用类型,那么传进去的就是一个引用。(亦即按值传递传递的是栈上的对象)此时如果在方法内改变引用类型的值,相当于把引用指向的堆上的对象改了,自然而然的,所有指向该堆上的对象的引用将全部受到影响。
class Program { static void Main(string[] args) { rectangle a = new rectangle("info", 10, 20); rectangle b = a; rectangle c = a; change(a, 999); Console.WriteLine(string.Format("A: chang is {0} and kuan is {1}, info: {2}", a._chang, a._kuan, a._recinfo.infostring)); Console.WriteLine(string.Format("B: chang is {0} and kuan is {1}, info: {2}", b._chang, b._kuan, b._recinfo.infostring)); Console.WriteLine(string.Format("C: chang is {0} and kuan is {1}, info: {2}", c._chang, c._kuan, c._recinfo.infostring)); Console.ReadKey(); } public static void change(rectangle r, double chang) { r._chang = chang; //没有意义,引用类型按值传递,不能改变引用指针的指向 r = new rectangle("newinfo", 1, 1); } } class rectangle { public shapeinfo _recinfo; public double _chang; public double _kuan; public rectangle(string recinfo, double chang, double kuan) { _recinfo = new shapeinfo(recinfo); _chang = chang; _kuan = kuan; } }
该例子中我们通过方法change修改了对象a的字段chang的值,但a,b,c三个对象全部受到了影响,这是因为这三个对象在栈上的引用的指针全部指向一个地方。下面的一个草图可能可以令大家加深一点印象。当我修改的时候,我修改的是堆上的字段chang的值,显然指向他的所有对象都有牵连。(这是对于具有可变性的引用类型来说的,对于具有不变性,或者不具有可变性的引用类型 - 字符串来说,我们无法修改它的值,也就是说我们做不到操作他堆上空间的值这件事,我们通过另一种方法进行修改,大家可以先思考一下这是什么方法 - 对了,如果我不能修改你,我可以另辟一块空间出来嘛,然后我把新的值放进去,然后把你的指针指向改成指到新的空间,不就可以了么,这正是修改字符串的时候发生的全部事情!)
所以字符串是特例,字符串虽然是引用类型,但它具有不变性,也就是说一经赋值就不能改变(堆上的空间)了。每次当改变某个字符串的值的时候,实际上是在堆上新开辟了一块内存,然后把新的值储存在那里,然后改变栈上引用的指针的指向,令其指向了新的内存。例如(例子来源:http://www.cnblogs.com/zhili/archive/2013/06/11/ParameterPass.html)。所以说多次操作字符串将会对系统性能产生不好的影响:
- 开辟大量新的空间,占用过多堆上的空间
- 会开辟新的空间,然后指针指向也改变了。那么旧的那块空间呢,回收了么,显然没有,因为旧的那块空间也是字符串类型的,是一个引用类型,而引用类型是要等待垃圾收集器回收的,所以一时半会回收不了,这也影响性能
一个解决方法是使用StringBuilder,因为其具有可变性,于是上面两个问题都没有了。
class Program { static void Main(string[] args) { string str = "old string"; ChangeStr(str); Console.WriteLine(str); } private static void ChangeStr(string oldStr) { oldStr = "New string"; Console.WriteLine(oldStr); } }
这就是引用类型的按值传递,传递的是栈上的引用,而指针的指向不能被改变。那么如何可以改变指针的指向呢?那就要借助按引用传递了。
按引用传递(ref关键字)
默认方法是按值传递的,如果要按引用传递,必须借助ref关键字。先说说值类型。如果是以ref传入值类型的话,可以预见,外部的值类型也会发生变化(此时值类型就拥有了一个类似引用类型的行为)。显而易见,如果传进去的引用指向null,那么在方法内部操作相当于对null进行操作,编译器将给出NullException。所以传入的引用必须已经初始化了一个值。例如下面的例子:
static void Main(string[] args) { string a = "123"; string b = "abc"; //这里也要显式的加上ref swap(ref a, ref b); Console.WriteLine(string.Format("a= {0}, b={1}", a, b)); Console.ReadKey(); } //值类型的按引用传递,不会为值类型装箱 public static void swap(ref string a, ref string b) { string temp = a; a = b; b = temp; }
这个简单的例子演示了按引用传递两个值类型,当方法运行完之后,外部的两个字符串也发生了变化。要注意的是,当值类型按引用传递时,不会对值类型装箱。传进去的仅仅是地址而已。对引用类型按引用传递时,传进去的也是地址(或者是指针),此时我们可以操作这个地址,也就是改变指针的指向。我们也可以和按值传递时候的行为一样,改变引用类型字段的值。
static void Main(string[] args) { rectangle a = new rectangle("info", 10, 20); rectangle b = a; rectangle c = a; change(ref a); Console.WriteLine(string.Format("A: chang is {0} and kuan is {1}, info: {2}", a._chang, a._kuan, a._recinfo.infostring)); Console.WriteLine(string.Format("B: chang is {0} and kuan is {1}, info: {2}", b._chang, b._kuan, b._recinfo.infostring)); Console.WriteLine(string.Format("C: chang is {0} and kuan is {1}, info: {2}", c._chang, c._kuan, c._recinfo.infostring)); Console.ReadKey(); } public static void change(ref rectangle r) { //引用类型按引用传递,更改指针的指向使其指向一个新的对象 r = new rectangle("newinfo", 1, 1); }
注意如果不是按引用传递的话,方法内部的
r = new rectangle("newinfo", 1, 1);
没有任何意义。