C#按值传递而不是按引用传递

考虑以下代码(我有意将 MyPoint编写为此示例的引用类型)

public class MyPoint { public int x; public int y; } 

它是普遍公认的(至少在C#中)当你通过引用传递时,该方法包含对被操作对象的引用,而当你通过值传递时,该方法复制被操纵的值,因此全局范围中的值是不受影响。

例:

 void Replace(T a, T b) { a = b; } int a = 1; int b = 2; Replace(a, b); // a and b remain unaffected in global scope since a and b are value types. 

这是我的问题; MyPoint是一个引用类型,因此我希望Point上的相同操作在全局范围内替换ab

例:

 MyPoint a = new MyPoint { x = 1, y = 2 }; MyPoint b = new MyPoint { x = 3, y = 4 }; Replace(a, b); // a and b remain unaffected in global scope since a and b...ummm!? 

我希望ab在内存中指向相同的引用…有人可以澄清我哪里出错吗?

回覆:

它是普遍公认的(至少在C#中),当你通过引用传递时,该方法包含对被操纵对象的引用,而当你传递值时,该方法复制被操纵的值…

TL; DR

除非使用refout关键字传递变量,否则C#会按将变量传递给方法。

详细地

问题是有两个不同的概念:

  • 值类型(例如int)vs引用类型(例如字符串)
  • 传递值(默认行为)vs传递参考(ref,out)

除非您通过引用显式传递(任何)变量,否则通过使用outref关键字,参数将在C#中按传递,而不管变量是值类型还是引用类型。

传递类型(例如intfloat或类似DateTime结构)时,被调用函数获取整个值类型的副本 (通过堆栈)。

退出被调用函数时,对值类型的任何更改以及对副本的任何属性/字段的任何更改都将丢失。

但是,对于引用类型(如string和自定义类,如MyPoint类),它是对在堆栈上复制和传递的相同共享对象实例的reference

这意味着:

  • 对共享对象的字段或属性的任何更改都是永久性的(即对xy任何更改)
  • 但是,引用本身仍然被复制(通过值传递),因此对引用副本的任何更改都将丢失。 这就是您的代码无法按预期工作的原因

这里发生了什么:

 void Replace(T a, T b) { a = b; } 

对于引用类型T ,表示将对象a的局部变量(堆栈)引用重新分配给本地堆栈引用b 。 此重新分配仅对此函数是本地的 – 只要作用域离开此函数,重新分配就会丢失。

如果您真的想要替换调用者的引用,则需要更改签名,如下所示:

 void Replace(ref T a, T b) { a = b; } 

这改变了通过引用调用的调用 – 实际上我们将调用者的变量的地址传递给函数,然后允许被调用的方法改变调用方法的变量。

但是,现在:

  • 通过引用传递通常被认为是一个坏主意 – 相反,我们应该在返回值中传递返回数据,如果有多个要返回的变量,则使用包含所有这些的Tuple或自定义classstruct返回变量。
  • 改变(’变异’)被调用方法中的共享值(甚至引用)变量是不受欢迎的,尤其是function编程社区,因为这会导致棘手的错误,尤其是在使用多个线程时。 相反,优先考虑不可变变量,或者如果需要变异,则考虑更改变量的(可能很深的)副本。 您可能会发现有关“纯函数”和“常量正确性”的主题有趣的进一步阅读。

编辑

这两个图可能有助于解释。

按值传递(参考类型):

在您的第一个实例中( Replace(T a,T b) ), ab按值传递。 对于引用类型 , 这意味着引用被复制到堆栈并传递给被调用的函数。

在此处输入图像描述

  1. 你的初始代码(我称之为main )在托管堆上分配两个MyPoint对象(我称之为point1point2 ),然后分配两个局部变量引用ab ,分别引用这些点(光蓝色箭头):
 MyPoint a = new MyPoint { x = 1, y = 2 }; // point1 MyPoint b = new MyPoint { x = 3, y = 4 }; // point2 
  1. 调用Replace(a, b)然后将两个引用的副本推送到堆栈(红色箭头)。 方法Replace将这些视为两个参数,也称为ab ,它们仍然分别指向point1point2 (橙色箭头)。

  2. 赋值, a = b; 然后更改Replace方法a局部变量,使得a现在指向b所引用的同一个对象(即point2 )。 但请注意,此更改仅适用于Replace的本地(堆栈)变量,此更改只会影响Replace后续代码(深蓝色线条)。 它不会以任何方式影响调用函数的变量引用,NOR会改变堆上的point1point2对象。

通过参考:

但是,如果我们将更改调用更改为Replace(ref T a, T b) ,然后更改main以通过引用传递,即Replace(ref a, b)

在此处输入图像描述

  1. 和以前一样,堆上分配了两个点对象。

  2. 现在,当调用Replace(ref a, b) ,在调用期间仍然复制main s引用b (指向point2 ),现在通过引用传递 a ,这意味着main的a变量的“地址”被传递给Replace

  3. 现在当分配a = b ……

  4. 它是调用函数, maina变量引用,现在更新为引用point2 。 现在, mainReplace都可以看到重新分配给a所做的更改。 现在没有对point1引用

引用该对象的所有代码都可以看到对(堆分配的)对象实例的更改

在上面的两个场景中,实际上没有对堆对象point1point2进行任何更改,它只是传递和重新分配的局部变量引用。

但是,如果实际对堆对象point1point2进行了任何更改,则对这些对象的所有变量引用都将看到这些更改。

所以,例如:

 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, ax); Assert.AreEqual(21, by); } public void DoSomething(MyPoint a, MyPoint b) { ax = 53; by = 21; } 

