7

在 .NET 中,字符串是不可变的并且是引用类型变量。这对于新的 .NET 开发人员来说常常是一个惊喜,他们可能会因为它们的行为而将它们误认为是值类型对象。但是,除了使用StringBuilder长连接 esp 的做法。在循环中,在实践中是否有任何理由需要知道这种区别?

通过了解 .NET 字符串的值引用区别与仅仅假装/误解它们是值类型,可以帮助或避免哪些实际场景?

4

4 回答 4

17

s的设计string是故意让你作为程序员不必担心太多。在许多情况下,这意味着您可以只分配、移动、复制、更改字符串,而无需过多考虑如果对您的字符串的另一个引用存在并且将同时更改(就像对象引用一样)可能产生的复杂后果。

方法调用中的字符串参数

(编辑:本节稍后添加)
当字符串传递给方法时,它们是通过引用传递的。当它们只在方法体中被读取时,没有什么特别的事情发生。但是当它们被更改时,会创建一个副本,并在方法的其余部分使用临时变量。此过程称为写时复制

让后辈们感到困扰的是,他们已经习惯了对象是引用的事实,并且它们在改变传递参数的方法中被改变。要对字符串做同样的事情,他们需要使用ref关键字。这实际上允许更改字符串引用并将其返回给调用函数。如果不这样做,则方法主体无法更改字符串:

void ChangeBad(string s)      { s = "hello world"; }
void ChangeGood(ref string s) { s = "hello world"; }

// in calling method:
string s1 = "hi";
ChangeBad(s1);       // s1 remains "hi" on return, this is often confusing
ChangeGood(ref s1);  // s1 changes to "hello world" on return

在 StringBuilder 上

这种区别很重要,但初学者程序员通常最好不要对此了解太多。在进行大量字符串“构建”时使用StringBuilder它是好的,但通常情况下,您的应用程序将有更多的鱼要炸,而性能的小幅提升StringBuilder可以忽略不计。警惕那些告诉你所有字符串操作都应该使用 StringBuilder 完成的程序员。

作为一个非常粗略的经验法则: StringBuilder 有一些创建成本,但附加很便宜。字符串的创建成本较低,但连接相对昂贵。转折点大约是 400-500 个连接,具体取决于大小:在那之后,StringBuilder 变得更有效率。

有关 StringBuilder 与字符串性能的更多信息

编辑:根据康拉德鲁道夫的评论,我添加了这一部分。

如果前面的经验法则让您感到疑惑,请考虑以下稍微更详细的解释:

  • 带有许多小字符串追加的 StringBuilder 很快就超过了字符串连接(30、50 个追加),但在 2µs 上,即使 100% 的性能提升通常也可以忽略不计(对于一些罕见的情况是安全的);
  • 带有一些大字符串附加(80 个字符或更大的字符串)的 StringBuilder 仅在数千次(有时是几十万次)迭代后才超过字符串连接,并且差异通常只有几个百分点;
  • 混合字符串操作(替换、插入、子字符串、正则表达式等)通常使使用 StringBuilder 或字符串连接相等;
  • 常量的字符串连接可以被编译器、CLR 或 JIT 优化掉,它不能用于 StringBuilder;
  • 代码经常混合连接、、、+和其他字符串操作StringBuilder.Append,在这种情况下使用 StringBuilder 几乎没有效果。String.FormatToString

那么,什么时候有效呢?在附加了许多小字符串的情况下,例如,将数据序列化到文件中,并且一旦“写入”到 StringBuilder 就不需要更改“写入”数据。在许多方法需要附加一些东西的情况下,因为 StringBuilder 是一种引用类型,并且字符串在更改时会被复制。

在实习字符串上

当他们尝试进行参考比较并发现在看似相同的情况下有时结果为真,有时为假时,就会出现问题——不仅是初级程序员。发生了什么?当字符串被编译器暂存并添加到全局静态字符串暂存池时,两个字符串之间的比较可以指向相同的内存地址。当(参考!)比较两个相等的字符串时,一个被保留,一个没有,将产生错误。使用=比较,或者在处理字符串时Equals不要玩弄。ReferenceEquals

在 String.Empty 上

在同一个联盟中,使用时有时会出现一种奇怪的行为String.Empty:静态String.Empty总是被实习的,但具有赋值的变量不是。但是,默认情况下,编译器将分配String.Empty并指向相同的内存地址。结果:一个可变字符串变量,当与 比较时ReferenceEquals,返回 true,而您可能期望 false。

// emptiness is treated differently:
string empty1 = String.Empty;
string empty2 = "";
string nonEmpty1 = "something";
string nonEmpty2 = "something";

// yields false (debug) true (release)
bool compareNonEmpty = object.ReferenceEquals(nonEmpty1, nonEmpty2);

// yields true (debug) false (release, depends on .NET version and how it's assigned)
bool compareEmpty = object.ReferenceEquals(empty1, empty2);

深入

您基本上询问了外行会发生什么情况。我认为我的观点归结为避免object.ReferenceEquals,因为它在与字符串一起使用时不可信。原因是当字符串在代码中是常量时使用字符串驻留,但并非总是如此。您不能依赖此行为。虽然String.Empty并且""总是被实习,但编译器并不认为该值是可变的。不同的优化选项(调试与发布等)将产生不同的结果。

什么时候需要ReferenceEquals?对于对象,它是有意义的,但对于字符串,它没有。教任何使用字符串的人避免使用它,除非他们也理解unsafe和固定对象。

表现

当性能很重要时,您会发现字符串实际上不是不可变的,并且使用StringBuilder并不总是最快的方法

这篇关于字符串的优秀文章详细介绍了我在此处使用的许多信息,以及就地操作字符串(可变字符串)的“操作方法”。

更新:添加了代码示例
更新:添加了“深入”部分(希望有人觉得这很有用;)
更新:添加了一些链接,添加了关于字符串参数的部分
更新:添加了关于何时从字符串切换到字符串生成器的估计
更新:添加了一个额外的部分在 Konrad Rudolph 发表评论之后,关于 StringBuilder 与 String 的性能

于 2009-11-02T00:32:01.350 回答
3

对于大多数代码来说真正重要的唯一区别是null可以分配给字符串变量。

于 2009-11-02T00:20:55.917 回答
3

不可变类在所有常见情况下都像值类型一样,您可以在不关心差异的情况下进行大量编程。

只有当您深入挖掘并关心性能时,您才能真正用于区分。例如,要知道虽然将字符串作为参数传递给方法的行为就像创建了字符串的副本,但实际上并没有发生复制。对于习惯于字符串实际上是值类型的语言(如 VB6?)的人来说,这可能是一个惊喜,并且将大量字符串作为参数传递对性能不利。

于 2009-11-02T00:33:43.983 回答
1

弦乐是一种特殊的品种。它们是引用类型,但被大多数编码人员用作值类型。通过使其不可变并使用实习池,它优化了内存使用,如果它是纯值类型,这将是巨大的。

更多阅读材料:
C# .NET String 对象真的是通过引用吗?在 MSDN 上的 SO
String.Intern
字符串上的方法(C# 参考)

更新:
请参阅abel对这篇文章的评论。它纠正了我的误导性陈述。

于 2009-11-02T00:17:35.763 回答