为什么结构中的引用类型表现得像值类型?
Why reference types inside structs behave like value types?
我是 C# 编程的初学者。我现在正在学习 strings
、structs
、value types
和 reference types
。作为 here and in here, strings
are reference types that have pointers stored on stack while their actual contents stored on heap. Also, as claimed in here 中接受的答案,structs
是值类型。现在我试着用 structs
和 strings
来练习一个小例子:
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 应该指向堆上相同的 space 区域,不是吗?
每个结构实例都有自己的字段。 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.S
和c2.S
指的是同一个变量。如果你把它设为 struct
那么它们就会变成不同的变量(就像在你的代码中一样)。 c2 = c1
然后在之前是对象引用的副本的位置上交结构值的副本。
理解这一点的最好方法是充分理解什么是变量;简而言之,变量是占位符,其中包含 values.
那么这个值到底是多少呢?在引用类型中,存储在变量中的值是给定对象的 reference(可以说是地址)。在值类型中,值是对象本身.
当您执行 AnyType y = x;
时,真正发生的是 复制 存储在 x
中的值,然后存储在 y
.
因此,如果 x
是引用类型,则 x
和 y
都将指向同一个对象,因为它们都持有相同引用的相同副本。如果 x
是值类型,那么 x
和 y
都将包含两个相同但 不同的 对象。
一旦你理解了这一点,就应该开始理解为什么你的代码会这样运行了。让我们一步一步地研究它:
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
中的值的 copy(对 "Person 1"
).
person_2.name = "Person 2";
现在我们只是重新分配变量 person_2.name
。这意味着我们正在存储一个 new 引用,它指向内存中某处的新 string
。请注意,person_2.name
最初持有存储在 person_1.name
中的值的 copy,因此无论您对 person_2.name
做什么,都不会影响任何值存储在 person_1.name
中,因为您只是在更改...没错,copy。这就是为什么您的代码会按照它的方式运行。
作为练习,尝试以类似的方式推理出如果 Person
是引用类型,您的代码将如何运行。
strings are reference types that have pointers stored on stack while their actual contents stored on heap
不不不。首先,停止考虑堆栈和堆。在 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# 中有一种方法可以做到这一点,但事实并非如此。)它的意思是 "copy the contents of person1 to person2"。对字符串的引用是复制的值。
如果不清楚,请尝试为变量绘制方框,为引用绘制箭头;复制结构时,会生成 arrow 的副本,而不是 box.
的副本
将字符串视为字符数组。下面的代码与您的类似,但使用数组。
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" 只持有一个 value 代表内存中的地址
现在如果我重写你的代码:
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
我认为这里的很多答案都没有抓住原问题的重点,主要是因为这个例子不是很好。一些答案指出字符串的不变性是导致此行为的正确原因,但在操作问题上确实不会产生任何影响。
一个更好的例子可以说明我在我的开发团队中看到的关于字符串的一些困惑:
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
我希望这能为仍然对此感到疑惑的人们消除一些困惑。
我是 C# 编程的初学者。我现在正在学习 strings
、structs
、value types
和 reference types
。作为 here and in here, strings
are reference types that have pointers stored on stack while their actual contents stored on heap. Also, as claimed in here 中接受的答案,structs
是值类型。现在我试着用 structs
和 strings
来练习一个小例子:
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 应该指向堆上相同的 space 区域,不是吗?
每个结构实例都有自己的字段。 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.S
和c2.S
指的是同一个变量。如果你把它设为 struct
那么它们就会变成不同的变量(就像在你的代码中一样)。 c2 = c1
然后在之前是对象引用的副本的位置上交结构值的副本。
理解这一点的最好方法是充分理解什么是变量;简而言之,变量是占位符,其中包含 values.
那么这个值到底是多少呢?在引用类型中,存储在变量中的值是给定对象的 reference(可以说是地址)。在值类型中,值是对象本身.
当您执行 AnyType y = x;
时,真正发生的是 复制 存储在 x
中的值,然后存储在 y
.
因此,如果 x
是引用类型,则 x
和 y
都将指向同一个对象,因为它们都持有相同引用的相同副本。如果 x
是值类型,那么 x
和 y
都将包含两个相同但 不同的 对象。
一旦你理解了这一点,就应该开始理解为什么你的代码会这样运行了。让我们一步一步地研究它:
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
中的值的 copy(对 "Person 1"
).
person_2.name = "Person 2";
现在我们只是重新分配变量 person_2.name
。这意味着我们正在存储一个 new 引用,它指向内存中某处的新 string
。请注意,person_2.name
最初持有存储在 person_1.name
中的值的 copy,因此无论您对 person_2.name
做什么,都不会影响任何值存储在 person_1.name
中,因为您只是在更改...没错,copy。这就是为什么您的代码会按照它的方式运行。
作为练习,尝试以类似的方式推理出如果 Person
是引用类型,您的代码将如何运行。
strings are reference types that have pointers stored on stack while their actual contents stored on heap
不不不。首先,停止考虑堆栈和堆。在 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# 中有一种方法可以做到这一点,但事实并非如此。)它的意思是 "copy the contents of person1 to person2"。对字符串的引用是复制的值。
如果不清楚,请尝试为变量绘制方框,为引用绘制箭头;复制结构时,会生成 arrow 的副本,而不是 box.
的副本将字符串视为字符数组。下面的代码与您的类似,但使用数组。
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" 只持有一个 value 代表内存中的地址
现在如果我重写你的代码:
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
我认为这里的很多答案都没有抓住原问题的重点,主要是因为这个例子不是很好。一些答案指出字符串的不变性是导致此行为的正确原因,但在操作问题上确实不会产生任何影响。
一个更好的例子可以说明我在我的开发团队中看到的关于字符串的一些困惑:
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
我希望这能为仍然对此感到疑惑的人们消除一些困惑。