C#中的引用类型


79

考虑以下代码:

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; }
}

显然,当分配person1person2Name属性person2更改时,Nameofperson1也将更改。person1person2具有相同的参考。

为什么在时person2 = nullperson1变量也不会为null?

Answers:


183

这两个personperson2引用,同一个对象。但是这些是不同的参考。所以当你跑步时

person2 = null;

您仅更改reference person2,而referenceperson和相应的对象保持不变。

我想最好的解释方法是简化图示。这是以前 的情况person2 = null

空分配之前

这是空分配的图片:

在此处输入图片说明

如您所见,在第二张图片上person2没有引用任何内容(或者null严格来说,因为没有引用和引用null是不同的条件,请参见Rune FS的注释),同时person仍引用现有对象。


22
换句话说,person和person2指向某物,然后person2通过设置为null指向空。
rro

2
@ShahroozJefri ㇱ基本上,person1并且person2是包含一个地址2张不同的名片。如果您这样做,person1 = person2person1现在是的副本person2。它们仍然是不同的名片,但是它们都包含指向相同对象的相同地址。对象本身不会改变。
Nolonar

我将删除person2第二张图片中的箭头,因为它没有引用任何内容-它不是悬挂的引用。
Sameer Singh 2013年

@SameerSingh,已完成,感谢您的回复。最初我也有同样的想法,但是由于某种原因改变了主意。
Andrei

2
当使用引用时,创建一个变量并将其分配给另一个指针,将创建一个称为Alias的事物,我们假设其中有3个变量指向相同的内存位置。当您使这些变量之一无效时,指向该内存位置的变量要少一个,当所有变量都指向NULL时,GC会清理该位置以分配新变量,至少在Java中,GC的工作方式为D:
NemesisDoom

55

考虑person1person2 作为指向存储中某个位置的指针。在第一步中,仅从person1存储中保存对象的地址,然后从存储person2中保存对象的存储位置的地址。稍后,当您分配null给时person2,不person1会受影响。这就是为什么您看到结果的原因。

您可能会读:Joseph Albahari的值与引用类型

但是,使用引用类型时,将在内存中创建一个对象,然后通过一个单独的引用(类似于指针)来处理该对象。

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

在此处输入图片说明

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

在此处输入图片说明

创建一个新的引用(指针)person2,该引用指向存储中的相同对象。

在此处输入图片说明

person2由于两个引用都指向同一个对象,因此将对象属性Name更改为新值Console.WriteLine(person1.Name);output Shahrooz

在此处输入图片说明

分配后null,以person2参考,这将指向什么,但person1仍抱着参考对象。

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


3
对“堆”的任何引用都是无关的实现细节。我什至不确定这是真的,当然也不必如此。
jmoreno

@jmoreno,这就是为什么最后一段实际上是我遵循约瑟夫·阿尔巴哈里(Joseph Albahari)文章中的样式的原因,答案是答案的顶部
Habib

4
就此答案而言,@ jmoreno正确地表明类型的对象person将存储在堆及其引用中,person1并且person2将在堆栈中。但我同意其更多的实现细节。但是在答案中有堆放(如albahari的文章)会使IMO更加清晰。
哈比卜

1
@jmoreno和Habin:我用存储代替了堆的使用,因为通常称为堆的特定存储机制的使用或不使用*是无关紧要的。
Pieter Geerkens

1
很好的答案,但是要提出的问题有点过头了。
Razor

14

您已更改person2为引用null,但person1此处未引用。

我的意思是,如果我们看一下person2,并person1指派那么这两个引用相同的对象之前。然后,您指定person2 = null,因此第2个人现在引用的是其他类型。它没有删除person2所引用的对象。

我创建了以下gif来说明这一点:

在此处输入图片说明


13

因为您已将参考设置为null

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

将它们视为一个变量,该变量的偏移量为0。person值为120。person2值为120。偏移量120的数据为Person对象。执行此操作时:

person2 = null;

..you're实际上是说,person2 = 0;。但是,person值仍为120。


所以人2和人有不同的参考?

它们引用相同的对象..但它们是单独的引用。这归结为copy-by-value语义。您正在复制参考值。
西蒙·怀特海德

4

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

但是,当设置person2为时null,您将创建person2一个空指针,因此不再指向同一对象person。它不会对对象本身做任何事情来销毁它,并且由于person仍然指向/引用该对象,它也不会被垃圾回收杀死。

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


2

person1person2指向相同的内存地址。当您为null时person2,您将对引用(而不是内存地址)为空,因此person1继续引用该内存地址,这就是原因。如果将更Classs Person改为Struct,则行为将改变。


它们可能指向也可能不指向相同的内存地址(或与此相关的任何地址)。重要的是它们标识相同的对象。在某些类型的并发垃圾收集器上,可能的是,在重定位对象时,某些引用可能包含旧地址,而另一些引用标识了新地址。[写入其中一个地址的代码可能必须阻塞,直到所有引用都被更新,并且比较地址的代码将必须查看一个地址是否为“当前”地址,如果不是,则找到与旧地址相关的“新”地址。]
supercat

2

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

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

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


1

person1和person2是堆栈上的两个独立引用,指向堆上的同一Person对象。

当您删除其中一个引用时,会将其从堆栈中删除,并且不再指向堆上的Person对象。剩下的另一个引用仍然指向堆上现有的Person对象。

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


1

当您创建引用类型时,它实际上会复制一个引用,并且所有对象都指向相同的内存位置,但是,如果您将Person2 = Null分配给它,则它将无效,因为person2只是引用人的副本,而我们只有 删除了一个副本参考


1

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

0
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; }
}

@doctorlove实际上忘记了注释:-(,对于引用类型,只分配了一个内存空间,Person 2或Person 1 ...只是该引用类型的副本,指向内存位置,删除了引用类型的副本不会造成任何影响。
苏拉杰·辛格

@doctorlove谢谢您的建议,我会记住这一点。
Suraj Singh

0

您首先将引用复制person1person2。现在,person1person2引用相同的对象,这意味着Name可以在两个变量下观察到对该对象的值的修改(即更改属性)。然后,在分配null时,您将删除刚刚分配给的引用person2。它仅分配给person1现在。请注意,引用本身不是更改。

如果您有一个通过引用接受参数的函数,则可以通过reference to person1 reference传递,并且可以更改引用本身:

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;}
}

0

说:

person2.Name = "Shahrooz";

跟随引用person2并“突变”(更改)引用恰好导致的对象。参考本身(person2)不变; 我们仍然在相同的“地址”中引用相同的实例。

分配给 person2如:

person2 = null;

更改参考。没有对象被更改。在这种情况下,参考箭头从一个对象“移动”到“无” null。但是,类似的分配person2 = new Person();也只会更改参考。没有对象被突变。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.