C#中的引用types
考虑这个代码:
public class Program { private static void Main(string[] args) { var person1 = new Person { Name = "Test" }; Console.WriteLine(person1.Name); Person person2 = person1; person2.Name = "Shahrooz"; Console.WriteLine(person1.Name); //Output: Shahrooz person2 = null; Console.WriteLine(person1.Name); //Output: Shahrooz } } public class Person { public string Name { get; set; } }
很明显,当将person1
分配给person2
并更改person2
的Name
属性时, person2
的Name
也将被更改。 person1
和person2
有相同的参考。
为什么当person2 = null
, person1
variables也不会为空?
person
和person2
都是对同一个对象的引用 。 但是这些是不同的参考。 所以当你跑步的时候
person2 = null;
你只改变参考person
person2
,不改变参考person2
相应的对象。
我想解释这个最好的方法是一个简单的例子。 person2 = null
之前情况如何:
这是空分配后的图片:
正如你所看到的,在第二张图片上, person2
没有引用任何东西(严格地说,由于引用没有和引用null
是不同的条件,请参阅符文FS的注释),而person
仍然引用现有的对象。
将person1
和person2
作为指向存储中某个位置的指针 。 在第一步中,只有person1
存储对象的地址,后面的person2
存储对象的内存地址。 稍后,当您为person2
指定null
时, person1
保持不受影响。 这就是为什么你看到结果。
您可以阅读: 来自Joseph Albahari的Value vs Reference Types
但是,对于引用types,在内存中创build一个对象,然后通过一个单独的引用来处理,而不是像指针一样。
我将尝试使用下面的图来描述相同的概念。
创build一个新types的person对象, person1
引用(指针)指向存储器中的内存位置。
创build一个新的参考(指针) person2
指向相同的存储。
由于两个引用都指向同一个对象Console.WriteLine(person1.Name);
将对象属性Name更改为新值Console.WriteLine(person1.Name);
输出Shahrooz
。
在将person2
引用赋值为null
之后,它将指向任何内容,但是person1
仍然保存对该对象的引用。
(最后对于内存pipe理,你应该看到堆栈是一个实现细节,第一部分 , 堆栈是一个实现细节, Eric Lippert的第二部分 )
您已将person2
更改为引用null
,但是person1
不在此处引用。
我的意思是,如果我们在赋值之前看person2
和person1
,那么两个引用同一个对象。 然后,你分配person2 = null
,所以人2现在引用一个不同的types。 它没有删除person2
被引用的对象。
我已经创build了这个gif来说明:
因为您已将参考设置为null
。
当您将引用设置为null
,引用本身是null
,而不是它引用的对象。
把它们看作一个variables,保存从0开始的偏移量。 person
的值是120. person2
的值是120.偏移量120的数据是Person
对象。 当你这样做:
person2 = null;
..你有效地说, person2 = 0;
。 但是, person
的价值仍然是120。
person
person2
指向同一个对象。 因此,当你改变任何一个的名字时,两者都会被改变(因为它们指向内存中的相同结构)。
但是当你把person2
设置为null
,你把person2
变成了一个空指针,这样就不会再指向同一个对象了。 它不会对对象本身做任何事情来摧毁它,因为person
仍然指向/引用对象,它不会被垃圾收集杀死。
如果您还设置person = null
,并且没有其他对象的引用,则最终将被垃圾收集器删除。
person1
和person2
指向相同的内存地址。 当你为空的人person2
,你空引用,而不是内存地址,所以person1
继续重新回到这个内存地址,这就是原因。 如果将Classs Person
更改为Struct
,则行为将会改变。
我觉得最有用的是将引用types视为持有对象ID。 如果有一个Car
类的variables,语句myCar = new Car();
要求系统创build一辆新车并报告其ID(假设它是对象#57); 然后将“object#57”放入variablesmyCar
。 如果有人写Car2 = myCar;
,将“object#57”写入variablesCar2。 如果有人写car2.Color = blue;
,指示系统find由Car2识别的汽车(例如对象#57)并将其涂成蓝色。
唯一直接在对象ID上执行的操作是创build一个新对象并获取ID,得到一个“空白”ID(即空),将对象ID复制到一个variables或存储位置,对象ID匹配(引用同一个对象)。 所有其他请求要求系统查找由ID引用的对象,并对该对象执行操作(不影响variables或持有该ID的其他实体)。
在.NET的现有实现中,对象variables很可能保存指向存储在垃圾回收堆上的对象的指针,但是这是一个无益的实现细节,因为对象引用和其他types的指针之间存在严重差异。 通常假设一个指针代表的东西的位置将保持足够长的时间来处理。 对象引用不。 一段代码可以通过引用位于地址0x12345678的对象加载SI寄存器,开始使用它,然后在垃圾收集器将对象移动到地址0x23456789时被中断。 这听起来像是一场灾难,但垃圾将检查与代码相关的元数据,观察代码使用SI来保存它正在使用的对象的地址(即0x12345678),确定已经移动到0x12345678的对象到0x23456789,并在返回之前更新SI来保存0x23456789。 请注意,在这种情况下,存储在SI中的数值已被垃圾收集器更改,但是在移动之前和之后引用同一个对象 。 如果在移动之前引用了自程序启动以来创build的第23,592个对象,则将在之后继续执行。 有趣的是,.NET不会为大多数对象存储任何唯一且不可变的标识符。 给定一个程序内存的两个快照,并不总是能够判断第一个快照中是否存在任何特定的对象,或者是否所有的对象都被抛弃了,并创build了一个新的对象,看起来像是在所有可观察的细节。
person1和person2是堆栈上的两个单独的引用,指向堆上的同一个Person对象。
删除其中一个引用时,将从堆栈中删除,而不再指向堆上的Person对象。 另一个引用仍然存在,仍然指向堆上现有的Person对象。
一旦对Person对象的所有引用都被删除,垃圾收集器最终将从内存中删除该对象。
当你创build一个引用types的时候,它实际上是复制了一个指向同一个内存位置的所有对象的引用,但是如果你指定了Person2 = Null,它将不起作用,因为person2只是引用人的一个副本,而我们刚刚删除了一个副本的参考 。
请注意,您可以通过更改为struct
来获取值语义。
public class Program { static void Main() { var person1 = new Person { Name = "Test" }; Console.WriteLine(person1.Name); Person person2 = person1; person2.Name = "Shahrooz"; Console.WriteLine(person1.Name);//Output:Test Console.WriteLine(person2.Name);//Output:Shahrooz person2 = new Person{Name = "Test2"}; Console.WriteLine(person2.Name);//Output:Test2 } } public struct Person { public string Name { get; set; } }
public class Program { private static void Main(string[] args) { var person = new Person {Name = "Test"}; Console.WriteLine(person.Name); Person person2 = person; person2.Name = "Shahrooz"; Console.WriteLine(person.Name);//Output:Shahrooz // Here you are just erasing a copy to reference not the object created. // Single memory allocation in case of reference type and parameter // are passed as a copy of reference type . person2 = null; Console.WriteLine(person.Name);//Output:Shahrooz } } public class Person { public string Name { get; set; } }
您首先person1
person2
的引用复制到person2
。 现在, person1
和person2
引用同一个对象,这意味着可以在这两个variables下观察到该对象的值的修改(即更改Name
属性)。 然后,在分配空值时,您将删除刚分配给person2
的引用。 现在只分配给person1
。 请注意,引用本身并没有改变。
如果你有一个通过引用接受参数的函数,你可以通过reference to person1
传递reference to person1
的引用 ,并且能够改变引用本身:
public class Program { private static void Main(string[] args) { var person1 = new Person { Name = "Test" }; Console.WriteLine(person1.Name); PersonNullifier.NullifyPerson(ref person1); Console.WriteLine(person1); //Output: null } } class PersonNullifier { public static void NullifyPerson( ref Person p ) { p = null; } } class Person { public string Name{get;set;} }
他说:
person2.Name = "Shahrooz";
跟随着person2
的引用,并且“改变”(改变)引用发生的对象。 参考本身( person2
的值 )不变; 我们仍然在相同的“地址”处引用相同的实例。
如下所示分配给person2
:
person2 = null;
改变参考 。 没有对象被改变。 在这种情况下,参考箭头从一个对象“移动”到“无”,为null
。 但是像person2 = new Person();
也只会改变参考。 没有对象被突变。