如何在C#中正确处理字符串?

3

我知道在C#中有一个关于字符串的规则,它说:

当我们创建一个string类型的文本字符串时,我们永远不能改变它的值!当为一个字符串变量赋予不同的值时,第一个字符串将保留在内存中,而变量(作为引用类型)只是获得新字符串的地址。

因此,像这样做:

string a = "aaa";
a = a.Trim(); // Creates a new string

不推荐使用。 但是如果我需要根据用户的偏好对字符串执行一些操作,怎么办呢:

string a = "aaa";
if (doTrim)
   a = a.Trim();
if (doSubstring)
   a = a.Substring(...);

etc...

如何在不创建新字符串的情况下执行操作? 我考虑通过引用将字符串发送到函数中,方法如下:

void DoTrim(ref string value)
{
  value = value.Trim(); // also creates new string
}

但是这也会创建一个新字符串… 有没有一种方法可以在不浪费内存的情况下完成该操作?


你不能更改字符串而不创建新实例,因为字符串是不可变的。 - user247702
3
你可以使用 StringBuilder 来替代。 - Fabian Bigler
9
“这样做并不被推荐。” - 实际上,这是完全被推荐的。事实上,这是实现你想要做的事情的唯一方法。 - Ant P
2
问题中绝对没有任何暗示字符串正在被迭代地附加。这里不需要使用 StringBuilder。 - Ant P
你可以创建一个函数,根据你支持的许多不同过程来重构你的字符串。正如其他人所提到的,你最好使用StringBuilder来完成这个任务。然而,最终你应该问问自己,这样做的工作量是否值得节省额外的字符串开销。我理解你的想法。我的一个朋友和我写了一个XNA游戏,在屏幕上每一帧都会发布调试字符串。垃圾回收器疯了。然而,一般情况下,你很少遇到这种情况,不需要进行这种低级别的操作。 - David Peterson
显示剩余2条评论
5个回答

11

你说的没错,你执行的操作创建了新的字符串,而不是改变单个字符串。

但是一般情况下这并不会造成问题或需要避免。

如果你的字符串有成千上万的字符,那么每次复制所有字符以删除前导空格或在末尾添加几个字符(特别是在循环中重复执行)可能会成为问题。

如果你的字符串不是很大,并且对字符串执行的操作不是很多(比如数千次),那么基本上不会出现问题。

现在有一些上下文环境很少见,确实在字符串操作时会出现问题。其中最常见的是将一堆字符串合并在一起,因为这样做意味着为每个新添加的数据复制之前添加的所有数据。如果你面临这种情况,请考虑使用类似于 StringBuilder 或单个调用 string.Concat(接受一系列字符串以拼接的重载)来执行此操作。

其他情况包括例如处理DNA链的程序。它们通常会将数百万个字符的字符串拆分成数千个长度为数千个字符的子字符串。因此,使用标准的C#字符串操作会导致很多不必要的复制。编写这种程序的人最终会创建可以表示另一个字符串子串的对象,而不必复制数据,只需使用偏移量引用现有字符串的基础数据源。


Stringbuilder还有一个很好的优势,可以产生“更可读”的(一眼就能看懂)代码。 - Gusdor
@Gusdor 这是主观的,也高度依赖于情况。就我个人而言,我几乎从不使用 StringBuilder。每当我想要连接许多字符串时,我使用 Concat。操作不可变对象通常更容易推理; 可变对象通常更容易做出卑鄙和复杂的事情。 - Servy
@Gusdor - 这是有争议的。就我个人而言,我发现格式良好的字符串连接比一堆StringBuilder调用更清晰。 - Ant P
@Gusdor 既然所有这些字符串在编译时都是已知的,我会将它们写成一个编译时字面量。如果它们是一堆静态定义的字符串变量,我会使用 + 运算符。如果它是未知数量的字符串(StringBuilder 设计解决的唯一情况),我会使用 单个 调用 string.Concat,传入要连接的值序列。 - Servy
感谢您提供的精彩答案 :) - Liran Friedman
显示剩余4条评论

