(译)C#参数传递
前言
菜鸟去重复之Sql的问题还没有得到满意的答案。如果哪位大哥有相关的资料解释,能够分享给我,那就太谢谢了。
接触C#一年了,感觉很多东西还是很模糊,像C#中的委托和事件
有些东西看多了不用也还是不会。还有些东西用多了不想也还是不精。
这次发现一篇解除我对于C#里面参数传递困惑的详细条例文章,忍不住翻译留存以备回顾。
英文好的可以直接点此处看原文了。MSDN相关解释链接在此处。
前奏:引用类型
在C#中有两种常用的类型:引用类型和值类型。他们表现不同,很多人在使用他们的时候都感到了困惑,这里简单解释下他们的区别:
引用类型指引用类型的变量存储对实际数据的引用。如下代码:
StringBuilder sb = new StringBuilder();
这里我们定义了一个变量sb,创建一个新的StringBuilder对象,并把这个对象的引用赋给sb。sb实际存储的值不是该对象,只是它的引用。
引用类型之间的赋值只是简单地将其表达式或者变量赋给对应的变量。再看如下代码:
StringBuilder first = new StringBuilder(); first.Append("hello"); StringBuilder second = first; Console.WriteLine(second); // Prints hello
这里我们定义了一个变量first,创建了一个新的StringBuilder对象,并把这个对象的引用赋给first。然后我们将first赋给second。
这意味着他们都指向了同一个对象。如果我们通过使用first.Append的方式修改该对象的内容,second也同样变化了。如下:
StringBuilder first = new StringBuilder(); first.Append("hello"); StringBuilder second = first; first.Append(" world"); Console.WriteLine(second); // Prints hello world
虽然会这样,但他们仍然应该被看作相互独立的变量。修改first指向一个完全不同的对象(或者直接将null赋给它)完全不会影响second。
StringBuilder first = new StringBuilder(); first.Append("hello"); StringBuilder second = first; first.Append(" world"); first = new StringBuilder("goodbye"); Console.WriteLine(first); // Prints goodbye Console.WriteLine(second); // Still prints hello world
类(class),接口(interface),委托(delegate)和数组(array)都是引用类型。
前奏:值类型
引用类型在变量与真实数据之间有个间接层,值类型却不是。值类型变量直接存储其数据。
值类型之间赋值是将其值拷贝一份然后赋给对应的变量。下面用一个结构类型来解释下:
public struct IntHolder { public int i; }
IntHolder是个结构体的值类型,它包含一个单独的整型i。对其赋值将会拷贝,如下所示:
IntHolder first = new IntHolder(); first.i = 5; IntHolder second = first; first.i = 6; Console.WriteLine (second.i); //输出5
这里second.i值为5是因为当second=first时second.i保存了first.i的拷贝。自此,second是与first相互独立的。
所以即使后来first.i=6,second.i依然不变。
简单的类型(例如float,int,char),enum类型和struct类型都是值类型。
Note:很多类型(例如string)是引用类型,但表现的却像是值类型。这些是不可变类型,就是这些类型的实例一旦创建就不能再改变。
这使得引用类型在一些方面表现得值类型一样——尤其是当你使用一个指向是不可变的对象的引用时,
你就不用担心把它传递到一个方法里面或者从某个方法返回后值不对。
无论如何,你总是知道这个变量引用的对象的内容。这也是为什么string.Replace不改变该string的值,而是返回一个新的string实例。
如果string改变了,通过其它所有指向它的引用获取到的string也改变了,那明显不是我们期望的。
用一个可变的引用类型对比(例如ArrayList)加深大家理解。
如果一个方法的返回值为保存在变量中的一个ArrayList类型的引用,在方法内部并没有创建新的实例直接使用,
而是对一个已有的实例进行增加等一些操作,其它人员却不知道,这就可能出现问题。
之前说过了不可变的引用类型表现得像值类型,但实际不是值类型,大家千万不要被其表面迷惑,理解其实质,才能更好地运用。
测试你的理解
如果之前的IntHolder不是结构类型而是类,那输出的结果是多少呢?如果你不理解为什么是6,那估计是我翻译的有问题,
或者我翻译得不够清楚,实在罪过。如果您有好的建议,请告知我,不胜感激。
这里再次奉上原文链接,以备有人实在看不下去我过烂的翻译。
间奏:不同类型的参数
在C#中有四种不同类型的参数:值类型参数(默认),引用类型(ref),out类型,参数数组(params)。
你可以同时使用值类型和引用类型参数。
当你使用这些参数的时候,你应该在脑海里对“值类型”和“引用类型”有非常清晰的认识。
这样不管是在使用它们还是与它们相关的类型的时候,你都会感觉非常轻松。
值类型参数
C#中默认参数是值类型参数,这意味着在方法成员声明时会为其变量会创建一份拷贝,它就是你在方法内部调用指定的变量的初始值。
如果你改变这个值,不会对调用中传的值源造成任何影响。看下代码:
void Foo (StringBuilder x) { x = null; } ... StringBuilder y = new StringBuilder(); y.Append ("hello"); Foo (y); Console.WriteLine (y==null);// 输出False
y的值没有改变只是因为x被赋null。无论如何,请一定记住引用类型变量存储的是一个引用——如果两个引用指向同一个对象,
那么改变对象的内容,通过这两个引用获取到的对象也会发生改变。例如:
void Foo (StringBuilder x) { x.Append (" world"); } ... StringBuilder y = new StringBuilder(); y.Append ("hello"); Foo (y); Console.WriteLine (y); //输出 hello world
在调用Foo(y)之后,y指向的值变为“hello world”,在Foo方法内部对引用变量x调用Append拼接“ world”字符实现了效果。
这里我们再来看下值类型参数传递如何。正如之前提到的,值类型的值就是它本身。
使用之前的结构类型IntHolder,我们写一些与之前类似的代码来测试下:
void Foo (IntHolder x) { x.i=10; } ... IntHolder y = new IntHolder(); y.i=5; Foo (y); Console.WriteLine (y.i); //输出5
当Foo被调用时,x是个struct类型,并且它的i为5。之后将10赋给了i。
Foo一点都不知道y。当这个方法执行结束,y还是和它之前一模一样。
我们之前展示了一些关于引用类型作为值类型参数传递的例子。
那么你应该明白当IntHolder声明为类时会发生什么。你应该清楚为什么y.i会因此变成10。
引用类型参数
引用类型参数使用的时候不传递其实际值,只是使用变量本身。也就是说不会创建一个新的拷贝,而是使用相同的存储地址。
因此在方法成员中的值类型与引用类型一直都是一样的。
使用引用类型参数在声明和调用的时候需要使用ref关键字——这意味着你要使用引用类型参数将会看起来清晰明确。
我们再来改动下之前的例子测试下:
void Foo (ref StringBuilder x) { x = null; } ... StringBuilder y = new StringBuilder(); y.Append ("hello"); Foo (ref y); Console.WriteLine (y==null); //输出True
这里,因为将y的引用传递给x而不是它的值,所以对x进行的操作相当于对y进行操作一样。在上例中y最后为null。
请将这个结果与上面的没有使用ref关键字的例子结果进行比较。
现在,让我们测试下之前使用结构作为参数的例子加上ref关键字后会发生什么:
void Foo (ref IntHolder x) { x.i=10; } ... IntHolder y = new IntHolder(); y.i=5; Foo (ref y); Console.WriteLine (y.i); //输出10
这两个变量共用一个内存地址,因此改变x时y也会发生改变。所以y.i是10。
注意:通过默认的值类型参数方式传递引用类型变量与通过引用类型参数方式传递值类型变量
有什么区别呢?
你可能已经注意到了在最后一个例子中,将一个结构类型通过引用方式传递,与通过值类型方式传递一个类有同样的结果。
但这不意味他们是一样的。好吧,还是让我们通过下面的代码加深下理解:
void Foo (??? IntHolder x) { x = new IntHolder(); } ... IntHolder y = new IntHolder(); y.i=5; Foo (??? y);假设IntHolder是个结构类型(即值类型),方法参数为引用类型(即将???替换为ref),
执行代码,y最后将会指向一个new IntHolder,y.i会因此变为0。
假设IntHolder是个类(即引用类型),方法参数为值类型(即将???去掉),
执行代码,y的指向不会改变,y.i仍为5。
在调用函数前y与x确实指向同一个对象。理解C#参数传递中的这个区别绝对是相当关键的。
这也是为什么原作者认为当人们谈及对象默认引用传递的时候会非常困惑,其实正确的说法应该是引用类型默认值传递。
Out类型参数
Out类型参数和引用类型参数很像,它不会创建新的内存地址,而是共享内存地址。
使用Out类型参数的方法在声明和调用的时候需要加上关键字Out。这样也会使代码看起来清晰。
虽然Out类型参数与引用类型参数非常相似,但它们还是有区别的。Out类型与引用类型的不同之处:
1.方法调用时传递的变量不用事先赋值。如果方法调用正常结束,即可认为该变量后来被赋值了(这样,你就可以直接读取它的值了)。
2.该参数传递时被看作没有初始化(也就是说你在读取它之前,必须先给它赋值)。
3.在方法结束前必须对这个参数进行赋值,否则编译器会报错。
下面展示一个例子进行加深大家理解,使用的是一个int型作为参数
int是值类型,如果你已经理解了引用类型,相信你也会知道引用类型作为参数会发生什么:
void Foo (out int x) { //这里不能读取x,它被认为是没有初始化的,读取会报错 //赋值-在方法结束前必须对其进行赋值,否则报错 x = 10; // 这里x可以读取了: int a = x; } ... // 声明一个没有初始化的变量 int y; // 虽然y没有初始化,但可以作为out类型参数传递 Foo (out y); // 现在y已经有值了,输出: Console.WriteLine (y); //输出10
参数数组(params)
参数数组允许传递给一个方法一组数据。定义包含参数数组的方法时必须加上关键字params,调用该方法的时候却不必加上这个关键字。
参数数组必须放在方法参数的最后,并且只能是一维数组。
当使用这类方法时,调用中只要参数与定义时参数数组类型兼容,就能够传递。
由于参数数组的这种使用方式,所以当你想传递一个单独的数组,它的效果就像是值类型参数传递。例如:
void ShowNumbers (params int[] numbers) { foreach (int x in numbers) { Console.Write (x+" "); } Console.WriteLine(); } ... int[] x = {1, 2, 3}; ShowNumbers (x); ShowNumbers (4, 5); //输出: 1 2 3 4 5
第一次调用时,x是一个整型数组,效果等同于将它(引用)作为值类型参数传递。
第二次调用时,将会创建一个包含4,5的整型数组,并将它的引用传递(仍然是值类型参数)。
尾奏:总结
翻译出来总是容易使文章读起来拗口难理解。第一次翻译技术文章,我肯定也避免不了。只求能够对大家及我有所帮助。
程序员,英语很重要。
如果您英文比较好的话,我还是建议您读一下原文。
如果本文中有疏漏或者理解错误的地方,还请指出,不胜感激。