将 IEnumerable<char> 转换为字符串的最佳方法?

Posted

技术标签:

【中文标题】将 IEnumerable<char> 转换为字符串的最佳方法?【英文标题】:Best way to convert IEnumerable<char> to string? 【发布时间】:2011-12-27 20:13:24 【问题描述】:

为什么string不能使用流利的语言?

例如:

var x = "asdf1234";
var y = new string(x.TakeWhile(char.IsLetter).ToArray());

难道没有更好的方法将IEnumerable&lt;char&gt; 转换为string

这是我做的一个测试:

class Program

  static string input = "asdf1234";
  static void Main()
  
    Console.WriteLine("1000 times:");
    RunTest(1000, input);
    Console.WriteLine("10000 times:");
    RunTest(10000,input);
    Console.WriteLine("100000 times:");
    RunTest(100000, input);
    Console.WriteLine("100000 times:");
    RunTest(100000, "ffff57467");


    Console.ReadKey();

  

  static void RunTest( int times, string input)
  

    Stopwatch sw = new Stopwatch();

    sw.Start();
    for (int i = 0; i < times; i++)
    
      string output = new string(input.TakeWhile(char.IsLetter).ToArray());
    
    sw.Stop();
    var first = sw.ElapsedTicks;

    sw.Restart();
    for (int i = 0; i < times; i++)
    
      string output = Regex.Match(input, @"^[A-Z]+", 
        RegexOptions.IgnoreCase).Value;
    
    sw.Stop();
    var second = sw.ElapsedTicks;

    var regex = new Regex(@"^[A-Z]+", 
      RegexOptions.IgnoreCase);
    sw.Restart();
    for (int i = 0; i < times; i++)
    
      var output = regex.Match(input).Value;
    
    sw.Stop();
    var third = sw.ElapsedTicks;

    double percent = (first + second + third) / 100;
    double p1 = ( first / percent)/  100;
    double p2 = (second / percent )/100;
    double p3 = (third / percent  )/100;


    Console.WriteLine("TakeWhile took 0 (1:P2).,", first, p1);
    Console.WriteLine("Regex took 0, (1:P2)." , second,p2);
    Console.WriteLine("Preinstantiated Regex took 0, (1:P2).", third,p3);
    Console.WriteLine();
  

结果:

1000 times:
TakeWhile took 11217 (62.32%).,
Regex took 5044, (28.02%).
Preinstantiated Regex took 1741, (9.67%).

10000 times:
TakeWhile took 9210 (14.78%).,
Regex took 32461, (52.10%).
Preinstantiated Regex took 20669, (33.18%).

100000 times:
TakeWhile took 74945 (13.10%).,
Regex took 324520, (56.70%).
Preinstantiated Regex took 172913, (30.21%).

100000 times:
TakeWhile took 74511 (13.77%).,
Regex took 297760, (55.03%).
Preinstantiated Regex took 168911, (31.22%).

结论:我怀疑什么是更好的选择,我想我会继续TakeWhile,这只是第一次运行时最慢的。

无论如何,我的问题是是否有任何方法可以通过重新设置TakeWhile 函数的结果来优化性能。

【问题讨论】:

请解释一下“最好”是什么意思:最快?最不占内存?最容易理解? @LukeH 我已经决定选择什么:最快的。我的问题是是否有比new string(x.TakeWhile(p).ToArray) 更好的方法 @LukeH:可能想取消删除您的解决方案:它比我的要快很多 所有这些答案都引出了一个问题——为什么 IEnumerable.ToString() 没有在 System.Linq.Enumerable 中被覆盖 @Dave,您不能使用扩展方法覆盖基本函数。但是,I would want to see 是 string 构造函数中的重载,它采用 IEnumerable&lt;char&gt; 【参考方案1】:

如何将IEnumerable&lt;char&gt; 转换为string

string.Concat(x.TakeWhile(char.IsLetter));

【讨论】:

我猜 string.Concat 在内部使用了 StringBuilder。如果没有,那就太奇怪了。所以这个解决方案也应该表现得非常好。 仅限.Net 4.0。即使您在 3.5 中编写自己的 .TakeWhile , string.Concat(IEnumerable) 也不会达到您的预期。【参考方案2】:

为 .Net Core 2.1 的发布而编辑

