连接字符串时的新引用

时间:2016-05-29 18:49:31

标签: c# variables memory-management stack clr

几周前,我在求职面试中被问到一个C#问题。问题恰恰如此:

string a = "Hello, ";

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

我被问到了,&#34;为什么这对于串联字符串来说是一个糟糕的方法?&#34;。我的回答是某种&#34;可读性,应该选择追加&#34;等

但显然,根据采访我的那个人的情况并非如此。因此,根据他的说法,每次我们连接一个字符串时,由于CLR的结构,在内存中创建了一个新的引用。因此,在下面的代码的末尾,我们将有99999999的字符串变量&#34; a&#34;在记忆中。

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

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

3 个答案:

答案 0 :(得分:0)

.NET区分ref和value类型。 string是ref类型。它在堆上分配,没有异常。它的寿命由GC控制。

  

所以,在下面的代码的末尾,我们将在内存中有99999999的字符串变量“a”。

已分配

99999999。当然,其中一些可能已经过GC了。

  

根据需要修改它们的值,并在完成范围的执行时将其处理

String不是基元或值类型。这些内容在其他内容中分配“内联”,例如堆栈,数组或内部堆对象。它们也可以装箱并成为真正的堆对象。这些都不适用于此。

此代码的问题不是分配,而是二次运行时复杂性。我不认为这个循环会在实践中完成。

答案 1 :(得分:0)

首先要记住这两个事实:

  • string是一个不可变类型(现有实例永远不会被修改)
  • string是一种引用类型(string表达式的“值”是引用到实例所在的位置)

因此,声明如下:

a += "world!";

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

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

所以我猜你的面试官绝对是对的。你的问题的循环将在内存中创建一堆(大多数很长!)字符串(在中,因为你想要技术)。

更简单的方法可能是:

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

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

(当然不要像ToArray()那样使用string.Concat(Enumerable.Repeat("world!", 999...).ToArray())或类似内容!)

答案 2 :(得分:-1)

始终在堆中创建

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

但是说在循环之后你将在内存中有N个对象并不完全正确。

的每次评估
a += "world!";
你创建一个新字符串的

语句。先前创建的字符串会发生什么变得更复杂。垃圾收集器现在拥有,因为在你的代码中没有其它的引用,并且会在某些时候释放它,你不知道什么时候会发生。

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

修改

根据要求,这里有与堆栈/堆相关的说明。堆栈中的值类型不是始终。当你在函数体内声明它们时,它们在堆栈中:

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

但是当它们是其他对象的一部分时进入堆,就像整数是类的属性一样(因为整个类实例在堆中)。