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
上的相同操作在全局范围内替换a
和b
。
例:
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!?
我希望a
和b
在内存中指向相同的引用…有人可以澄清我哪里出错吗?
回覆:
它是普遍公认的(至少在C#中),当你通过引用传递时,该方法包含对被操纵对象的引用,而当你传递值时,该方法复制被操纵的值…
TL; DR
除非使用ref或out关键字传递变量,否则C#会按值将变量传递给方法。
详细地
问题是有两个不同的概念:
- 值类型(例如int)vs引用类型(例如字符串)
- 传递值(默认行为)vs传递参考(ref,out)
除非您通过引用显式传递(任何)变量,否则通过使用out
或ref
关键字,参数将在C#中按值传递,而不管变量是值类型还是引用类型。
传递值类型(例如int
, float
或类似DateTime
结构)时,被调用函数获取整个值类型的副本 (通过堆栈)。
退出被调用函数时,对值类型的任何更改以及对副本的任何属性/字段的任何更改都将丢失。
但是,对于引用类型(如string
和自定义类,如MyPoint
类),它是对在堆栈上复制和传递的相同共享对象实例的reference
。
这意味着:
- 对共享对象的字段或属性的任何更改都是永久性的(即对
x
或y
任何更改) - 但是,引用本身仍然被复制(通过值传递),因此对引用副本的任何更改都将丢失。 这就是您的代码无法按预期工作的原因
这里发生了什么:
void Replace(T a, T b) { a = b; }
对于引用类型T
,表示将对象a
的局部变量(堆栈)引用重新分配给本地堆栈引用b
。 此重新分配仅对此函数是本地的 – 只要作用域离开此函数,重新分配就会丢失。
如果您真的想要替换调用者的引用,则需要更改签名,如下所示:
void Replace(ref T a, T b) { a = b; }
这改变了通过引用调用的调用 – 实际上我们将调用者的变量的地址传递给函数,然后允许被调用的方法改变调用方法的变量。
但是,现在:
- 通过引用传递通常被认为是一个坏主意 – 相反,我们应该在返回值中传递返回数据,如果有多个要返回的变量,则使用包含所有这些的
Tuple
或自定义class
或struct
返回变量。 - 改变(’变异’)被调用方法中的共享值(甚至引用)变量是不受欢迎的,尤其是function编程社区,因为这会导致棘手的错误,尤其是在使用多个线程时。 相反,优先考虑不可变变量,或者如果需要变异,则考虑更改变量的(可能很深的)副本。 您可能会发现有关“纯函数”和“常量正确性”的主题有趣的进一步阅读。
编辑
这两个图可能有助于解释。
按值传递(参考类型):
在您的第一个实例中( Replace
), a
和b
按值传递。 对于引用类型 , 这意味着引用被复制到堆栈并传递给被调用的函数。
- 你的初始代码(我称之为
main
)在托管堆上分配两个MyPoint
对象(我称之为point1
和point2
),然后分配两个局部变量引用a
和b
,分别引用这些点(光蓝色箭头):
MyPoint a = new MyPoint { x = 1, y = 2 }; // point1 MyPoint b = new MyPoint { x = 3, y = 4 }; // point2
-
调用
Replace
然后将两个引用的副本推送到堆栈(红色箭头)。 方法(a, b) Replace
将这些视为两个参数,也称为a
和b
,它们仍然分别指向point1
和point2
(橙色箭头)。 -
赋值,
a = b;
然后更改Replace
方法a
局部变量,使得a
现在指向b
所引用的同一个对象(即point2
)。 但请注意,此更改仅适用于Replace的本地(堆栈)变量,此更改只会影响Replace
后续代码(深蓝色线条)。 它不会以任何方式影响调用函数的变量引用,NOR会改变堆上的point1
和point2
对象。
通过参考:
但是,如果我们将更改调用更改为Replace
,然后更改main
以通过引用传递,即Replace(ref a, b)
:
-
和以前一样,堆上分配了两个点对象。
-
现在,当调用
Replace(ref a, b)
,在调用期间仍然复制main
s引用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, ax); Assert.AreEqual(21, by); } public void DoSomething(MyPoint a, MyPoint b) { ax = 53; by = 21; }
现在,当执行返回到main
,所有对point1
和point2
引用,包括main's
变量a
和b
,现在它们将在下次读取点的x
和y
值时“看到”这些变化。 您还将注意到变量a
和b
仍然按值传递给DoSomething
。
对值类型的更改仅影响本地副本
值类型( System.Int32
, System.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在示例中不受全局范围影响的原因。 这是对那些失败选民的参考 。