为什么字符串的行为类似于ValueType

在执行这段代码之后,我感到很困惑,其中字符串似乎表现得好像它们是值类型。 我想知道赋值运算符是否运行像字符串的相等运算符之类的值。

这是我为测试此行为而执行的一段代码。

using System; namespace RefTypeDelimma { class Program { static void Main(string[] args) { string a1, a2; a1 = "ABC"; a2 = a1; //This should assign a1 reference to a2 a2 = "XYZ"; //I expect this should change the a1 value to "XYZ" Console.WriteLine("a1:" + a1 + ", a2:" + a2);//Outputs a1:ABC, a2:XYZ //Expected: a1:XYZ, a2:XYZ (as string being a ref type) Proc(a2); //Altering values of ref types inside a procedure //should reflect in the variable thats being passed into Console.WriteLine("a1: " + a1 + ", a2: " + a2); //Outputs a1:ABC, a2:XYZ //Expected: a1:NEW_VAL, a2:NEW_VAL (as string being a ref type) } static void Proc(string Val) { Val = "NEW_VAL"; } } } 

在上面的代码中,如果我使用自定义类而不是字符串,我得到了预期的行为。 我怀疑这与字符串不变性有关吗?

欢迎专家对此提出意见。

  a2 = "XYZ"; 

这是语法糖,由编译器提供。 更准确地表达此陈述将是:

  a2 = CreateStringObjectFromLiteral("XYZ") 

这解释了a2如何简单地获取对新字符串对象的引用并回答您的问题。 实际代码经过高度优化,因为它非常常见。 在IL中有一个专用的操作码:

  IL_0000: ldstr "XYZ" 

字符串文字被收集到程序集内的表中。 这允许JIT编译器非常有效地实现赋值语句:

  00000004 mov esi,dword ptr ds:[02A02088h] 

单机代码指令,无法击败。 更重要的是:一个非常值得注意的结果是字符串对象不在堆上。 垃圾收集器不会打扰它,因为它识别字符串引用的地址不在堆中。 所以你甚至不需要支付收取费用。 不能打败那个。

另请注意,此方案可轻松实现字符串实习。 编译器只为相同的文字生成相同的LDSTR参数。

你没有改变任何关于a1指向的对象,而是改变a1指向的对象。

a =新人(); b = a; b = new Person(); http://sofzh.miximages.com/c%23/Valuevs.Reference3.png

您的示例用字符串文字替换“new Person {…}”,但原理是相同的。

当您更改对象的属性时,会出现差异。 更改值类型的属性,它不会反映在原始值中。

a =新人(); b = a; b.Name = …; http://sofzh.miximages.com/c%23/Valuevs.Reference1.png

更改引用类型的属性,它将反映在原始类型中。

a =新人(); b = a; b.Name = …; http://sofzh.miximages.com/c%23/Valuevs.Reference2.png

ps对于图像的大小很抱歉,它们只是来自我躺在的东西。 您可以在http://dev.morethannothing.co.uk/valuevsreference/上查看完整集,其中包含值类型,引用类型以及按值和引用传递值类型,以及按值和引用传递引用类型。

每当你看到

 variableName = someValue; 

这正在改变变量的值 – 它不会改变变量值所引用的对象的内容。

string的这种行为与其他引用类型完全一致,与immutability无关。 例如:

 StringBuilder b1 = new StringBuilder("first"); StringBuilder b2 = b1; b2 = new StringBuilder("second"); 

最后一行不会改变b1任何内容 – 它不会改变它所引用的对象,也不会改变它引用的对象的内容 。 它只是让b2引用一个新的StringBuilder

这里唯一的“惊喜”是字符串在文字forms的语言中有特殊的支持。 虽然有一些重要的细节,例如字符串实习(这样在同一个程序集中多个位置出现的相同字符串常量将始终产生对同一对象的引用),但这不会影响赋值运算符的含义。

他们没有。 您更改了a2的指针,而不是它指向的对象。
当您使用类并获得预期的行为时,您必须设置对象的属性 ,而不是它的引用。

任何其他类都将表现相同:

 Foo a = new Foo(1); Foo b = a; //a, b point to the same object b.Value = 4; // change property Assert.Equals(a.Value, 4); //true - changed for a b = new Foo(600); // new reference for b Assert.Equals(a.Value, 4); //true Assert.Equals(b.Value, 600); //true 

如果我没记错,一旦Erik Lippert在SO上写道,选择这种行为是为了让multithreading更容易,更安全。 这样,当您在a1中存储字符串时,您知道只有您可以更改它。 例如,它无法从其他线程更改。

第一个原因是String是一个不可变类。

如果对象在创建后无法修改其值,则该对象有资格称为不可变。 例如,似乎修改String的方法实际上返回包含修改的新String。 开发人员一直在代码中修改字符串。 对于开发人员而言,这似乎是可变的 – 但事实并非如此。 实际发生的是您的字符串变量/对象已更改为引用包含新字符串值结果的新字符串值。 出于这个原因,.NET具有System.Text.StringBuilder类。 如果您发现有必要大量修改类似字符串的对象的实际内容,例如在for或foreach循环中,请使用System.Text.StringBuilder类。


例如:

string x = 123;

如果你做x = x + abc它做的是它为123和abc分配新的内存位置。 然后添加两个字符串并将计算结果放在新的内存位置并将x指向它。

如果您使用System.Text.StringBuilder sb new System.Text.StringBuilder(123); sb.Append(abc); x sb.ToString();

stringbuilder是可变类。 它只是将字符串添加到相同的内存位置。 这样字符串操作更快。


字符串是String类型的对象,其值为text。 在内部,文本存储为Char对象只读集合 ,每个Char对象代表一个以UTF-16编码的Unicode字符。

实体有三种语义类型:

  1. 可变引用类型
  2. 可变值类型(*)
  3. 不可变类型

如果一个人制作了一个可变引用Y的副本X然后对该副本做了一些事情,那么对X执行的任何变异都会影响Y,反之亦然,因为X和Y都指向同一个对象。 相反,如果制作可变值类型实例YY的副本XX,则对XX的更改不会影响YY,反之亦然。

因为引用类型和值类型之间唯一的语义差异是它们在复制后被更改的行为,所以不可变引用类型在语义上与不可变值类型相同。 这并不意味着使用一个相对于另一个没有相当大的性能优势。

(*)含义值类型,可以在不完全替换的情况下进行部分更改。 例如,Point是可变的,因为人们可以改变它的一部分,而不必阅读和重写整个事物。 相比之下,Int32是不可变的,因为(至少从“安全”代码),如果不重写整个内容,就不可能对Int32进行任何更改。