重复测试.Net Core 2.1的发布,我得到这样的结果

“Concat”的 1000000 次迭代耗时 842 毫秒。

“新字符串”的 1000000 次迭代耗时 1009 毫秒。

“sb”的 1000000 次迭代耗时 902 毫秒。

简而言之,如果您使用的是 .Net Core 2.1 或更高版本,Concat 为王。


我已将此作为 another question 的主题,但越来越多地,这正在成为这个问题的直接答案。

我已经对 3 种将IEnumerable&lt;char&gt; 转换为string 的简单方法进行了一些性能测试,这些方法是

新字符串

return new string(charSequence.ToArray());

连接

return string.Concat(charSequence)

StringBuilder

var sb = new StringBuilder();
foreach (var c in charSequence)

    sb.Append(c);


return sb.ToString();

在我的测试中,这在linked question 中有详细说明,对于"Some reasonably small test data"1000000 迭代,我得到这样的结果,

“Concat”的 1000000 次迭代耗时 1597 毫秒。

“新字符串”的 1000000 次迭代耗时 869 毫秒。

“StringBuilder”的 1000000 次迭代耗时 748 毫秒。

这表明我没有充分的理由使用string.Concat 来完成这项任务。如果您想要简单,请使用 new string 方法,如果想要性能,请使用 StringBuilder

我会警告我的断言,实际上所有这些方法都可以正常工作,而这都可能是过度优化。

【讨论】:

我想牺牲 121 毫秒来使用 new string,而不是编写额外的三行代码来使用 StringBuilder。 #cleanCode。 您的 MS Blog Post 链接指向您的 Stack Overflow 问题。【参考方案3】:

假设您主要关注性能,那么这样的事情应该比您的任何示例都快得多:

string x = "asdf1234";
string y = x.LeadingLettersOnly();

// ...

public static class StringExtensions

    public static string LeadingLettersOnly(this string source)
    
        if (source == null)
            throw new ArgumentNullException("source");

        if (source.Length == 0)
            return source;

        char[] buffer = new char[source.Length];
        int bufferIndex = 0;

        for (int sourceIndex = 0; sourceIndex < source.Length; sourceIndex++)
        
            char c = source[sourceIndex];

            if (!char.IsLetter(c))
                break;

            buffer[bufferIndex++] = c;
        
        return new string(buffer, 0, bufferIndex);
    

【讨论】:

嗯,刚刚注意到您只需要字符串开头的字母,在这种情况下,我希望BrokenGlass's answer 是最快的。 (同样,我实际上并没有进行基准测试来确认。) +1 预分配缓冲区可能是使这更快的原因,但这只是一个猜测 - 有限的测试表明它比使用 Substring() 更快【参考方案4】:

为什么不能在字符串上使用流利的语言?

这是可能的。您在问题本身中做到了:

var y = new string(x.TakeWhile(char.IsLetter).ToArray());

难道没有更好的方法将IEnumerable&lt;char&gt; 转换为字符串吗?

(我的假设是:)

框架没有这样的构造函数,因为字符串是不可变的,您必须遍历枚举两次才能为字符串预先分配内存。这并不总是一种选择,尤其是当您的输入是流时。

对此的唯一解决方案是先推送到支持数组或StringBuilder,然后随着输入的增长重新分配。对于像字符串这样低级的东西,这可能应该被认为是一种过于隐藏的机制。通过鼓励人们使用一种不能尽可能快的机制,它还会将性能问题推到字符串类中。

通过要求用户使用ToArray扩展方法,这些问题很容易解决。

正如其他人所指出的,如果您编写支持代码并将该支持代码包装在扩展方法中以获得干净的界面,则可以实现您想要的(执行 表达代码)。

【讨论】:

顺便说一句,“流利”的最佳做法是我在我的扩展库中添加了一个 Join 重载,该重载采用 IEnumerable&lt;char&gt; 并返回 string 匿名投票者无济于事。说出你的理由,我会解决你的顾虑。【参考方案5】:

您通常可以在性能方面做得更好。但这能给你带来什么?除非这确实是您的应用程序的瓶颈并且您已经测量到它是我会坚持使用 Linq TakeWhile() 版本:它是最易读和可维护的解决方案,这对大多数应用程序都很重要。

