参数传递的语义很大程度上受 . NET 世界,ValueTypes 和 Reference Types。在理解ref之类的内容之前,您需要很好地理解它们的含义。
这个答案是从另一个已经关闭的问题中复制而来的(这是我的答案)。问题有些不同,但答案非常相似。
背景
.NET 领域中有两种对象,引用类型和值类型。两者的主要区别在于分配的工作方式。
值类型
当您将值类型实例分配给变量时,该值将被复制到变量中。基本的数字类型(int、float、double 等)都是值类型。结果,在这段代码中:
decimal dec1 = 5.44m;
decimal dec2 = dec1;
dec1 = 3.1415m;
两个十进制变量(dec 和 dec2)都足够宽以容纳十进制数值。在每种情况下,都会复制该值。最后,dec1 == 3.145m 和 dec2 == 5.44m。
几乎所有值类型都被声明为结构(是的,如果您可以访问 .NET 源代码,则 int 是一个结构)。像所有 .NET 类型一样,它们的行为(装箱时)就好像它们是从对象基类派生的一样(它们是通过 System.ValueType 派生的。对象(又名 System.Object)和 System.ValueType 都是引用类型,即使从 System.ValueType 派生的未装箱类型是值类型(这里发生了一点魔法)。
所有值类型都是密封/最终的 - 您不能对它们进行子类化。您也不能为它们创建默认构造函数——它们带有一个默认构造函数,将它们初始化为默认值。您可以创建额外的构造函数(不会隐藏内置的默认构造函数)。
所有enums 也是值类型。它们继承自 System.Enum,但属于值类型,其行为与其他值类型基本相同。
一般来说,值类型应该被设计成不可变的;不是全部。
引用类型
引用类型的变量保存引用,而不是值。也就是说,有时认为它们持有一个值会有所帮助 - 只是该值是对托管堆上对象的引用。
当您分配给引用类型的变量时,您正在分配引用。例如:
public class MyType {
public int TheValue { get; set; }
// more properties, fields, methods...
}
MyType mt1 = new MyType() {TheValue = 5};
MyType mt2 = mt1;
mt1.TheValue = 42;
这里,mt1 和 mt2 变量都包含对同一对象的引用。当该对象在最后一行代码中发生突变时,您最终会得到两个变量,它们都引用了 TheValue 属性为 42 的对象。
所有声明为类的类型都是引用类型。通常,除了数字类型、枚举和布尔值之外,您通常遇到的大多数(但不是全部)类型都是引用类型。
任何被声明为delegate 或event 的东西在幕后也是引用类型。 (在对原始问题的回答中,这已发布到...)有人提到interface。不存在纯粹作为接口类型的对象。结构和类都可以被声明为实现一个接口——它不会改变它们的值/引用类型性质,但是存储在一个类型化为接口的变量中的结构将被装箱。
构造函数行为的不同
引用类型和值类型之间的另一个区别是new 关键字在构造新对象时的含义。考虑这个类和这个结构:
public class CPoint {
public float X { get; set; }
public float Y { get; set; }
public CPoint (float x, float y) {
X = x;
Y = y;
}
}
public struct SPoint {
public float X { get; set; }
public float Y { get; set; }
public CPoint (float x, float y) {
X = x;
Y = y;
}
}
除了 CPoint 是类(引用类型)和 SPoint 是结构(值类型)之外,它们基本相同。
当您使用两个浮点构造函数创建 SPoint 实例时(请记住,它会自动获得一个默认构造函数),如下所示:
var sp = new SPoint (42.0, 3.14);
构造函数运行并创建了一个值。然后将该值复制到sp 变量中(该变量为 SPoint 类型,并且大到足以容纳两个浮点数的 SPoint)。
如果我这样做:
var cp = new CPoint (42.0, 3.14);
发生了一些非常不同的事情。首先,在托管堆上分配的内存足够大以容纳 CPoint(即,足以容纳两个浮点数加上作为引用类型的对象的开销)。然后两个浮点构造函数运行(并且该构造函数是唯一的构造函数 - 没有默认构造函数(额外的、程序员编写的构造函数隐藏了编译器生成的默认构造函数))。构造函数在托管堆上分配的内存中初始化新的 CPoint。最后,创建对该新创建对象的引用并将其复制到变量 cp。
参数传递
抱歉,序言拖了这么久。
除非另有说明,否则函数/方法的所有参数均按值传递。但是,不要忘记引用类型的变量的值是引用。
所以,如果我有一个函数声明为(MyType 是上面声明的类):
public void MyFunction(decimal decValue, MyType myObject) {
// some code goes here
}
还有一些看起来像这样的代码:
decimal dec1 = 5.44m;
MyType mt1 = new MyType() {TheValue = 5};
MyFunction (dec1, mt1);
发生的情况是dec1 的值被复制到函数参数 (decValue) 并且可在 MyFunction 中使用。如果有人在函数内更改了 decValue 的值,则不会出现函数外的副作用。
类似但不同的是,mt1 的值被复制到方法参数 myObject 中。但是,该值是对驻留在托管堆上的 MyType 对象的引用。如果在该方法中,某些代码改变了该对象(例如:myObject.TheValue=666;),那么 mt1 和 myObject 变量所引用的对象都会发生突变,这会导致在函数外部可见的副作用。也就是说,一切仍然是按值传递的。
通过引用传递参数
这是您的问题得到解答的地方
您可以通过两种方式通过引用传递参数,使用out 或ref 关键字。 out 参数不需要在函数调用之前初始化(而 ref 参数必须是)。在函数内,必须在函数返回之前初始化 out 参数 - ref 参数可以初始化,但它们不需要。这个想法是 ref 参数期望传入和传出函数(通过引用)。但是 out 参数的设计只是为了将某些东西从函数中传递出去(通过引用)。
如果我声明如下函数:
public void MyByRefFunction(out decimal decValue, ref MyType myObject) {
decValue = 25.624; //decValue must be intialized - it's an out parameter
myObject = new MyType (){TheValue = myObject.TheValue + 2};
}
然后我这样称呼它
decimal dec1; //note that it's not initalized
MyType mt1 = new MyType() {TheValue = 5};
MyType mt2 = mt1;
MyByRefFunction (out dec1, ref mt1);
调用后,dec1 将包含值 25.624;该值是通过引用从函数中传递出来的。
通过引用传递引用类型变量更有趣。函数调用后,mt1 将不再引用创建的 TheValue 等于 5 的对象,而是引用新创建的 TheValue 等于 5 + 2 的对象(在函数内创建的对象)。现在,mt1 和 mt2 将引用具有不同 TheValue 属性值的不同对象。
对于引用类型,当您正常传递变量时,传递给它的对象可能会发生变异(并且该变异在函数返回后可见)。如果按引用传递引用,则引用本身可能会发生变异,函数返回后引用的值可能不同。