现在,当执行返回到main ,所有对point1point2引用,包括main's变量ab ,现在它们将在下次读取点的xy值时“看到”这些变化。 您还将注意到变量ab仍然按值传递给DoSomething

对值类型的更改仅影响本地副本

值类型( System.Int32System.Double等原语和System.Double等结构)在堆栈上分配,而不是堆,并在传递给调用时逐字复制到堆栈中。 这里的一个区别是被调用函数对值类型字段或属性所做的更改只能由被调用函数在本地观察,因为它只会改变值类型的本地副本。

例如,使用可变结构体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关键字的作用类似于允许通过引用传递值类型变量,即调用者的值类型变量的“地址”被传递到堆栈,现在可以直接分配调用者的已分配变量。

C#实际上是按值传递的。 你得到了它通过引用传递的幻觉,因为当你传递一个引用类型时,你得到一个引用的副本(引用是通过值传递的)。 但是,由于您的替换方法正在用另一个引用替换该引用副本,因此它实际上什么都不做(复制的引用立即超出范围)。 您可以通过添加ref关键字来实际通过引用传递:

 void Replace(ref T a, T b) { a = b; } 

这会得到你想要的结果,但在实践中有点奇怪。

C#不是通过引用传递引用类型对象,而是通过值传递引用 。 意思是你可以搞乱他们的内心,但你不能改变任务本身。

阅读Jon Skeet撰写的这篇精彩文章 ,深入了解。

在C#中,传递给方法的所有参数都按值传递。
现在,在你继续大喊:

值类型的值是在引用类型的值实际上是引用时复制的数据。

因此,当您将对象引用传递给方法并更改该对象时,更改将反映在方法之外,因为您正在操作分配对象的相同内存。

 public void Func(Point p){px = 4;} Point p = new Point {x=3,y=4}; Func(p); // px = 4, py = 4 

现在让我们来看看这个方法:

 public void Func2(Point p){ p = new Point{x=5,y=5}; } Func2(p); // px = 4, py = 4 

所以这里没有发生变化,为什么? 您的方法只是创建了一个新的Point并更改了p的引用(通过值传递),因此更改是本地的。 你没有操纵这一点,你改变了参考,你在本地做了。

并且有一个ref关键字可以节省一天:

 public void Func3(ref Point p){ p = new Point{x=5,y=5}; } Func3(ref p); // px = 5, py = 5 

你的例子也是如此。 您为新点引用了一个点,但是您是在本地完成的。

你不明白通过引用传递的是什么。 您的Replace方法正在创建Point对象的副本 – 按值传递(这实际上是更好的方法)。

要通过引用传递,以便a和b都引用内存中的相同点,您需要在签名中添加“ref”。

你说不对。

它类似于Java – 一切都是通过价值传递的! 但你必须知道,价值是什么。

在原始数据类型中,值是数字本身。 在其他情况下,它是参考。

但是,如果您复制对另一个变量的引用,它将保留相同的引用,但不引用该变量(因此它不会通过C ++中已知的引用传递)。

默认情况下,c#按值传递ALL争论…这就是为什么a和b在示例中不受全局范围影响的原因。 这是对那些失败选民的参考 。