考虑以下代码(我故意将MyPoint编写为该示例的引用类型)
public class MyPoint
{
public int x;
public int y;
}
普遍公认(至少在C#中),当您通过引用传递时,该方法包含对要操作的对象的引用,而当您通过值传递时,该方法将复制要操作的值,因此全局范围内的值是不受影响。
例子:
void Replace<T>(T a, T b)
{
a = b;
}
int a = 1;
int b = 2;
Replace<int>(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<MyPoint>(a, b);
// a and b remain unaffected in global scope since a and b...ummm!?
我期望a
并b
指向内存中的同一参考...有人可以澄清我哪里出了问题吗?
回复:OP的断言
普遍公认(至少在C#中),当您通过引用传递时,该方法包含对要操作的对象的引用,而当您通过值传递时,该方法将复制要操作的值...
TL; DR
不仅如此。除非您使用ref或out关键字传递变量,否则C#会按值将变量传递给方法,而不管变量是值类型还是引用类型。
如果通过引用传递,则被调用函数可以更改变量的地址(即,更改原始调用函数的变量的赋值)。
如果通过值传递变量:
由于这一切都相当复杂,因此我建议尽可能避免通过引用传递(相反,使用复合类或结构作为返回类型,或使用元组)
另外,在传递引用类型时,可以通过不更改(更改)传递给方法的对象的字段和属性来避免很多错误(例如,使用C#的不可变属性来防止对属性进行更改,并努力分配属性施工期间仅一次)。
详细地
问题在于有两个不同的概念:
除非您使用out
或ref
关键字通过引用显式传递(任何)变量,否则无论变量是值类型还是引用类型,参数都将按C#中的值传递。
当按值传递值类型(例如int
,float
或结构之类DateTime
)(即不带out
或ref
)时,被调用的函数将获得整个值类型的副本(通过堆栈)。
退出调用的函数时,对值类型的任何更改以及对副本的任何属性/字段的任何更改都将丢失。
但是,当通过传递引用类型(例如,自定义类,如您的MyPoint
类)时value
,将它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
}
这将调用改为按引用进行调用-实际上,我们正在将调用者变量的地址传递给函数,该函数随后允许被调用方法更改调用方法的变量。
但是,如今:
Tuple
或class
或struct
包含所有此类返回变量的自定义。编辑
这两个图可能有助于说明。
按值传递(引用类型):
在您的第一个实例(Replace<T>(T a,T b)
)中,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<Point>(a, b)
然后,对的调用将两个引用的副本推入堆栈(红色箭头)。方法Replace
将它们视为也称为a
和的两个参数b
,它们仍分别指向point1
和point2
(橙色箭头)。
然后,赋值a = b;
将更改Replace
方法的a
局部变量,从而a
现在指向与b
(即point2
)引用的对象相同的对象。但是,请注意,此更改仅适用于Replace的本地(堆栈)变量,并且此更改仅会影响Replace
(深蓝色线)中的后续代码。它不会以任何方式影响调用函数的变量引用,NOR完全不会更改堆上的point1
andpoint2
对象。
通过参考传递:
但是,如果我们将调用更改为Replace<T>(ref T a, T b)
,然后更改main
为通过a
引用传递,即Replace(ref a, b)
:
和以前一样,在堆上分配了两个点对象。
现在,在Replace(ref a, b)
调用when的同时,在调用过程中仍复制main
s的引用b
(指向point2
),a
现在通过引用传递,这意味着maina
变量的“地址”传递给Replace
。
现在完成分配后a = b
...
它是调用函数,它main
的a
变量引用现在已更新为reference point2
。a
现在,main
和都可以看到重新分配对所做的更改Replace
。现在没有参考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
关键字的工作方式类似于允许值类型变量为通过引用而通过,即,该呼叫方的值的变量类型“地址”被传递到堆栈,和调用者的分配的变量的赋值是现在直接可能的。
本文收集自互联网,转载请注明来源。
如有侵权,请联系[email protected] 删除。
我来说两句