回复:OP 的断言
众所周知(至少在 C# 中),当您通过引用传递时,该方法包含对被操作对象的引用,而当您通过值传递时,该方法复制被操作的值...
TL;DR
不止于此。除非您使用 ref or out 关键字传递变量,否则 C# 将通过 value 将变量传递给方法,而不管变量是 值类型 还是 引用类型。
由于这一切都相当复杂,我建议尽可能避免通过引用传递(相反,如果您需要从函数返回多个值,请使用复合类、结构或元组作为return 类型,而不是在参数上使用 ref 或 out 关键字)
此外,当传递引用类型时,可以通过不更改(变异)传递给方法的对象的字段和属性来避免很多错误(例如,使用 C# 的 immutable properties以防止更改属性,并力求在构造期间仅分配一次属性)。
详细说明
问题在于有两个截然不同的概念:
- 值类型(例如 int)与引用类型(例如字符串或自定义类)
- 按值传递(默认行为)与按引用传递(ref,out)
除非您通过引用显式传递(任何)变量,否则通过使用 out 或 ref 关键字,参数在 C# 中由 value 传递,无论变量是否为值类型或引用类型。
当通过值(即没有out或ref)传递值类型(例如int、float或类似DateTime的结构)时,被调用的函数会得到一个copy of the entire value type(通过堆栈)。
对值类型的任何更改,以及对副本的任何属性/字段的任何更改都将在被调用函数退出时丢失。
但是,当通过value 传递reference 类型(例如自定义类,如您的MyPoint 类)时,将reference 复制并传递给相同的共享对象实例在堆栈上。
这意味着:
- 如果传递的对象具有可变(可设置)字段和属性,则对共享对象的这些字段或属性的任何更改都是永久性的(即任何观察对象的人都可以看到对
x 或y 的任何更改)
- 但是,在方法调用期间,引用本身仍然被复制(通过值传递),因此如果重新分配参数变量,则此更改仅对引用的本地副本进行,因此更改不会被呼叫者,召集者。 这就是您的代码无法按预期工作的原因
这里发生了什么:
void Replace<T>(T a, T b) // Both a and b are passed by value
{
a = b; // reassignment is localized to method `Replace`
}
对于引用类型T,意味着对对象a 的局部变量(堆栈)引用被重新分配给局部堆栈引用b。此重新分配仅对此函数本地 - 一旦范围离开此函数,重新分配就会丢失。
如果你真的想替换调用者的引用,你需要像这样更改签名:
void Replace<T>(ref T a, T b) // a is passed by reference
{
a = b; // a is reassigned, and is also visible to the calling function
}
这会将调用更改为引用调用 - 实际上我们将调用者变量的地址传递给函数,然后允许被调用的方法改变调用方法的变量。
然而,如今:
- 通过引用传递是generally regarded as a bad idea - 相反,我们应该在返回值中传递返回数据,如果要返回多个变量,则使用
Tuple 或自定义class 或@ 987654355@,其中包含所有此类返回变量。
- 在被调用的方法中更改(“变异”)共享值(甚至引用)变量是不受欢迎的,尤其是函数式编程社区,因为这会导致棘手的错误,尤其是在使用多个线程时。相反,优先考虑不可变变量,或者如果需要突变,则考虑更改变量的(可能很深)副本。您可能会发现有关“纯函数”和“常量正确性”的主题很有趣。
编辑
这两张图可能有助于解释。
按值传递(引用类型):
在您的第一个实例 (Replace<T>(T a,T b)) 中,a 和 b 按值传递。对于reference types,this means the references 被复制到堆栈中并传递给被调用的函数。
- 您的初始代码(我称之为
main)在托管堆上分配两个MyPoint 对象(我称之为point1 和point2),然后分配两个局部变量引用@987654363 @ 和b,分别引用这些点(浅蓝色箭头):
MyPoint a = new MyPoint { x = 1, y = 2 }; // point1
MyPoint b = new MyPoint { x = 3, y = 4 }; // point2
-
对Replace<Point>(a, b) 的调用然后将两个引用的副本推入堆栈(红色箭头)。方法Replace 将这些视为两个参数,也称为a 和b,它们仍然分别指向point1 和point2(橙色箭头)。
-
赋值,a = b; 然后更改Replace 方法的a 局部变量,使得a 现在指向与b 引用的相同对象(即point2)。不过要注意,这个改动只针对Replace的本地(栈)变量,这个改动只会影响Replace(深蓝线)中的后续代码。它不会以任何方式影响调用函数的变量引用,也不会改变堆上的point1 和point2 对象。
通过引用传递:
如果我们将调用更改为Replace<T>(ref T a, T b),然后将main 更改为通过引用传递a,即Replace(ref a, b):
-
和以前一样,在堆上分配两个点对象。
-
现在,当Replace(ref a, b) 被调用时,虽然mains 引用b(指向point2)在调用过程中仍然被复制,a 现在通过引用传递 ,这意味着 main 的 a 变量的“地址”被传递给 Replace。
-
现在当分配a = b 时...
-
这是调用函数,main 的a 变量引用现在更新为引用point2。现在main 和Replace 都可以看到重新分配给a 所做的更改。现在没有对point1的引用
所有引用该对象的代码都可以看到(堆分配的)对象实例的更改
在上述两种情况下,堆对象point1 和point2 实际上没有发生任何更改,只是传递并重新分配了局部变量引用。
但是,如果实际上对堆对象 point1 和 point2 进行了任何更改,那么对这些对象的所有变量引用都会看到这些更改。
所以,例如:
void main()
{
MyPoint a = new MyPoint { x = 1, y = 2 }; // point1
MyPoint b = new MyPoint { x = 3, y = 4 }; // point2
// Passed by value, but the properties x and y are being changed
DoSomething(a, b);
// a and b have been changed!
Assert.AreEqual(53, a.x);
Assert.AreEqual(21, b.y);
}
public void DoSomething(MyPoint a, MyPoint b)
{
a.x = 53;
b.y = 21;
}
现在,当执行返回到main 时,所有对point1 和point2 的引用,包括main's 变量a 和b,现在将在下次读取时“看到”更改点的x 和y 的值。您还会注意到变量a 和b 仍然按值传递给DoSomething。
值类型的更改仅影响本地副本
值类型(原语如System.Int32、System.Double)和结构(如System.DateTime,或您自己的结构)分配在堆栈上,而不是堆上,并在传递到称呼。这导致了行为上的重大差异,因为被调用函数对值类型字段或属性所做的更改只会由被调用函数在本地观察,因为它只会改变值类型。
例如考虑以下带有可变结构实例的代码,System.Drawing.Rectangle
public void SomeFunc(System.Drawing.Rectangle aRectangle)
{
// Only the local SomeFunc copy of aRectangle is changed:
aRectangle.X = 99;
// Passes - the changes last for the scope of the copied variable
Assert.AreEqual(99, aRectangle.X);
} // The copy aRectangle will be lost when the stack is popped.
// Which when called:
var myRectangle = new System.Drawing.Rectangle(10, 10, 20, 20);
// A copy of `myRectangle` is passed on the stack
SomeFunc(myRectangle);
// Test passes - the caller's struct has NOT been modified
Assert.AreEqual(10, myRectangle.X);
以上内容可能会让人很困惑,并强调了为什么将自己的自定义结构创建为不可变结构是一种很好的做法。
ref 关键字的作用类似,允许通过引用传递值类型变量,即调用者的值类型变量的“地址”被传递到堆栈上,现在可以直接对调用者的赋值变量进行赋值.