【问题标题】:Optimizing LINQ combining multiple lists into new generic list优化 LINQ 将多个列表组合成新的通用列表
【发布时间】:2017-11-06 18:28:27
【问题描述】:

给定以下三个列表:

    var FirstNames = new List<string>(){ "Bob", "Sondra", "Avery", "Von", "Randle", "Gwen", "Paisley" };
    var LastNames = new List<string>(){ "Anderson", "Carlson", "Vickers", "Black", "Schultz", "Marigold", "Johnson" };
    var Birthdates = new List<DateTime>()
                    { 
                        Convert.ToDateTime("11/12/1980"), 
                        Convert.ToDateTime("09/16/1978"), 
                        Convert.ToDateTime("05/18/1985"), 
                        Convert.ToDateTime("10/29/1980"), 
                        Convert.ToDateTime("01/19/1989"), 
                        Convert.ToDateTime("01/14/1972"), 
                        Convert.ToDateTime("02/20/1981") 
                    };

我想将它们组合成一个新的泛型类型,其中列表共享的关系是它们在集合中的位置。即 FirstNames[0]、LastNames[0]、Birthdates[0] 是相关的。

所以我想出了这个 LINQ,与索引匹配,现在似乎工作正常:

    var students = from fn in FirstNames
                   from ln in LastNames
                   from bd in Birthdates
                   where FirstNames.IndexOf(fn) == LastNames.IndexOf(ln)
                   where FirstNames.IndexOf(fn) == Birthdates.IndexOf(bd)
                   select new { First = fn, Last = ln, Birthdate = bd.Date };

但是,我强调测试了这段代码(每个List&lt;string&gt;List&lt;DateTime&gt; 都加载了几百万条记录)并且遇到了SystemOutOfMemory 异常。

有没有其他方法可以写出这个查询来更有效地使用 Linq 获得相同的结果?