1

我在这里稍微冒了一点风险,所以我先声明,在大多数情况下,Servy的答案都是正确的。然而,如果您确实需要更低级别的访问和较少的字符串分配,您可以考虑创建一个字符缓冲区(例如简单数组),它足够大,可以容纳您处理过的字符串,并允许您直接操作字符。然而,这种方法有一些显著的缺点。其中包括,您可能需要编写自己的Substring()和Trim()修改器,并且在许多情况下,您的缓冲区可能比输入字符串更大,以适应意外的字符串大小。一旦您完成对缓冲区的操作,您可以将字符数组封装为一个字符串。由于所有操作都在单个缓冲区上执行,因此您应该节省了很多内存分配。

我会认真考虑以上内容是否值得麻烦,但如果您确实需要性能,这是我能想到的最好的解决方案。


虽然可能性不大,但了解那些极端情况总是一个好主意。在某些情况下,这种做法确实有助于提高实际性能,而不仅仅是理论上的。 - Bobson
@Bobson 是的,这正是我需要解决的问题。有时候确实需要性能,但我认为这是罕见的情况。如果 OP 真的需要性能,这个解决方案可能会有所帮助。 - David Peterson

0

你为什么觉得创建新字符串会让你感到不舒服?字符串API被设计成这样是有原因的。例如,不可变对象是线程安全的(它们还允许更函数式的编程风格)。

如果你用stringbuilders替换简单字符串代码,在多线程场景中可能会更容易出错(例如在Web应用程序中很常见)。

StringBuilders用于连接字符串,插入字符,删除字符等。但是,它们需要定期重新分配和复制其内部字符数组。

当你谈论内存消耗时,你已经开始微调你的代码。不要这样做。

顺便说一句:看看LINQ API。每个操作都是做什么的?老鼠 - 它创建了一个新的枚举器!像foos.Where(bar).Select(baz).FirstOrDefault()这样的查询肯定可以通过创建单个枚举器对象并在枚举时修改它应用的条件来进行内存优化。</irony>


0
如何在不创建新字符串的情况下完成操作?
只有当你处理大字符串或在短时间内进行多个字符串操作时,才需要担心这个问题。
即使是这样,由于创建更多引用而导致的性能损失也是微不足道的。垃圾回收器必须收集所有未使用的字符串变量,但这只在你进行许多字符串操作时才真正重要。
因此,应该更加注重代码的可读性,而不是一开始就试图优化其性能。

如果你真的必须保持相同的字符串引用,你可以简单地使用StringBuilder


-1

这将取决于您的确切用例,但您可能希望探索使用StringBuilder类,您可以使用它来构建和修改字符串。


1
@DavidCrowell 但是它并不适用于OP在这里执行的操作。(以比本地字符串更有效的方式。)通常情况下,它不会更高效地从字符串开头修剪字符,也不会从另一个字符串创建子字符串而不复制该数据。虽然使用StringBuilder可以做到所有这些事情,但它并没有被优化来执行它们,因此实际上它不太可能优于仅使用本地字符串操作。 - Servy
我认为编译器甚至会在认为更优时将 StringBuilder 优化为字符串连接。 - user247702
1
@Stijn 不会这样做。它将采取静态数量的字符串连接,即 a+b+c+d 并将其转换为 单个 调用 string.Concat 而不是 3 个不同的调用。因此,结果为 string.Concat(a,b,c,d) 而不是 string.Concat(a,String.Concat(b,string.Concat(c,d)))。当然,这意味着不创建中间字符串,而不是创建 2 个。 - Servy
@Servy 我可能记错了,也许是相反的情况。 - user247702
@Stijn 它也永远不会反过来做。同时请参见我上一条评论的修改。 - Servy
显示剩余2条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接