5

我的问题是: C# 中的字符串连接安全吗?如果字符串连接导致意外错误,并且使用 StringBuilder 替换该字符串连接会导致这些错误消失,这可能表明什么?

背景:我正在开发一个小型命令行 C# 应用程序。它接受命令行参数,执行稍微复杂的 SQL 查询,并将大约 1300 行数据输出到格式化的 XML 文件中。

我的初始程序在调试模式下总是运行良好。但是,在发布模式下,它会得到大约第 750 个 SQL 结果,然后因错误而死。错误是无法读取某列数据,即使通过SqlDataReader 对象的Read() 方法刚刚返回true。

此问题已通过对代码中的所有操作使用 StringBuilder 来解决,以前存在“string1 + string2”。我不是在谈论 SQL 查询循环中的字符串连接,其中 StringBuilder 已经在使用中。我说的是代码前面两个或三个短字符串变量之间的简单连接。

我的印象是 C# 足够聪明,可以处理将几个字符串添加在一起的内存管理。我错了吗?或者这是否表明某种其他类型的代码问题?

4

8 回答 8

15

回答您的问题: C#(和一般的 .NET)中的字符串连接“安全的”,但是如您所描述的那样在紧密循环中执行它可能会导致严重的内存压力并对垃圾收集器造成压力。

我会大胆猜测您所说的错误与某种资源耗尽有关,但如果您能提供更多详细信息会有所帮助——例如,您是否收到异常?应用程序是否异常终止?

背景: .NET 字符串是不可变的,所以当你进行这样的连接时:

var stringList = new List<string> {"aaa", "bbb", "ccc", "ddd", //... };
string result = String.Empty;
foreach (var s in stringList)
{
    result = result + s;
}

这大致相当于以下内容:

string result = "";
result = "aaa"
string temp1 = result + "bbb";
result = temp1;
string temp2 = temp1 + "ccc";
result = temp2;
string temp3 = temp2 + "ddd";
result = temp3;
// ...
result = tempN + x;

这个例子的目的是强调每次循环都会导致分配一个新的临时字符串。

由于字符串是不可变的,因此运行时没有其他选择,只能在每次将另一个字符串添加到结果末尾时分配一个新字符串。

尽管result字符串会不断更新以指向最新和最大的中间结果,但您会产生大量这些未命名的临时字符串,这些字符串几乎立即可以进行垃圾回收。

在此连接结束时,您将在内存中存储以下字符串(为简单起见,假设垃圾收集器尚未运行)。

string a = "aaa";
string b = "bbb";
string c = "ccc";
// ...
string temp1 = "aaabbb";
string temp2 = "aaabbbccc";
string temp3 = "aaabbbcccddd";
string temp4 = "aaabbbcccdddeee";
string temp5 = "aaabbbcccdddeeefff";
string temp6 = "aaabbbcccdddeeefffggg";
// ...

尽管所有这些隐式临时变量几乎可以立即进行垃圾回收,但它们仍然必须被分配。当在一个紧密的循环中执行连接时,这会给垃圾收集器带来很大的压力,如果没有别的,会让你的代码运行得很慢。我亲眼目睹了这对性能的影响,并且随着您的连接字符串变得更大,它变得真正具有戏剧性。

推荐的方法是始终使用 aStringBuilder如果您正在执行多个字符串连接。 StringBuilder使用可变缓冲区来减少构建字符串所需的分配数量。

于 2009-04-22T21:32:09.737 回答
11

如果在循环中连接大量字符串,则字符串连接是安全的,但比使用 StringBuilder 更占用内存。在极端情况下,您可能会耗尽内存。

这几乎可以肯定是您的代码中的一个错误。

也许你正在包含大量的字符串。或者也许它是完全不同的东西。

我会在没有对根本原因的任何先入之见的情况下返回调试 - 如果您仍然遇到问题,请尝试将其减少到重现问题和发布代码所需的最低限度。

于 2009-04-22T20:29:07.687 回答
7

除了您正在做的事情之外,最好使用 XML APIs 而不是字符串或 StringBuilder 来完成我怀疑您看到的错误是由于字符串连接造成的。也许切换到 StringBuilder 只是掩盖了错误或优雅地解决了它,但我怀疑使用字符串真的是原因。

于 2009-04-22T20:29:45.423 回答
3

连接版本与字符串生成器版本需要多长时间?您与数据库的连接可能正在关闭。如果您要进行大量连接,我会使用 StringBuilder,因为它更有效。

于 2009-04-22T20:30:28.543 回答
1

一个原因可能是字符串在 .Net 中是不可变的,因此当您对一个字符串进行操作(例如连接)时,您实际上是在创建一个新字符串。

另一个可能的原因是字符串长度是 int,因此最大可能长度是 Int32.MaxValue 或 2,147,483,647。

无论哪种情况,对于此类操作,StringBuilder 都比“string1 + string2”更好。虽然,使用内置的 XML 功能会更好。

于 2009-04-22T20:35:50.650 回答
1

string.Concat(string[])是迄今为止连接字符串的最快方法。在循环中使用时,它会严重影响StringBuilder性能,尤其是StringBuilder在每次迭代中创建时。如果你用谷歌搜索“c# string format vs stringbuilder”或类似的东西,就会有很多参考资料。 http://www.codeproject.com/KB/cs/StringBuilder_vs_String.aspx让您了解时代。在这里 string.Join 赢得了连接测试,但我相信这是因为string.Concat(string, string)使用的是而不是采用数组的重载版本。如果您查看由不同方法生成的 MSIL 代码,您将看到幕后发生的事情。

于 2009-04-23T12:32:32.287 回答
0

这是我在黑暗中拍摄的...

.NET 中的字符串(不是字符串生成器)进入字符串实习生池。这基本上是由 CLR 管理的一个区域,用于共享字符串以提高性能。这里必须有一些限制,虽然我不知道那个限制是什么。我想你正在做的所有连接都是撞到字符串实习生池的天花板。所以 SQL 说是的,我对你有一个价值,但它不能把它放在任何地方,所以你会得到一个例外。

一个快速简单的测试是对你的程序集进行nGen测试,看看你是否仍然得到错误。在 nGen'ing 之后,您的应用程序将不再使用该池。

如果失败了,我会联系微软来尝试获取一些具体的细节。我认为我的想法听起来很合理,但我不知道为什么它可以在调试模式下工作。也许在调试模式下,字符串不会被保留。我也不是专家。

于 2009-04-22T20:50:57.573 回答
-3

将字符串组合在一起时,我总是使用 StringBuilder。它是为它设计的,并且比简单地使用“string1 + string2”更有效。

于 2009-04-22T20:29:13.007 回答