将IEnumerable<char>转换为字符串

180

我以前从没遇到过这种情况,但现在遇到了,而且很惊讶居然找不到一种真正简单的方法将 IEnumerable<char> 转换为 string

我能想到的最好的方法是 string str = new string(myEnumerable.ToArray());,但对我来说,似乎这会创建一个新的 char[],然后再从中创建一个新的 string,这似乎很费资源。

我本以为这应该是 .NET 框架中内置的常见功能。有没有更简单的方法实现这个功能?

对于那些感兴趣的人,我希望使用 LINQ 过滤字符串:

string allowedString = new string(inputString.Where(c => allowedChars.Contains(c)).ToArray());

奇怪,几分钟前我也问了自己同样的问题:http://stackoverflow.com/questions/11653119/checking-for-and-removing-any-characters-in-a-string/11653272#11653272 - Tim Schmelter
多奇怪啊!我确实搜索了类似的问题,但惊讶地发现没有找到任何答案。不过,我确实也可以使用那个解决方案! - Connell
是的,那可能更有效率。但是你有一个白名单而不是黑名单。所以你需要使用 inputString.Intersect(allowedChars) - Tim Schmelter
1
只是出于好奇,allowedChars 是一个 HashSet<char> 吗?我亲身学习了它如何提高性能。它将处理文件的时间从 34 秒缩短到了 4 秒。 - Scott Chamberlain
1
@Scott 不是的,它是一个编译时常量“字符串”。哇,那真是一个非常棒的性能提升。下次我会记得尝试一下的 ;) - Connell
6个回答

183
你可以使用 String.Concat()
var allowedString = String.Concat(
    inputString.Where(c => allowedChars.Contains(c))
);

注意:这种方法会对性能产生一定的影响。 String.Concat不会为字符集合专门处理,因此它执行的操作就像文档中提到的那样实际上是这样的)。虽然这为您提供了一种内置的完成此任务的方式,但它可以更好地完成。

我认为在框架中没有任何实现将特殊处理char,所以您需要自己实现。 一个简单循环将字符追加到字符串构建器中非常容易创建。


这里是一些基准测试结果,看起来还不错。

在32位发布版本上,对300个字符序列进行1000000次迭代:

ToArrayString:    00:00:03.1695463
Concat:           00:00:07.2518054
StringBuilderChars:00:00:03.1335455
StringBuilderStrings:00:00:06.4618266
static readonly IEnumerable<char> seq = Enumerable.Repeat('a', 300);

static string ToArrayString(IEnumerable<char> charSequence)
{
    return new String(charSequence.ToArray());
}

static string Concat(IEnumerable<char> charSequence)
{
    return String.Concat(charSequence);
}

static string StringBuilderChars(IEnumerable<char> charSequence)
{
    var sb = new StringBuilder();
    foreach (var c in charSequence)
    {
        sb.Append(c);
    }
    return sb.ToString();
}

static string StringBuilderStrings(IEnumerable<char> charSequence)
{
    var sb = new StringBuilder();
    foreach (var c in charSequence)
    {
        sb.Append(c.ToString());
    }
    return sb.ToString();
}

7
这段代码可能内部使用了 StringBuilder,StringBuilder 又在内部使用了动态增长的 char[],最终从中创建了最终的字符串。这与 new string(.ToArray()) 没有太大区别。 - dtb
1
由于字符串是字符的固定数组,因此为了构建它,您无法避免将可枚举项压缩成一个。这要么发生在您自己的代码中,要么发生在框架内部。 - MikeP
1
区别在于字符串需要是不可变的,因此当它从外部源接受一个char[]时,它需要复制它,以便更改不会反映在新字符串中。如果char[]是在内部构建的(即从传递的IEnumerable<char>中),则无需进行副本。传递IEnumerable并不会防止转换为数组,而是防止复制该数组。 - Servy
2
@Servy 我理解你的意思。但如果它确实使用了 StringBuilder,并且最终在该 StringBuilder 实例上使用了 sb.ToString(),那么 sb.ToString() 也可能会复制数据。因为通常情况下,在调用 .ToString() 后,StringBuilder 可以继续存在(并被修改)。但我同意他们可能已经采取了一些技巧来防止最终的复制,例如如果 StringBuilder 有一个非公共方法 ToStringWithoutCopy - Jeppe Stig Nielsen
1
你能发布使用.Aggregate()的性能结果吗?例如,new char[] {}.Aggregate("", (s, c) => s+c)。还有new char[] {}.Aggregate(new StringBuilder(), (sb, c) => sb.Append(c)).ToString() - Pluto
显示剩余5条评论

