C#中值传递和引用传递
一. 传递的概念
传递是指在调用方法时,用实际参数为方法的形式参数赋值。方法的形式参数,按照参数类型划分有两种,分为是值类型和引用类型。
二. 数据类型
数据类型是计算机科学核心概念之一。在C#中,数据类型分两大类,值类型和引用类型。
写程序,首先面对的一个概念是变量。什么是变量?
变量的定义:变量是指在程序运行过程中,其值可以改变的量。
变量三要素,分别是变量的名称(name)、变量的类型(type)、变量的值(value)。其中,变量的名称,也可以理解为变量的地址,变量在内存单元意义上的地址,变量名其实就是内存单元的名称。
变量名称决定了存储变量的内存地址;变量类型(数据类型)决定了存储变量的内存空间大小,同时也决定了变量的值的上限和下限。
为什么要提变量三要素呢?因为值类型和引用类型的区别,就要从变量的三要素说起。
值类型和引用类型的区别有两点:1.数据的存储方式;2.数据的传递方式(复制方式)。
可以简单的认为,对于一个值类型变量,只存储变量三要素中的值。所以,值类型变量在进行数据传递时,只传递三要素中的值;对于一个引用类型的变量,它存储的是变量三要素中变量的地址,同时,在进行数据传递时,传递的也是变量的地址。
怎么理解这段话呢?我们来个程序示例,在程序中感受下这种区别。
(1)新建一个Window窗体应用程序项目,项目名称“值传递和引用传递”。为项目添加一个类文件,保存为General.cs。
在类文件中输入如下代码:
1 using System; 2 using System.Collections.Generic; 3 using System.Text; 4 5 namespace 值传递和引用传递 6 { 7 class General 8 { 9 public string Name { get; set; } //姓名 10 public int Age { get; set; } //年龄 11 } 12 }
(2)在项目默认创建的窗体Form1中添加一个按钮button1,将其Text属性改为“确定”,在窗体的代码文件中分别添加两个方法ChangeGeneralInfoFirst和ChangeGeneralInfoSecond,并创建button1的事件处理过程,然后输入以下代码:
1 using System; 2 using System.Collections.Generic; 3 using System.ComponentModel; 4 using System.Data; 5 using System.Drawing; 6 using System.Text; 7 using System.Windows.Forms; 8 9 namespace 值传递和引用传递 10 { 11 public partial class Form1 : Form 12 { 13 public Form1() 14 { 15 InitializeComponent(); 16 } 17 //自定义方法 18 void ChangeGeneralInfoFirst(string name, int age) 19 { 20 name = "徐达"; 21 age = 41; 22 } 23 //自定义方法 24 void ChangeGeneralInfoSecond(General general) 25 { 26 general.Name = "霍去病"; 27 general.Age = 19; 28 } 29 //按钮button1的事件处理过程 30 private void button1_Click(object sender, EventArgs e) 31 { 32 General newGeneral = new General(); 33 newGeneral.Name = "卫青"; 34 newGeneral.Age = 38; 35 36 string testStr = ""; 37 testStr = string.Format("调用前,结果为:{0},{1}\r\n",newGeneral.Name,newGeneral.Age); 38 39 ChangeGeneralInfoFirst(newGeneral.Name, newGeneral.Age);//传递值 40 testStr += string.Format("值传递调用后,结果为:{0},{1}\r\n",newGeneral.Name,newGeneral.Age); 41 42 ChangeGeneralInfoSecond(newGeneral);//传递对象(引用传递) 43 testStr += string.Format("引用传递调用后,结果为:{0},{1}\r\n",newGeneral.Name,newGeneral.Age); 44 45 MessageBox.Show(testStr,"消息",MessageBoxButtons.OK,MessageBoxIcon.Asterisk); 46 } 47 } 48 }
代码说明:
(1)ChangeGeneralInfoFirst(string name,int age)方法中包含两个值类型形式参数,在调用该方法时,如果将General类的对象newGeneral的属性值作为实际参数传递给该方法对应位置的形式参数,为形式参数赋值。调用完该方法后,方法体中代码对形式参数的值进行了改变,并不改变General类的对象newGeneral原来的属性值。
(2)ChangeGeneralInfoSecond(General general)方法中的参数是引用类型,在调用该方法时,如果将General类的对象newGeneral作为实际参数传递(赋值)给该方法的形式参数general,此时newGeneral和general都指向同一个对象。在方法体内如果通过形式参数修改了对象的属性值,那么在方法执行完毕后实际参数general的属性值也会发生改变。
(3)在button1的事件处理过程中,首先创建了General对象,并对Name和Age属性进行了赋值,然后将调用ChangeGeneralInfoFirst和ChangeGeneralSecond方法前后的属性值进行比较。从而得出结论:
如果是值传递,不影响实际参数的值;如果是引用传递,在方法中如果改变了对象的属性,那么实际参数指向的对象的属性也将改变。
“ 值类型用作方法参数时,用实际参数给方法的形式参数赋值时,只是将实际参数的值赋值给形式参数,方法中形式参数的值修改了,实际参数的值,并不改变;
引用类型用作方法参数时,用实际参数给方法的形式参数赋值时,是将实际参数的地址赋值给形式参数,这样一来,方法中形式参数的值修改了,同时也把实际参数的值一并修改。”
我们来看一下程序运行结果,如下图所示:
特别注意:
引用类型作为方法的形式参数时,
1.如果方法体内的代码是【修改变量本身】时,调用方法时,结果类似值传递,即不会改变传递前的变量的值;
2.如果方法体内的代码是【修改变量的属性或字段】时,调用方法时,才是引用传递,会影响传递前的变量的值。
3.方法的形式参数使用了ref后,才是真正的引用传递。无论方法体内的代码是【修改变量本身】还是【修改变量的属性或字段】,都会影响传递前的变量的值。
关于第2点,上述程序示例中的ChangeGeneralInfoSecond(General general)方法,方法体就是在【修改变量的属性或字段】。
关于第1点,关于方法体【修改变量本身】,我们看下面这个程序示例;
1 using System; 2 using System.Collections.Generic; 3 using System.ComponentModel; 4 using System.Data; 5 using System.Drawing; 6 using System.Text; 7 using System.Windows.Forms; 8 9 namespace 值传递和引用传递 10 { 11 public partial class Form1 : Form 12 { 13 public Form1() 14 { 15 InitializeComponent(); 16 } 17 /* 18 * 引用类型用作形式参数 19 * 方法中修改是变量本身,即对象本身时 20 * 方法的执行结果,类似值传递,即不会改变方法执行前变量的值 21 * */ 22 void SwapObject(General first,General second) 23 { 24 General temp = first; 25 first = second; 26 second = temp; 27 } 28 29 private void button2_Click(object sender, EventArgs e) 30 { 31 General one = new General(); 32 General two = new General(); 33 34 one.Name = "张无忌"; 35 one.Age = 25; 36 37 two.Name = "常遇春"; 38 two.Age = 30; 39 40 string str = ""; 41 str = string.Format("SwapObject交换前,结果为:\r\none.Name ={0},one.Age ={1}\r\ntwo.Name ={2},two.Age ={3}\r\n", one.Name, one.Age, two.Name, two.Age); 42 SwapObject(one,two); 43 str += string.Format("SwapObject交换后,结果为:\r\none.Name ={0},one.Age ={1}\r\ntwo.Name ={2},two.Age ={3}\r\n", one.Name, one.Age, two.Name, two.Age); 44 45 MessageBox.Show(str, "消息",MessageBoxButtons.OK,MessageBoxIcon.Asterisk); 46 } 47 } 48 }
程序执行结果为:
为了验证第3点,我们将上述代码的第22行和第42行修改为:
1 void SwapObject(ref General first,ref General second) 2 3 SwapObject(ref one,ref two);
三.数据类型的划分
上面例子提到,方法ChangGeneralInfoFirst(string name,int age)的形式参数是值类型,方法ChangeGeneralInfoSecond(General general)的形式参数是引用类型。那么问,我们如何区分一个给定的形式参数,是属于值类型呢还是引用类型呢?
关于值类型和引用类型的划分,具体见下图:
由上表可以得知,string类型是引用类型,但是,上面方法ChangGeneralInfoFirst(string name,int age)的形式参数是值类型。是不是有些矛盾?string类型到底是引用类型还是值类型?
看下面这段代码:
1 //值类型 2 int a = 1; 3 int b = a; 4 a = 2; 5 string str = ""; 6 str= string.Format("a={0}\r\nb={1}\r\n",a,b); 7 //引用类型 8 string str1 = "ab"; 9 string str2 = str1; 10 str1 = "abc"; 11 str += string.Format("str1={0}\r\nstr2={1}\r\n",str1,str2); 12 13 MessageBox.Show(str,"消息",MessageBoxButtons.OK,MessageBoxIcon.Asterisk);
执行结果:
从运行结果可以看出,str2并没有随着st1的改变而改变。如果string是引用类型,str1和st2应该是指向同一个内存单元,如果st1内容发生变化,st2也应该跟着变化。由此例看,string更像值类型。但是MSDN上明确指出,string类型是引用类型。这是为何呢?究其原因,是因为string类型的对象是不可变的,包括长度和其中的任何字符都是不可以改变的。
string类型的不变性
string对象称为不可变的(只读),因为一旦创建了该对象,就不能修改该对象的值。有时候看起来似乎改了,实际上string经过了特殊处理,每次改变值时都会建立一个新的string对象,变量会指向这个比较新的对象,而原来的还是指向原来的对象,所以不会改变。这也是string效率低下的原因。如果经常改变string的值则应该使用StringBuilder而不使用string。
在上述例子中,str1 = "ab",这时在内存中将"ab"存下来,如果在创建字符串对象st2,其值也等于"ab",即str2 = str1,则并非再重新分配内存空间,而是将之前保存"ab"的地址赋给str2。而当str1 = "abc",str1的值发生改变时,这时检查内存,发现不存在"abc"的字符串,则重新分配内存空间,存储"abc",并将其地址付给str1,而str2依旧指向保存字符串"ab"的内存地址。上述例子的结果,可以印证这段解释。
结论
string类型是引用类型,只是编译器对其做了特殊处理。
本文参考文献:
1.《C#程序设计基础与应用》,严健武 严耿超 李彬 主编,2019年7月第1版;