为什么结构中的引用类型的行为类似于值类型?

时间:2016-11-12 15:59:18

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

我是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);
    }
}

以上代码段输出

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

5 个答案:

答案 0 :(得分:12)

  

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

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

其次,尽管引用可以实现为指针,但引用不是逻辑指针。参考文献是参考文献。 C#有引用和指针。不要混淆它们。在C#中没有指向字符串的指针。有对字符串的引用。

第三,对字符串的引用可以存储在堆栈中,但也可以存储在堆上。当你有一个对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”。对字符串的引用是复制的值。

如果不清楚,请尝试绘制变量和箭头框以供参考;复制结构时,会生成箭头的副本,而不是的副本。

答案 1 :(得分:7)

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

那究竟是什么价值呢?在引用类型中,存储在变量中的值是给定对象的引用(可以说是地址)。在值类型中,值是对象本身

执行AnyType y = x;时,实际发生的是{em>存储在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中存储的值的副本(对{{1}的引用})。

"Person 1"

现在我们只是重新分配变量person_2.name = "Person 2"; 。这意味着我们存储了一个 new 引用,该引用指向内存中的某个新person_2.name。请注意,string最初持有<{1}}中存储的值的副本,因此无论您对person_2.name所做的任何操作都不会影响person_1.name中存储的任何值{1}}因为你只是在改变......是的,一个副本。这就是为什么你的代码的行为方式。

作为练习,如果person_2.name是引用类型,请尝试以类似的方式推断代码的行为。

答案 2 :(得分:6)

每个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值的副本,它之前是对象引用的副本。

答案 3 :(得分:3)

认为字符串是字符数组。下面的代码与您的代码相似,但是使用数组。

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
}

答案 4 :(得分:0)

我要强调一个事实,就是通过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")的操作,而“名称”仅持有一个,该值表示内存中的地址

现在,如果我重写您的代码:

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://docs.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/ref