为什么结构体内部的引用类型表现得像值类型

本文关键字:类型 结构体 内部 引用类型 为什么 | 更新日期: 2023-09-27 18:17:32

我是c#编程的初学者。我现在正在学习strings, structs, value typesreference types。正如这里和这里公认的答案,strings是指针存储在堆栈上而实际内容存储在堆上的引用类型。此外,如这里所述,structs是值类型。现在我试着用一个小例子来练习structsstrings:

struct Person
{
    public string name;
}
class Program
{
    static void Main(string[] args)
    {
        Person person_1 = new Person();
        person_1.name = "Person 1";
        Person person_2 = person_1;
        person_2.name = "Person 2";
        Console.WriteLine(person_1.name);
        Console.WriteLine(person_2.name);
    }
}
上面的代码片段输出
Person 1
Person 2

这让我很困惑。如果strings是引用类型,structs是值类型,那么person_1.name和person_2.name应该指向堆上相同的空间区域,不是吗?

为什么结构体内部的引用类型表现得像值类型

字符串是一种引用类型,其指针存储在堆栈中,而其实际内容存储在堆中

不不不。首先,不要再考虑堆栈和堆了。在c#中,这几乎总是错误的思考方式。c#为你管理存储生命周期

第二,虽然引用可以实现为指针,但引用在逻辑上不是指针。引用就是引用。c#既有引用又有指针。不要把它们弄混了。c#中没有指向string的指针,从来没有。有对string的引用。

第三,对字符串的引用可以存储在堆栈中,但也可以存储在堆中。当你有一个引用string的数组时,数组的内容在堆上。

现在让我们来讨论你的实际问题。

    Person person_1 = new Person();
    person_1.name = "Person 1";
    Person person_2 = person_1; // This is the interesting line
    person_2.name = "Person 2";

让我们来说明代码在逻辑上是做什么的。您的Person结构体只不过是一个字符串引用,因此您的程序与

相同:
string person_1_name = null; // That's what new does on a struct
person_1_name = "Person 1";
string person_2_name = person_1_name; // Now they refer to the same string
person_2_name = "Person 2"; // And now they refer to different strings

