为什么在复制之后原始对象发生了变化,而不使用ref参数?
在工作中,我们遇到了一个问题,在我们通过方法发送副本后,原始对象发生了变化。 我们确实通过在原始课程中使用IClonable
找到了解决方法,但我们无法找到它为什么会在第一时间发生。
我们编写了这个示例代码来重现问题(类似于我们的原始代码),并希望有人能够解释它为什么会发生。
public partial class ClassRefTest : System.Web.UI.Page { protected void Page_Load(object sender, EventArgs e) { var myclass = new MyClass(); var copy = myclass; myclass.Mystring = "jadajadajada"; Dal.DoSomeThing(copy); lit.Text = myclass.Mystring; //Text is expected to be jadajadajada, but ends up to be referenced } } public class MyClass { public string Mystring { get; set; } } public static class Dal { public static int? DoSomeThing(MyClass daclass) { daclass.Mystring = "referenced"; return null; } }
正如您所看到的,在DoSomething()
方法中,我们没有使用任何ref
参数,但lit.Text
最终仍然被引用。
为什么会这样?
解释这是如何工作总是很有趣。 当然,我的解释不能与Jon Skeet或Joseph Albahari的辉煌相提并论 ,但我会尝试。
在C编程的旧时代,掌握指针的概念是使用该语言的基础。 这么多年过去了,现在我们称它们为引用,但它们仍然是……美化指针,如果你了解它们是如何工作的,你就成了程序员的一半(开玩笑)
什么是参考? 我会告诉你一个非常简短的回答。 它是存储在变量中的数字,该数字表示数据所在的内存中的地址。
我们为什么需要参考? 因为处理单个数字非常简单,我们可以使用它来读取数据的内存区域,而不是将整个对象及其所有字段与我们的代码一起移动。
那么,当我们写作时会发生什么
var myclass = new MyClass();
我们都知道这是对MyClass
类的构造函数的调用,但是对于Framework,它也是一个提供内存区域的请求,其中实例的值(属性,字段和其他内部管理信息)存在并存在于一个特定的时间点。 假设MyClass需要100个字节来存储它需要的一切。 框架以某种方式搜索计算机内存,并假设它在由地址4200标识的内存中找到一个位置。该值(4200)是它分配给var myclass
它是一个指向内存的指针(oops)它是对象实例的引用)
现在打电话会发生什么?
var copy = myclass;
没什么特别的。 copy
变量获取myclass
(4200)的相同值。 但是这两个变量引用了相同的内存区域,因此使用其中一个或另一个没有任何区别。 内存区域(MyClass的实例)仍然位于我们的虚构内存地址4200。
myclass.Mystring = "jadajadajada";
它使用引用值作为基值来查找属性占用的内存区域,并将其值设置为保留文字字符串的实习区域。 如果我可以用指针进行类比,就像你取基本内存(4200)一样,添加一个偏移量来找到表示属性MyString的引用保存在我们的对象实例占用的100字节边界内的点。 假设MyString引用超过了内存区域的开头42个字节。 添加42到4200个yelds 4242,这就是存储对文字“jadajadajada”的引用的点。
Dal.DoSomeThing(copy);
这里的问题(你有问题的地方)。 当您传递copy
变量时,不要认为框架重复搜索内存区域并复制新区域中原始区域的所有内容。 不,这几乎是不可能的(想想MyClass是否包含属性是另一个类的实例等等……它永远不会停止。)因此传递给DoSomeThing
方法的值再次是参考值4200。 value被自动分配给声明为DoSomething
输入参数的局部变量daclass
(就像你之前用var copy = myclass;
明确做过的那样)。
此时很明显,使用daClass
任何操作daClass
作用于原始实例占用的相同内存区域,并且当代码返回到起始点时,您会看到结果。
请原谅技术专家的用户请原谅。 特别是对于我对“记忆地址”一词的随意和不精确的使用。
这是正常的,因为你的MyClass
是一个引用类型,所以你传递一个对原始数据的引用而不是数据本身,这就是为什么这里的预期行为是对参考传递C#的参考类型的解释
引用类型是一种类型,其值为对适当数据的引用,而不是数据本身
MSDN的文档非常清楚地说明了这一点。 默认情况下,值类型作为副本传递,默认情况下将对象作为引用传递。 C#中的方法
我在这里看到两个问题……
制作一个对象的副本
var copy = myClass;
不做副本 – 它真正做的是为myClass创建第二个引用(“指针”)(命名变量“copy”是误导性的)。 所以你有myClass
和副本指向同一个确切的对象。
要制作副本,您必须执行以下操作:
var copy = new MyClass(myClass);
请注意,我创建了一个新对象。
通过参考传递
-
在没有ref的情况下传递值类型变量时,接收方法无法更改变量。
示例:
DoSomething(int foo)
– DoSomething不能影响foo本身的值。 -
使用ref传递值类型变量时,可以更改变量
示例:
DoSomething(ref int foo)
– 如果DoSomething更改foo,它将保持更改状态。 -
传递没有ref的对象时,可以更改对象的数据,但不能更改对象的引用。
void DoSomething(MyClass myClass) { myClass.myString = "ABC" // the string is set to ABC myClass = new MyClass(); // has no affect - or may not even be allowed }
- 使用ref传递对象时,可以更改对象的数据,并可以更改对象的引用。
void DoSomething(ref MyClass myClass) { myClass.myString = "ABC" // the string is set to ABC myClass = new MyClass(); // the string will now be "" since myClass has been changed }