假装 .NET 字符串是值类型

Posted

技术标签:

【中文标题】假装 .NET 字符串是值类型【英文标题】:Pretending .NET strings are value type 【发布时间】:2010-12-12 03:30:50 【问题描述】:

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

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

【问题讨论】:

这里为什么要对值类型和引用类型大做文章呢?有些对象是可变的,有些是不可变的。可变/不可变概念适用于许多语言(即使是那些没有价值/引用区别的语言)——一旦理解了这一点,我认为它大部分就变成了学术上的“非问题”。之后,可以讨论 MUTABLE 值类型与 MUTABLE 引用类型(一旦定义了规则,这仍然完全是微不足道的)。 (实际上,我收回关于在 C# 中微不足道的最后一点...... arg。) 【参考方案1】:

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

方法调用中的字符串参数

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

困扰小辈的是他们习惯了对象是引用的事实,并且它们在改变传递参数的方法中被改变。要对字符串做同样的事情,他们需要使用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 与字符串性能的对比

编辑:根据 Konrad Rudolph 的评论,我添加了此部分。

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

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

那么,什么时候 是有效的?在附加了许多小字符串的情况下,即将数据序列化到文件中,例如,当您不需要更改“写入”到 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 和固定对象。

性能

当性能很重要时,您会发现字符串实际上不是不可变的并且using StringBuilder is not always the fastest approach。

我在这里使用的很多信息都非常详细 in this excellent article on strings,以及用于就地操作字符串(可变字符串)的“操作方法”。

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

【讨论】:

串联性能的转折点震惊我。我会怀疑它要低两个数量级:大约 4 个,而不是 400 个串联。看到这些分析结果,我不禁想知道StringBuilder 怎么会变得如此低效。 震惊了很多人,甚至很多人都不相信。转折点可能会有很大差异。大字符串使用concat+,转折点只能在几个1000s之后,使用concat非常小的字符串,转折点可以在50左右。与替换、正则表达式或Insert 混合使用会使两者的性能差不多。代码结构、使用 const 或动态字符串等可能会产生很大的影响。【参考方案2】:

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

更多阅读:C# .NET String object is really by reference? on SOString.Intern Method on MSDNstring (C# Reference) on MSDN

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

【讨论】:

优化内存使用,如果它是纯值类型,这将是巨大的。?恐怕情况恰恰相反。字符串没有优化,当然也不是为了内存使用。 String.Intern 仅在字符串被认为是常量时偶尔使用(不是表示不可变,而是表示它没有被分配另一个值)。在所有其他情况下,字符串在重新分配或更改时会重复地被内存复制,这对性能不利。 @Abel:你是对的。我仍然会在这里留下我的答案,以便其他人可以阅读您的有用评论。除非你当然选择回答:) 两者都可以,只是决定详细说明一下:)【参考方案3】:

在所有常见情况下,不可变类的行为类似于值类型,您可以进行大量编程而无需过多关注差异。

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

【讨论】:

实际上,C#(或更好的:.NET)使用 "copy-on-write" 处理字符串。这意味着如果您将它作为参数传递,则确实传递了一个引用,但是一旦您尝试更改它,就会创建一个本地副本,并为其分配新值。传递的字符串参数保持不变。如果您希望参数也更改,请在字符串上使用 ref【参考方案4】:

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

【讨论】:

以上是关于假装 .NET 字符串是值类型的主要内容,如果未能解决你的问题,请参考以下文章

.NET基础拾遗字符串集合和流2

Swift中数组和字典都是值类型

在.Net中如何判断一个属性是值类型还是引用类型

2021-05-15 C#.NET面试题 C#中什么是值类型与引用类型?

字符串类型是存储在堆上还是栈上?

值类型和引用类型