【问题标题】:C# List Comprehensions = Pure Syntactic Sugar?C# 列表理解 = 纯语法糖?
【发布时间】:2010-10-31 13:06:19
【问题描述】:

考虑以下 C# 代码:

IEnumerable numbers = Enumerable.Range(0, 10);
var evens = from num in numbers where num % 2 == 0 select num;

这种纯语法糖是否允许我将forforeach 循环作为单行代码编写?是否有任何编译器优化使上面的列表理解比循环构造更有效?这在后台是如何工作的?

【问题讨论】:

    标签: c# linq optimization compiler-construction list-comprehension


    【解决方案1】:

    正如杰森所说,你的代码相当于:

    Enumerable.Range(0, 10).Where(n => n % 2 == 0);
    

    请注意,lambda 将被转换为对每个元素进行的函数调用。这可能是开销的最大部分。我做了一个测试,这表明 LINQ 在这个确切的任务上慢了大约 3 倍(mono gmcs 版本 1.2.6.0)

    10000000 次循环代表的时间:00:00:17.6852560 10000000 次 LINQ 代表的时间:00:00:59.0574430 1000000 次循环代表的时间:00:00:01.7671640 1000000 次 LINQ 代表的时间:00:00:05.8868350

    编辑:Gishu 报告说 VS2008 和框架 v3.5 SP1 提供:

    1000000 次循环代表的时间:00.3724585 1000000 次 LINQ 代表的时间::00.5119530

    LINQ 在那里慢了大约 1.4 倍。

    它将 for 循环和列表与 LINQ(以及它在内部使用的任何结构)进行比较。无论哪种方式,它将结果转换为一个数组(必须强制 LINQ 停止“懒惰”)。两个版本重复:

    using System;
    using System.Collections.Generic;
    using System.Diagnostics;
    using System.Linq;
    
    public class Evens
    {
        private static readonly int[] numbers = new int[]{0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
    
        private static int MAX_REPS = 1000000;
    
        public static void Main()
        {
            Stopwatch watch = new Stopwatch();
    
            watch.Start();
            for(int reps = 0; reps < MAX_REPS; reps++)
            {
                List<int> list = new List<int>(); // This could be optimized with a default size, but we'll skip that.
                for(int i = 0; i < numbers.Length; i++)
                {
                    int number = numbers[i];
                    if(number % 2 == 0)
                        list.Add(number);
                }
                int[] evensArray = list.ToArray();
            }
            watch.Stop();
            Console.WriteLine("Time for {0} for loop reps: {1}", MAX_REPS, watch.Elapsed);
    
            watch.Reset();
            watch.Start();
            for(int reps = 0; reps < MAX_REPS; reps++)
            {
                var evens = from num in numbers where num % 2 == 0 select num;
                int[] evensArray = evens.ToArray();
            }
            watch.Stop();
            Console.WriteLine("Time for {0} LINQ reps: {1}", MAX_REPS, watch.Elapsed);
        }
    }
    

    过去对类似任务的性能测试(例如LINQ vs Loop - A performance test)证实了这一点。

    【讨论】:

    • 你在 Mono 上运行这个?您确定这可以与 Microsoft IL 相媲美吗?
    • Mono 使用 MSIL,按照标准化也称为 CIL。
    • 是的,但这并不意味着两个编译器正在创建等效的输出。
    • 使用 VS2008 和框架 v3.5 SP1,1000000 次循环代表的时间:00.3724585 1000000 次 LINQ 代表的时间:00.5119530
    • 罗伯特,我没有说他们创建了相同的目标代码。这正是我说我使用gmcs的原因。我添加了 Gishu 的结果进行比较。
    【解决方案2】:

    LINQ 对不同类型的数据的工作方式不同。您正在为其提供对象,因此它使用 LINQ-to-objects。这会被翻译成类似于直接 for 循环的代码。

    但是 LINQ 支持不同类型的数据。例如,如果您有一个名为“numbers”的数据库表,LINQ-to-SQL 将转换相同的查询;

    var evens = from num in numbers where num % 2 == 0 select num;
    

    像这样进入SQL;

    select num from numbers where num % 2 = 0
    

    然后执行。请注意,它不会通过创建一个内部带有“if”块的 for 循环来进行过滤; 'where' 子句修改发送到数据库服务器的查询。从查询到执行代码的转换取决于您提供给它的数据类型。

    所以不,LINQ 不仅仅是 for 循环的语法糖,而是一个用于“编译”查询的更复杂的系统。欲了解更多信息,请搜索Linq Provider。这些是 linq-to-objects 和 linq-to-xml 之类的组件,如果您有勇气,可以自己编写。

    【讨论】:

      【解决方案3】:

      在上面的代码中,您有一个 Linq 查询,它以与 foreach 循环相同的功能方式循环遍历 IEnumerable。但是,在您的代码中,引擎盖下发生了很多事情。如果您打算编写一个高性能循环,那么 foreach 可能效率更高。 Linq 旨在用于不同的目的(通用数据访问)。

      IEnumerable 接口公开了一个迭代器方法,然后由循环构造连续调用该方法,例如 foreach 或 Linq 查询。每次调用迭代器时都会返回集合中的下一项。

      【讨论】:

      • for 循环我的意思是 forforeach ...我编辑了这个问题以反映这一点...
      • 好的,但是假设我没有像上面的例子那样做一些简单的事情,我有一个更像这样的场景:我有一个 List 我想过滤掉所有 Person.LastName匹配某个正则表达式。我使用 List Comprehension 语法来迭代 List 并将正则表达式作为 lambda 表达式传入。这会比编写循环更有效吗?
      • 肯定会更有趣。它是否更快可能取决于许多事情,例如列表中有多少项目。正则表达式可能需要一段时间,所以你可能会受到约束。唯一确定的方法是运行一些测试。
      • Jonas,类似的原则适用于任何使用常规 C# 对象的 LINQ 查询。正如我在下面所说的,每个元素的谓词函数调用都会产生很大的开销。
      • 罗伯特,无论哪种方式,他都必须使用正则表达式(for-loop/LINQ)。
      【解决方案4】:

      您可以进一步简化代码

      var evens = Enumerable.Range(0, 10).Where(n => n % 2 == 0);
      

      这种形式的一个优点是这个表达式的执行被推迟到evens被迭代(foreach(var n in evens) { ... })。上面的语句只是告诉编译器要捕获如何枚举 0 到 10 之间的偶数的想法,但在绝对必要之前不要执行该想法。

      【讨论】:

        猜你喜欢
        • 2011-12-23
        • 2020-07-24
        • 2011-08-14
        • 2023-01-02
        • 2023-03-08
        • 2011-08-02
        • 1970-01-01
        • 2011-09-01
        • 2010-10-06
        相关资源
        最近更新 更多