【问题讨论】:

    标签: c# linq


    【解决方案1】:

    这就是 Zip 的用途。

    var result = FirstNames
      .Zip(LastNames, (f,l) => new {f,l})
      .Zip(BirthDates, (fl, b) => new {First=fl.f, Last = fl.l, BirthDate = b});
    

    关于缩放:

    int count = 50000000;
    var FirstNames = Enumerable.Range(0, count).Select(x=>x.ToString());
    var LastNames = Enumerable.Range(0, count).Select(x=>x.ToString());
    var BirthDates = Enumerable.Range(0, count).Select(x=> DateTime.Now.AddSeconds(x));
    
    var sw = new Stopwatch();
    sw.Start();
    
    var result = FirstNames
      .Zip(LastNames, (f,l) => new {f,l})
      .Zip(BirthDates, (fl, b) => new {First=fl.f, Last = fl.l, BirthDate = b});
    
    foreach(var r in result)
    {
        var x = r;
    }
    sw.Stop();
    Console.WriteLine(sw.ElapsedMilliseconds); // Returns 69191 on my machine.
    

    虽然这些因内存不足而爆炸:

    int count = 50000000;
    var FirstNames = Enumerable.Range(0, count).Select(x=>x.ToString());
    var LastNames = Enumerable.Range(0, count).Select(x=>x.ToString());
    var BirthDates = Enumerable.Range(0, count).Select(x=> DateTime.Now.AddSeconds(x));
    
    var sw = new Stopwatch();
    sw.Start();
    
    var FirstNamesList = FirstNames.ToList(); // Blows up in 32-bit .NET with out of Memory
    var LastNamesList = LastNames.ToList();
    var BirthDatesList = BirthDates.ToList();
    
    var result = Enumerable.Range(0, FirstNamesList.Count())
        .Select(i => new 
                     { 
                         First = FirstNamesList[i], 
                         Last = LastNamesList[i], 
                         Birthdate = BirthDatesList[i] 
                     });
    
    result = BirthDatesList.Select((bd, i) => new
    { 
        First = FirstNamesList[i], 
        Last = LastNamesList[i], 
        BirthDate = bd 
    });
    
    foreach(var r in result)
    {
        var x = r;
    }
    sw.Stop();
    Console.WriteLine(sw.ElapsedMilliseconds);
    

    在较低的值下,将 Enumerables 转换为 List 的成本也比创建额外的对象要昂贵得多。 Zip 比索引版本快大约 30%。随着您添加更多列,Zips 的优势可能会缩小。

    性能特征也大不相同。 Zip 例程将几乎立即开始输出答案,而其他例程仅在读取整个 Enumerable 并将其转换为列表后才开始输出答案,因此,如果您获取结果并使用 .Skip(x).Take(y) 对其进行分页,或者检查是否有什么存在.Any(...) 它会更快,因为它不必转换整个可枚举。

    最后,如果它变得对性能至关重要,并且您需要实现许多结果,您可以考虑扩展 zip 以处理任意数量的 Enumerable,例如(无耻地从 Jon Skeet 窃取 - https://codeblog.jonskeet.uk/2011/01/14/reimplementing-linq-to-objects-part-35-zip/):

    private static IEnumerable<TResult> Zip<TFirst, TSecond, TThird, TResult>( 
        IEnumerable<TFirst> first, 
        IEnumerable<TSecond> second,
        IEnumerable<TThird> third, 
        Func<TFirst, TSecond, TThird, TResult> resultSelector) 
    { 
        using (IEnumerator<TFirst> iterator1 = first.GetEnumerator()) 
        using (IEnumerator<TSecond> iterator2 = second.GetEnumerator()) 
        using (IEnumerator<TThird> iterator3 = third.GetEnumerator()) 
        { 
            while (iterator1.MoveNext() && iterator2.MoveNext() && iterator3.MoveNext()) 
            { 
                yield return resultSelector(iterator1.Current, iterator2.Current, iterator3.Current); 
            } 
        } 
    }
    

    那么你可以这样做:

    var result = FirstNames
      .Zip(LastNames, BirthDates, (f,l,b) => new {First=f,Last=l,BirthDate=b});
    

    现在你甚至没有创建中间对象的问题,所以你得到了世界上最好的。

    或者使用这里的实现来通用处理任何数字:Zip multiple/abitrary number of enumerables in C#

    【讨论】:

    • 我最初考虑在出现此问题时使用Zip。我想我同意你的观点,这就是它的设计目的,但你认为这个解决方案会扩展吗?使用简单的秒表类,所有答案的表现都非常相似。这将创建一个新的名字和姓氏列表,然后是另一个名字/姓氏和出生日期。如果我们添加另一个列表,比如年龄,它会在这方面进行扩展吗?
    • @FusRoDah 这在很大程度上取决于来源以及数据的使用方式。创建临时对象确实会花费一些性能(我相信),但是,如果源和/或消耗将从 Enumerables 惰性评估中受益,它的性能可能会更好或更差,特别是因为在最好的情况下,它需要更少的内存作为数据流一次一个地从源中提取,处理然后丢弃,而不是假设源是可索引的(或转换为可索引的)。
    • 感谢您详细说明优缺点并提供代码进行比较。检查Any() 正是我们通用列表的最终产品要做的事情。因此,我认为您已经完全回答了我的问题。
    【解决方案2】:

    另一种选择是在提供的索引器中使用 Select 重载:

    var result = Birthdates.Select((bd, i) => new
    { 
        First = FirstNames[i], 
        Last = LastNames[i], 
        Birthdate = bd 
    });
    

    【讨论】:

    • 我真的很想知道这将有利于未来的应用,甚至不仅仅与这个问题有关。我还有很多关于 LINQ 的知识需要了解,谢谢
    【解决方案3】:

    是的,使用范围生成器:

    var result = Enumerable.Range(0, FirstNames.Count)
        .Select(i => new 
                     { 
                         First = FirstNames[i], 
                         Last = LastNames[i], 
                         Birthdate = Birthdates[i] 
                     });
    

    【讨论】:

    • 我喜欢这样,如果我们需要添加另一个列表,我们可以添加一个更短的代码行,答案是:Age = Ages[i]。对初级开发者友好的支持
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2019-02-13
    相关资源
    最近更新 更多