【问题标题】:Why reference types inside structs behave like value types?为什么结构中的引用类型表现得像值类型?
【发布时间】:2017-03-26 16:07:19
【问题描述】:

我是 C# 编程的初学者。我现在正在学习stringsstructsvalue typesreference types。正如herehere 中公认的答案,strings 是引用类型,它们的指针存储在堆栈上,而它们的实际内容存储在堆上。此外,如here 中所述,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);
    }
}

以上代码sn-p输出

Person 1
Person 2

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

【问题讨论】:

  • 我……也很困惑……您实际上是在设置名称的值,对吗?那么结果有什么问题呢?
  • @IanH OP 混淆了改变引用(使其完全引用不同的对象)和改变引用引用的对象。字符串是一个不好的例子,因为它是不可变的。
  • @RaymondChen 啊,我想我知道了,谢谢:)

标签: c# string value-type reference-type


【解决方案1】:

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

不不不。首先,停止考虑堆栈和堆。在 C# 中,这几乎总是错误的思考方式。 C# 为您管理存储生命周期。

其次,虽然引用可以实现为指针,但引用在逻辑上不是指针。参考是参考。 C# 既有引用又有指针。不要把它们混在一起。 C# 中没有指向字符串的指针。有对字符串的引用。

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

现在让我们来回答您的实际问题。

    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”。对字符串的引用是被复制的值。

如果不清楚,请尝试为变量绘制框并为引用绘制箭头;当结构被复制时,arrow 的副本被制作,而不是 box 的副本。

【讨论】:

    【解决方案2】:

    理解这一点的最好方法是充分理解变量是什么;简单地说,变量是保存的占位符。

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

    当您执行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。这意味着我们正在存储一个 new 引用,该引用指向内存中某处的新 string。请注意,person_2.name 最初持有存储在person_1.name 中的值的副本,因此无论您对person_2.name 执行的任何操作都不会影响存储在person_1.name 中的任何值,因为您只是改变……没错,一个副本。这就是为什么您的代码会以这种方式运行的原因。

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

    【讨论】:

      【解决方案3】:

      每个结构实例都有自己的字段。 person_1.name 是来自 person_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 然后在之前是对象引用的副本的位置上交结构值的副本。

      【讨论】:

      • 也许值得一提的是,与引用类型不同,结构是通过赋值(以及在其他类似情况下)复制的。
      • 我很困惑。如果Person 是一个类,那么行为会如何?我希望输出是“Person 2 Person 2”。
      • @InBetween 确实如此。我不明白为什么这个答案被投票了 4 次,然后 :) 我误读了他的代码,并假设他对每个变量都使用了 new。我真的不喜欢用户名旁边的代表数字导致盲目的赞成。
      【解决方案4】:

      认为字符串是字符数组。下面的代码与您的类似,但带有数组。

      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
      }
      

      【讨论】:

        【解决方案5】:

        我要强调一个事实,通过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);
            }
        }
        

        现在是sn-p的输出:

        Person 2
        Person 2 
        

        要能够更改 person_1.name 最初在struct 中发布的方式,您需要使用 ref https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/ref

        【讨论】:

          【解决方案6】:

          我认为这里的很多答案都错过了原始问题的重点,主要是因为示例不是很好。一些答案指出字符串的不变性是导致这种行为的正确原因,但在操作问题上确实不会产生影响。

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

          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
          

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

          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
          

          我希望这能为仍然对此感到疑惑的人们消除一些困惑。

          【讨论】:

          • person_2 和 person_1 具有相同的引用,这就是为什么您具有相同的值!!!
          • @langme 不,他们没有,结构是一种值类型。人person_2 = person_1; -> 该行复制了 person_1,它们没有“相同的引用”
          • @langme,如果 Person 是一个类而不是一个结构,你会是正确的,但是上面代码的输出将是: Person 1 changed Person 1 changed 5 5 我的回答的重点是解释差异
          猜你喜欢
          • 2011-12-06
          • 2011-01-25
          • 2023-01-20
          • 1970-01-01
          • 1970-01-01
          • 2013-06-01
          • 2017-11-28
          • 1970-01-01
          • 2015-07-18
          相关资源
          最近更新 更多