连接字符串时的新引用

New Reference When Concatenating A String

几周前,我在一次求职面试中被问到一个 C# 问题。问题是这样的:

string a = "Hello, ";

for(int i = 0; i < 99999999; i++)
{
    a += "world!";
}

有人问我,"why this is a bad method for concatenated string?"。我的回应是某种 "readability, append should be chosen" 等等

但显然,根据面试我的人的说法,情况并非如此。所以,根据他的说法,每次我们连接一个字符串时,由于 CLR 的结构,都会在内存中创建一个新的引用。因此,在以下代码的末尾,内存中将有 99999999 个字符串变量 "a"。

我想,只要给对象赋值,对象就会在堆栈中创建一次(我不是在谈论堆)。我所知道的方法是,内存分配在堆栈中为每个原​​始数据类型完成一次,它们的值根据需要修改并在范围执行完成时处理。那是错的吗?或者,变量 "a" 的新引用是否实际上在每次连接时都在堆栈中创建?

有人可以解释一下堆栈的工作原理吗?非常感谢。

Reference types(即 classes 和字符串)总是在堆中创建。值类型(例如结构)在堆栈中创建,并在函数结束执行时丢失。

然而,在循环之后您将在内存中拥有 N 个对象的说法并不完全正确。在

的每一次评价中
a += "world!";

声明你确实创建了一个新字符串。先前创建的字符串会发生什么更复杂。垃圾收集器现在拥有,因为您的代码中没有其他引用它,并且会在某个时候释放它,您不确切知道什么时候会发生。

最后,这段代码的最终问题是你认为你正在修改一个对象,但字符串是不可变的,这意味着一旦创建你就不能真正改变它们的值。您只能创建新的,这就是 += 运算符正在做的事情。使用可变的 StringBuilder 会更有效率。

编辑

根据要求,这里是堆栈/堆相关的说明。值类型在堆栈中并非 always。当您在函数体内声明它们时,它们在堆栈中:

void method()
{
    int a = 1; // goes in the stack
}

但是当它们是其他对象的一部分时进入堆,比如当整数是 class 的 属性 时(因为整个 class 实例都在堆中).

.NET 区分 ref 和 value 类型。 string 是一个引用类型。它无一例外地分配在堆上。它的生命周期由 GC 控制。

So, in the end of the following code, we would have 99999999 of string variable "a" in memory.

99999999 已分配。当然,其中一些可能已经被 GC 处理过。

their values are modified as needed and disposed when the execution of a scope is finished

字符串不是原始类型或值类型。这些分配 "inline" 在其他东西内部,例如堆栈、数组或内部堆对象。它们也可以装箱并成为真正的堆对象。 None 其中适用于此处。

此代码的问题不在于分配,而在于二次运行时复杂度。我认为这个循环在实践中永远不会结束。

首先记住这两个事实:

  • string 是不可变类型(现有实例永远不会被修改)
  • string 是引用类型(string 表达式的 "value" 是对实例所在位置的 reference

因此,像这样的语句:

a += "world!";

的工作方式与 a = a + "world!"; 类似。它将首先跟随对 "old" a 的引用,并将旧字符串与字符串 "world!" 连接起来。这涉及将两个旧字符串的内容复制到新的内存位置。那就是“+”部分。然后它将 移动 a 的引用从指向旧位置到指向新位置(新连接的字符串)。那是语句的“=”赋值部分。

现在,旧的字符串实例没有对它的引用。所以在某些时候,垃圾收集器将删除它(并可能移动内存以避免 "holes")。

所以我猜你的求职面试官就在那里。你的问题的循环将在内存中创建一堆(大部分很长!)字符串(在 heap 中,因为你想要技术)。

更简单的方法可能是:

string a = "Hello, "
    + string.Concat(Enumerable.Repeat("world!", 999...));

这里我们使用string.Concat。该方法将知道它需要将一堆字符串连接成一个长字符串,并且它可以在内部使用某种可扩展缓冲区(例如 StringBuilder 甚至指针类型 char*)来制作确保它不会在内存中创建无数 "dead" 个对象实例。

(当然不要使用 ToArray() 或类似的东西,如 string.Concat(Enumerable.Repeat("world!", 999...).ToArray())!)