C # 中的引用类型

考虑下面的代码:

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并且 person2Name属性发生变化时,person1Name也会发生变化。person1person2具有相同的引用。

为什么当 person2 = null时,person1变量也不为空?

8270 次浏览

您已经将 person2更改为引用 null,但是 person1没有在那里引用。

我的意思是,如果我们在赋值之前查看 person2person1,那么它们都引用同一个对象。然后您分配 person2 = null,因此人2现在引用一个不同的类型。它没有删除引用 person2的对象。

我创建了这个 gif 来说明它:

enter image description here

personperson2都是 参考文献,对同一个对象。但是这些是不同的引用。所以当你运行

person2 = null;

只改变引用 person2,保留引用 person和对应的对象不变。

我想解释这个问题的最好方法是用一个简单的例子。以下是 之前 person2 = null的情况:

Before null assignment

这是图片 之后空赋值:

Enter image description here

正如您所看到的,在第二张图片中,person2没有引用任何东西(或者严格地说,null,因为没有引用任何东西和引用 null是不同的条件,参见 符文 FS的注释) ,而 person仍然引用一个现有的对象。

因为您已经将引用设置为 null

当您设置对 null的引用时,引用本身是 null. . 而不是它引用的对象。

可以将它们看作是一个从0保存偏移量的变量。person的值为120。person2的值为120。偏移量120处的数据是 Person对象。当你这样做的时候:

person2 = null;

. . 你实际上说,person2 = 0;。然而,person仍然有值120。

personperson2都指向同一个对象。因此,当您更改其中任何一个的名称时,两者都将被更改(因为它们指向内存中的相同结构)。

但是当您将 person2设置为 null时,您将使 person2成为一个空指针,因此 不再指向与 person相同的对象。它不会对对象本身做任何事情来破坏它,而且由于 person仍然指向/引用对象,所以它也不会被垃圾收集杀死。

如果您还设置了 person = null,并且没有对该对象的其他引用,那么它最终将被垃圾收集器删除。

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是堆栈上指向堆上同一 Person 对象的两个独立引用。

当您删除其中一个引用时,它将从堆栈中移除,不再指向堆上的 Person 对象。另一个引用仍然存在,仍然指向堆上现有的 Person 对象。

删除对 Person 对象的所有引用之后,垃圾收集器最终将从内存中删除该对象。

当你创建一个引用类型的时候,它实际上是在复制一个引用,所有的对象都指向同一个内存位置,但是如果你分配了 Person2 = Null,它就不会有任何效果,因为 person 2只是一个引用 person 的副本,我们只有 删除引用的副本

注意,您可以通过更改为 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; }
}

考虑 person1person2作为指引到存储中的某个位置。在第一步中,只有 person1保存存储器中对象的地址,然后 person2保存存储器中对象的内存位置的地址。稍后,当您将 null分配给 person2时,person1保持不受影响。这就是为什么你会看到结果。

你可以阅读: 值与引用类型

但是,对于引用类型,将在内存中创建对象,并且 然后通过一个单独的引用进行处理ーー更像是一个指针。

我将尝试使用下面的图表描述相同的概念。

enter image description here

创建一个新的 person 类型对象,并且 person1引用(指针)指向存储器中的内存位置。

enter image description here

创建了一个新的引用(指针) person2,它指向存储中的相同内容。

enter image description here

通过 person2将对象属性 Name 更改为新值,因为两个引用都指向同一个对象,所以 Console.WriteLine(person1.Name);输出 Shahrooz

enter image description here

在将 null分配给 person2引用之后,它将不会指向任何东西,但是 person1仍然保存着对该对象的引用。

(最后,对于内存管理,您应该看到 Eric Lippert 提供的 堆栈是一个实现细节,第一部分堆栈是一个实现细节,第二部分)

person1person2指向相同的内存地址。 当您为 person2设置空值时,您将为引用设置空值,而不是为内存地址设置空值,因此 person1将继续引用该内存地址,这就是原因。如果你把 Classs Person改成 Struct,行为就会改变。

首先将对 person1的引用复制到 person2。现在 person1person2引用同一个对象,这意味着对该对象的值的修改(即改变 Name属性)可以在两个变量下观察到。然后,在赋值 null 时,您将删除刚刚赋值给 person2的引用。现在只分配给 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;}
}

我发现将引用类型视为保存对象 ID 非常有帮助。如果有一个类型为 Car的变量,那么语句 myCar = new Car();要求系统创建一辆新车并报告它的 ID (假设它是对象 # 57) ; 然后它将“对象 # 57”放入变量 myCar中。如果写入 Car2 = myCar;,则将“ object # 57”写入变量 Car2。如果一个人写 car2.Color = blue;,这指示系统找到 Car2标识的汽车(例如对象 # 57)并将其涂成蓝色。

直接在对象 ID 上执行的唯一操作是创建一个新对象并获得 ID,获得一个“空”ID (即 null) ,将对象 ID 复制到一个可以保存它的变量或存储位置,检查两个对象 ID 是否匹配(引用同一个对象)。所有其他请求都要求系统查找 ID 引用的对象并对该对象进行操作(而不影响持有 ID 的变量或其他实体)。

在现有的实现中。NET 中,对象变量可能包含指向存储在垃圾回收堆中的对象的指针,但是这是一个没有帮助的实现细节,因为对象引用和任何其他类型的指针之间有一个关键的区别。一个指针通常被假定为代表某个东西的位置,这个东西可以停留足够长的时间来处理。对象引用不会。一段代码可以加载 SI 寄存器,引用位于地址0x12345678的对象,开始使用它,然后在垃圾收集器将对象移动到地址0x23456789时中断。这听起来像是一场灾难,但是垃圾会检查与代码相关的元数据,观察代码使用 SI 来保存它正在使用的对象的地址(即0x12345678) ,确定0x12345678的对象已经移动到0x23456789,并在返回之前更新 SI 来保存0x23456789。请注意,在那种情况下,SI 中存储的数值由垃圾收集器更改,但它在移动之前和之后引用 同样的东西。如果在移动之前它引用了自程序启动以来创建的第23,592个对象,那么它将在程序启动之后继续这样做。有趣的是。NET 不为大多数对象存储任何唯一的和不可变的标识符; 给定一个程序内存的两个快照,它不可能总是能够告诉第一个快照中的任何特定对象是否存在于第二个快照中,或者它的所有跟踪是否已经被放弃,并且创建了一个在所有可观察的细节中看起来像它的新对象。

说:

person2.Name = "Shahrooz";

跟随 person2的引用并“变更”(更改)引用恰好导向的对象。引用本身(person2价值)没有变化; 我们仍然在相同的“地址”上引用相同的实例。

赋予 person2如下:

person2 = null;

改变 参考文献。不改变任何对象。在这种情况下,引用箭头从一个对象“移动”到“无”,null。但像 person2 = new Person();这样的赋值也只会改变引用。没有物体变异。