当您说person2 = person1时,这并不意味着变量person1现在是变量person2的别名。(在c#中有一种方法可以做到这一点,但不是这样。)它的意思是"将person1的内容复制到person2"。对字符串的引用是复制的值。

如果不清楚,可以尝试为变量绘制方框,为引用绘制箭头;复制结构体时,复制的是箭头,而不是方框

理解这一点的最好方法是完全理解什么是变量;简单地说,变量是包含的占位符。

那么这个值到底是什么呢?在引用类型中,存储在变量中的值是给定对象的引用(也就是地址)。在值类型中,值是对象本身

当您执行AnyType y = x;时,实际发生的情况是复制存储在x中的值,然后存储在y中。

因此,如果x是引用类型,xy都将指向同一个对象,因为它们都将保存同一引用的相同副本。如果x是值类型,则xy将保存两个相同但不同的对象。

一旦你理解了这一点,你就应该开始明白为什么你的代码是这样运行的。让我们一步一步来研究:

Person person_1 = new Person();

我们正在创建一个值类型的新实例。根据我之前的解释,person_1中的值存储是新创建的对象本身。这个值存储在哪里(堆或堆栈)是一个实现细节,它与你的代码如何行为完全无关。

person_1.name = "Person 1";

现在我们正在设置变量name,它恰好是person_1的一个字段。同样,根据前面的解释,name的值是指向存储string "Person 1"的内存中的某个地方的引用。同样,值或字符串的存储位置与此无关。

Person person_2 = person_1;

好的,这是有趣的部分。这里发生了什么?那么,对person_1中存储的值进行复制并存储在person_2中。由于该值恰好是值类型的实例,因此创建该实例的新副本并将其存储在person_2中。这个新的副本有自己的字段name,并且存储在该变量中的值同样是存储在person_1.name(对"Person 1"的引用)中的值的副本

person_2.name = "Person 2";

现在我们只是重新赋值变量person_2.name。这意味着我们正在存储一个指向内存中某个新stringnew引用。请注意,person_2.name最初持有person_1.name中存储的值的副本,因此无论您对person_2.name做什么都不会对person_1.name中存储的任何值产生影响,因为您只是简单地更改…没错,一个拷贝。这就是为什么你的代码是这样的。

作为练习,尝试用类似的方式推理出如果Person是引用类型,代码将如何表现。

每个struct实例都有自己的字段。person_1.nameperson_2.name的自变量。这些是而不是 static字段。

person_2 = person_1按值复制结构体。

string是不可变的这个事实并不能解释这个行为。

下面是class的相同情况,以演示差异:

class C { public string S; }
C c1 = new C();
C c2 = c1; //copy reference, share object
c1.S = "x"; //it appears that c2.S has been set simultaneously because it's the same object

这里,c1.Sc2.S指的是同一个变量。如果您将其设置为struct,那么它们将成为不同的变量(如您的代码中)。然后,c2 = c1返回一个struct值的副本,它之前是一个对象引用的副本。

字符串就是字符数组。下面的代码与您的代码类似,但是使用了数组。

public struct Lottery
{
    public int[] numbers;
}
public static void Main()
{
    var A = new Lottery();
    A.numbers = new[] { 1,2,3,4,5 };
    // struct A is in the stack, and it contains one reference to an array in RAM
    var B = A;
    // struct B also is in the stack, and it contains a copy of A.numbers reference
    B.numbers[0] = 10;
    // A.numbers[0] == 10, since both A.numbers and B.numbers point to same memory
    // You can't do this with strings because they are immutable
    B.numbers = new int[] { 6,7,8,9,10 };
    // B.numbers now points to a new location in RAM
    B.numbers[0] = 60;
    // A.numbers[0] == 10, B.numbers[0] == 60        
    // The two structures A and B *are completely separate* now.
}

所以如果你有一个包含引用(字符串,数组或类)的结构,并且你想实现ICloneable,请确保你也克隆了引用的内容。

public class Person : ICloneable
{
    public string Name { get; set; }
    public Person Clone()
    {
        return new Person() { Name=this.Name }; // string copy
    }
    object ICloneable.Clone() { return Clone(); } // interface calls specific function
}
public struct Project : ICloneable
{
    public Person Leader { get; set; }
    public string Name { get; set; }
    public int[] Steps { get; set; }
    public Project Clone()
    {
        return new Project()
        {
            Leader=this.Leader.Clone(),         // calls Clone for copy
            Name=this.Name,                     // string copy
            Steps=this.Steps.Clone() as int[]   // shallow copy of array
        };
    }
    object ICloneable.Clone() { return Clone(); } // interface calls specific function
}

我要强调的事实是,通过person_2.name = "Person 2",我们实际上是在内存中创建一个包含值"Person 2"的新字符串对象,并且我们正在分配该对象的引用。你可以这样想象:

class StringClass 
{
   string value; //lets imagine this is a "value type" string, so it's like int
   StringClass(string value)
   { 
      this.value = value
   }
}

通过person_2.name = "Person 2",您实际上正在做类似person_2.name = new StringClass("Person 2")的事情,而"name"仅保存,它表示内存中的地址

现在如果我重写你的代码:

struct Person
{
    public StringClass name;
}
class Program
{
    static void Main(string[] args)
    {
        Person person_1 = new Person();
        person_1.name = new String("Person 1"); //imagine the reference value of name is "m1", which points somewhere into the memory where "Person 1" is saved
        Person person_2 = person_1; //person_2.name holds the same reference, that is "m1" that was copied from person_1.name 
        person_2.name = new String("Person 2"); //person_2.name now holds a new reference "m2" to  a new StringClass object in the memory, person_1.name still have the value of "m1"
        person_1.name = person_2.name //this copies back the new reference "m2" to the original struct
        Console.WriteLine(person_1.name);
        Console.WriteLine(person_2.name);
    }
}
现在代码片段的输出:
Person 2
Person 2 

为了能够改变person_1.name的方式,你原来张贴在你的片段在struct你需要使用ref https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/ref

我认为这里的很多答案错过了原来的问题的重点,主要是因为这个例子不是很好。有些答案指出,字符串的不可变性是导致这种行为的正确原因,但在op的问题上,这确实没有什么区别。

一个更好的例子来说明我在开发团队中看到的一些关于字符串的困惑:

class SomeClass
{
    public int SomeNumber;
}
struct Person
{
    public string name;
    public SomeClass someClass;
}
class Program
{
    static void Main(string[] args)
    {
        Person person_1 = new Person();
        person_1.someClass = new SomeClass()
        {
            SomeNumber = 4,
        };
        person_1.name = "Person 1";
        Person person_2 = person_1;
        person_2.name += " changed";
        person_2.someClass.SomeNumber += 1;
        Console.WriteLine(person_1.name);
        Console.WriteLine(person_2.name);
        Console.WriteLine(person_1.someClass.SomeNumber);
        Console.WriteLine(person_2.someClass.SomeNumber);
    }
}

在这个例子中,输出将是

Person 1
Person 1 changed 
5
5

op的问题是,如果对象和字符串的实例都是引用类型,那么为什么它们在复制时的行为不同?在这个例子中,正确的答案应该是:因为字符串是不可变的。

Person person_2 = person_1; // at this point the properties of person_2 both point to the same memory location as those of person 1. this is because person_1 is copied by value to person_2, the references are the values being copied, not what they point to (no deep copy)
person_2.name += " changed"; // strings are immutable, so the first string is not changed, instead a new memory location is allocated, the characters are stored and a new reference to that location is stored in the second struct
person_2.someClass.SomeNumber += 1; // nothing here changes the reference of someClass, thus both structs reflect this new value