一. 基本型别(primitive types)
对于基本型别,情况看起来要单纯一些.假设我们有如下的一段程序:
//Example 1
using System;
class MainClass{
public static void Main(){
char c = 'a ';
Console.WriteLine("1#: c= " + c);
prmtvFun(c);
Console.WriteLine("2#: c = " + c);
}
static void prmtvFun(char c){
c++;
}
}
用CSC编译它并运行,可以发现控制台的输出为:
1#: c= a
2#: c= a
这表明方法prmtvFun(char c)并没有真正地修改到参数c对应的char变量.这是什么原因呢?在MSDN中,我们看到这样的描述:
If a parameter is declared for a method without ref or out, the parameter can have a value associated with it. That value can be changed in the method, but the changed value will not be retained when control passes back to the calling procedure.
如果要实现修改的话,也是相当简单,只需要使用C#的关键字ref或out.关于ref关键字的介绍,你可以参考MSDN(我使用的是MSDN Library – January 2002):
ms-help://MS.MSDNQTR.2002JAN.1033/csref/html/vclrfRef.htm.
而out关键字,则可以参考:
ms-help://MS.MSDNQTR.2002JAN.1033/csref/html/vclrfout.htm
正如在文档中看到的那样,这两个关键字并没有太大的区别.它们的主要区别是:若变量作为ref方式传入函数,它必须在传入函数之前被初始化;若变量作为out方式传入函数,在函数体中必须对其赋值.
下面为一示例:
//Example 2
using System;
class MainClass{
public static void Main(){
char c = 'm';//必须初始化先
Console.WriteLine("1#: c = " + c);
prmtvFunWithRef(ref c); // 调用时,必须显式使用ref关键字
Console.WriteLine("2#: c = " + c);
char d; //不必初始化,当然也可以初始化
//Console.WriteLine("3#: d = " + d);
prmtvFunWithOut(out d); // 调用时,必须显式使用out关键字
Console.WriteLine("4#: d = " + d);
}
static void prmtvFun(char c){
c = 's';//the changed value will not be retained when control passes back to the calling procedure
}
static void prmtvFunWithRef(ref char c){// 使用ref关键字,c必须被初始化过
c = 's';
}
static void prmtvFunWithOut(out char c){// 使用out关键字
c = 'n';//必须在函数体内,对c进行赋值操作
}
}
这是它的输出:
1#: c = m
2#: c = s
4#: d = n
毫无疑问,这次的修改是成功的.
结论:
对于基本型别(primitive types),在作为函数的参数传递的时候,如果没有使用ref或out关键字,那么在函数体内部,对其进行的任何修改都不会波及其本身. 如果正确使用了ref或out关键字,那么对于参数的操作,其对应的"本尊"也会感同身受.
二. 对象类型(object types)
对于对象类型,如果你认为由于其结构的相对复杂性,作为参数传递时将面临比基本型别更为复杂的情形,那你就错了.事实上,它的传递并不复杂:
//Example 3
using System;
class TestClass{//a class for test
private char c;
public char C{//简单的属性
set{
c = value;
}
get{
return c;
}
}
//构造函数
public TestClass(char c){
this.c = c;
}
}
//============ TestClass 定义到此结束 ===========
class MainClass{//Main Class
public static void Main(){
TestClass tc = new TestClass('a');
Console.WriteLine("1#: " + tc.C);
objFun(tc,'b');
Console.WriteLine("2#: " + tc.C);
objFunWithRef(ref tc,'c');
Console.WriteLine("3#: " + tc.C);
objFunWithOut(out tc,'d');//这里简单地使用tc,这与没有new过的对象效果是一样的
Console.WriteLine("4#: " + tc.C);
}
static void objFun(TestClass tc,char newC){
tc.C = newC;
}
static void objFunWithRef(ref TestClass tc,char newC){
tc.C = newC;
}
static void objFunWithOut(out TestClass tc,char c){
tc = new TestClass(c);
}
}
在上面的程序中,首先定义了一个TestClass类.这个类相当简单,向外只提供了一个构造函数和char型C属性.在MainClass类中,我们定义了一些同基本型别相似的测试函数来判断对象类型的传递方式.整个程序的输出为:
1#: a
2#: b
3#: c
4#: d
从这些输出可以看出,对象类型的对象作为函数的参数传递的时候(无论是否使用关键字ref/out等),在函数体中作的任何修改都会修改到本身对象.是这样的吗?答案是令人沮丧的NO!我们尝试将objFun和objFunWithRef改写为如下形式:
//Example 4<fragment>
static void objFun(TestClass tc,char newC){
tc = new TestClass('a');//plus this statement
tc.C = newC;/*在我们的例子中,其值为'b'*/
}
static void objFunWithRef(ref TestClass tc,char newC){
tc = new TestClass('a');//plus this statement
tc.C = newC; /*在我们的例子中,其值为'c'*/
}
然后再编译,运行程序,这时候程序的输出变为:
1#: a
2#: a
3#: c
4#: d
不意外,你可能对2#和3#的输出感到惊讶.如果前面的问题答案为Yes的话,2#的输出无疑是错误的.那么,对于对象类型对象作函数的参数是如何进行传递的呢?这个问题可以分为两部分来回答:
1> 不使用ref/out关键字
对于这种情况,参数的传递情况和基本型别是一致的:pass by value.为了接受这种观点,你得首先知道引用(reference)的概念.这和Java中的引用是相同的.当我们写下:
TestClass tc = new TestClass('a');
你可能并不知道,你已经在使用引用了.另一方面,你可能认为你创建了一个TestClass类的对象---tc.不过,很遗憾地告诉你,你错了.你的确创建了一个TestClass类的对象,但它不是tc,而是new TestClass('a')对应的一个在堆(Heap)上的对象.tc是与这个对象联系的引用,或说是指向那个对象的指针.像这样:
当参数传递的时候,正如我说的那样:pass by value.那么这里的value为何呢?是引用!当调用objFun(TestClass , char)时,如基本型别那样,产生一个引用的副本(copy).自然地,这个引用副本也是指向实参所指向的同一个对象:
从这里我们可以看到,如果我们在函数体中修改了某个属性(像Example 3中那样),由于copy of tc与tc所指向的对象是同一的,修改自然也就会如实地反映到对象身上.这与我们在Example 3的输出中看到的情况是一致的.
如果像Example 4中那样使用了new关键字,根据我们现在的理解,应该是这样的:
从这个图中,我们可以看到:tc仍然是指向了原来的对象(图中用加单下划线的方式表示),而由于new的作用,tc的副本(copy of tc)指向了在Heap上创建的一个新对象(图中用加双下划线的方式表示).如此一来,在函数体中对tc的副本的操作不会使原来的对象受到影响.对于我们的Example 4来说,调用了objFun后仍然输出了2#: a.
2> 使用了ref/out关键字
对于使用了ref/out关键字修饰的对象类型实例,不会生成一个引用的副本,而是直接使用原来的引用(自然指向原来的对象).如此一来,任何操作都会如实地折射到原来的对象上.如果我们将objFunWithRef(ref TestClass ,char )改写为:
static void objFunWithRef(ref TestClass tc,char newC){
tc = new TestClass('x');
//tc.C = newC;
}
由于ref的作用,原来的对象将成为垃圾,在理论上最终会被GC回收.同时,tc与Heap上创建的另一个对象建立关联.因此,将产生如下的输出:
3#: x
结论:
对于对象型别(object types),在作为函数的参数传递的时候,如果没有使用ref或out关键字,那么传递的是引用,并生成一个引用的副本. 如果正确使用了ref或out关键字,那么将直接使用原来的引用,对于该引用的操作,其对应的"本尊"也会感同身受.
三.结构类型(struct)
众所周知,C#中仍然保留了struct(结构)这一类型.比如,System.Drawing.Rectangle类,System.Drawing.Point类等都是结构类型.那么对于结构类型的对象,参数又是如何传递的呢?我们先看下面的程序:
struct TestStruct{//注意这是结构
private char c;
public char C{
set{
c = value;
}
get{
return c;
}
}
public TestStruct(char c){
this.c = c;
}
}
类似地,我们来测试参数的传递方式:
class MainClass{//Main Class
public static void Main(){
TestStruct ts = new TestStruct('a');
Console.WriteLine("1#: " + ts.C);
structFun(ts,'b');
Console.WriteLine("2#: " + ts.C);
structFunWithRef(ref ts,'c');
Console.WriteLine("3#: " + ts.C);
structFunWithOut(out ts,'d');
Console.WriteLine("4#: " + ts.C);
}
static void structFun(TestStruct ts,char newC){
ts.C = newC;
}
static void structFunWithRef(ref TestStruct ts,char newC){
ts.C = newC;
}
static void structFunWithOut(out TestStruct ts,char c){
ts = new TestStruct(c);
}
}
编译并运行程序,可以看到这样的输出:
1#: a
2#: a
3#: c
4#: d
这表明structFun(TestStruct ,char )的修改是不成功的.从而可以说明struct的参数传递情况与基本型别是一致的(从这里就得出这个结论似乎有点武断,但我们可以通过更多的测试来揭示这点).
四.结论
我们知道,在C#中数据类型被分为两类:值类型(value types)和引用类型(reference types).基本型别和Struct类型属于前者,对象类型则属于后者.根据前面的探讨,我们可以得出如下结论:
1). 值类型
1a). 不使用ref/out关键字
参数传递时,生成值的副本.对副本的操作不会影响到原来的值.
1b). 使用ref/out关键字
直接使用原来的变量,对于所作的修改会在原来的变量上如实反映.
2). 引用类型
2a). 不使用ref/out关键字
参数传递时,生成引用的副本.若副本仍指向原来的对象,则修改会影响到原来的对象,否则不会.
2b). 使用ref/out关键字
使用原来的引用,所有修改将如实反映到原来的对象上.
C#中的参数传递
最新推荐文章于 2024-04-23 17:20:49 发布