95

为发布.Net Core 2.1版本而编辑

针对.Net Core 2.1版本的测试结果如下:

1000000次“Concat”操作耗时842毫秒。

1000000次“new String”操作耗时1009毫秒。

1000000次“sb”操作耗时902毫秒。

简而言之,如果您使用的是.Net Core 2.1或更高版本,则Concat操作最佳。

有关更多详细信息,请参见微软博客文章


我已将这个问题作为另一个问题,但越来越多的人认为那是这个问题的直接答案。

我对将IEnumerable<char>转换为string的3种简单方法进行了性能测试,这些方法如下:

new string

return new string(charSequence.ToArray());

连接

return string.Concat(charSequence)

字符串构建器

var sb = new StringBuilder();
foreach (var c in charSequence)
{
    sb.Append(c);
}

return sb.ToString();

在我的测试中,详细描述在链接的问题中,对于"一些相当小的测试数据"的1000000次迭代,我得到了如下结果:

1000000次"Concat"迭代花费1597毫秒。

1000000次"new string"迭代花费869毫秒。

1000000次"StringBuilder"迭代花费748毫秒。

这使我认为,在这个任务中没有使用string.Concat的好理由。如果你想要简单性,请使用new string方法;如果你想要更好的性能,请使用StringBuilder

我需要注意的是,实际上所有这些方法都可以正常工作,这可能只是过度优化。


根据 https://github.com/dotnet/coreclr/pull/14298,我怀疑这可能需要重新审视。 - Jodrell
1
感谢 Stephen Toub 在 https://social.msdn.microsoft.com/profile/Stephen+Toub+-+MSFT 上的改进。@user:479403 - Jodrell

26

从.NET 4开始,许多字符串方法将IEnumerable作为参数。

string.Concat(myEnumerable);

11

另一个可能性是使用

string.Join("", myEnumerable);

我没有测量表现。


我进行了粗略的性能测量。 它与 x = String.Concat(y)foreach (...){x += y} 几乎相同。 - Alexander Khomenko

11

以下是StringBuilder答案的更简洁版本:

return charSequence.Aggregate(new StringBuilder(), (seed, c) => seed.Append(c)).ToString();

我使用与Jeff Mercado相同的测试进行了计时,结果比更加明确的方法慢了1秒,在相同的300个字符序列(32位发布版本)上迭代100万次。

static string StringBuilderChars(IEnumerable<char> charSequence)
{
    var sb = new StringBuilder();
    foreach (var c in charSequence)
    {
        sb.Append(c);
    }
    return sb.ToString();
}

所以,如果你是累加器的粉丝,那么这里就提供给你了。


10

我的数据与Jodrell发布的结果相反。首先看一下我使用的扩展方法:

public static string AsStringConcat(this IEnumerable<char> characters)
{        
    return String.Concat(characters);
}

public static string AsStringNew(this IEnumerable<char> characters)
{
    return new String(characters.ToArray());
}

public static string AsStringSb(this IEnumerable<char> characters)
{
    StringBuilder sb = new StringBuilder();
    foreach (char c in characters)
    {
        sb.Append(c);
    }
    return sb.ToString();
}

我的结果

使用

  • STRLEN = 31
  • ITERATIONS = 1000000

输入

  • ((IEnumerable<char>)RandomString(STRLEN)).Reverse()

结果

  • Concat: 1x
  • New: 3x
  • StringBuilder: 3x

输入

  • ((IEnumerable<char>)RandomString(STRLEN)).Take((int)ITERATIONS/2)

结果

  • Concat: 1x
  • New: 7x
  • StringBuilder: 7x

输入

  • ((IEnumerable<char>)RandomString(STRLEN)) (这只是一个向上转型)

结果

  • Concat: 0 毫秒
  • New: 2000 毫秒
  • StringBuilder: 2000 毫秒
  • Downcast: 0 毫秒

我在目标为.NET Framework 3.5的Intel i5 760上运行了此操作。


1
就其价值而言,我的测试针对的是 .Net 4.0,并且在没有附加调试器的情况下从命令行运行了一个发布版本。请尝试使用更纯净的序列进行测试,而不是强制转换。例如 Enumerable.Range(65, 26).Select(i => (char)i);,这应该可以避免优化快捷方式的机会。 - Jodrell

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