我是C#编程的初学者。我现在正在研究strings
,structs
,value types
和reference types
。正如在here和here中接受的答案一样,strings
是指针存储在堆栈上而其实际内容存储在堆上的引用类型。另外,正如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应该指向堆上的相同空间区域,不应该是它们吗?
答案 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
是引用类型,则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
中存储的值的副本(对{{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.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
然后输入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