如果您真的正在寻找原始性能,您可以手动进行转换 - 在我的测试中,以下是比 TakeWhile() 快 4+ 倍(取决于输入字符串长度) - 但是除非很关键,否则我不会亲自使用它:

int j = 0;
for (; j < input.Length; j++)

    if (!char.IsLetter(input[j]))
        break;

string output = input.Substring(0, j);

【讨论】:

+1。将其包装在某种辅助方法中以供重用并没有错。 source.LeadingLettersOnly() 之类的东西比 new string(source.TakeWhile(char.IsLetter).ToArray()) 更易读,imo。 @LukeH:您的解决方案更快 - 请取消删除! 该函数应该将搜索查询与几千 (100000) 个字符串的第一个字符进行比较,因此性能才是最重要的。 @BrokenGlass:好的,我已取消删除。我还没有运行任何基准测试,但令我惊讶的是我的跑赢了你的。我猜你的需要两个循环,先是显式的循环,然后是 Substring 内的另一个循环(尽管我假设 Substring 会使用一些本机代码尽可能快地传输所需的数据。) @LukeH:该行更具可读性,但支持代码不是更具可读性。我必须为扩展方法编写许多单元测试,而我可能只是对 Linq 进行代码审查。【参考方案6】:
return new string(foo.Select(x => x).ToArray());

【讨论】:

【参考方案7】:

此答案旨在结合已提供的优秀答案的以下方面。

    可读 面向未来/易于重构 快速

为此,使用IEnumerable&lt;char&gt; 上的扩展方法。

public static string Join(this IEnumerable<char> chars)

#if NETCOREAPP2_1_OR_GREATER
    return String.Concat(chars);
#else
    var sb = new System.Text.StringBuilder();
    foreach (var c in chars)
    
        sb.Append(c);
    

    return sb.ToString();
#endif

这涵盖了所有的基础。

    可读性很强:

    var y = x.TakeWhile(char.IsLetter).Join();

    如果将来有首选的新方法,可以通过更改一个代码块来更新所有转换。

    它支持基于当前正在编译的 .NET 版本的当前性能最佳的实现。

【讨论】:

【参考方案8】:

我在带有 BenchmarkDotNet 的 LINQPad 7 (dotnet 6.0.1) 中运行了一些测试:

Method Mean Error StdDev
StringFromArray 76.35 μs 1.482 μs 1.522 μs
StringConcat 100.93 μs 0.675 μs 0.631 μs
StringBuilder 100.52 μs 0.963 μs 0.901 μs
StringBuilderAggregate 116.80 μs 1.714 μs 1.519 μs

测试代码:

void Main() => BenchmarkRunner.Run<CharsToString>();

public class CharsToString 
    private const int N = 10000;
    private readonly char[] data = new char[N];

    public CharsToString() 
        var random = new Random(42);
        for (var i = 0; i < data.Length; i++) 
            data[i] = (char)random.Next(0, 256);
        
    

    [Benchmark]
    public string StringFromArray()
        => new string(data.Where(char.IsLetterOrDigit).ToArray());

    [Benchmark]
    public string StringConcat()
        => string.Concat(data.Where(char.IsLetterOrDigit));

    [Benchmark]
    public string StringBuilder() 
        var sb = new StringBuilder();
        
        foreach (var c in data.Where(char.IsLetterOrDigit))
            sb.Append(c);
        
        return sb.ToString();
    

    [Benchmark]
    public string StringBuilderAggregate() => data
        .Where(char.IsLetterOrDigit)
        .Aggregate(new StringBuilder(), (sb, c) => sb.Append(c))
        .ToString();

【讨论】:

以上是关于将 IEnumerable<char> 转换为字符串的最佳方法?的主要内容,如果未能解决你的问题,请参考以下文章

如何将多个 IEnumerable<IEnumerable<T>> 列表添加到 IEnumerable<List<int>>

为啥我不能将 List<List<Foo>> 传递给 IEnumerable<IEnumerable<Foo>>

无法将类型“IEnumerable<T>”隐式转换为“ActionResult<IEnumerable<T>>”

如何将两个 IEnumerable<T> 连接成一个新的 IEnumerable<T>?

将 IEnumerable<T> 转换为 List<T>

将 DataRowCollection 转换为 IEnumerable<T>