LINQ 实际上是编译成什么?

8

背景

这个问题的背景是,我最近在评论区与另一个显然很有知识的用户讨论了LINQ是如何编译的。我首先进行了"总结",说LINQ被编译成了for循环。虽然这并不正确,但我从其他堆栈(例如这个)中了解到,LINQ查询被编译为一个带有循环的lambda表达式。当变量第一次枚举时调用该表达式(之后结果将被存储)。另一个用户说,LINQ采取了额外的优化,比如哈希。我找不到任何支持或反对这种说法的文献。

我知道这似乎是一个非常晦涩的点,但我一直觉得,如果我不完全理解某件事情的工作原理,那么使用它时出错的原因就很难理解。

问题

那么,让我们来看下面这个非常简单的例子:

var productNames = 
    from p in products 
    where p.Id > 100 and p.Id < 5000
    select p.ProductName;

这个语句在 CLR 中实际编译成什么?LINQ 采取了哪些优化措施,来取代我手动解析结果的函数?这只是语义学还是其中还有更多内容?
澄清:显然,我问这个问题是因为我不理解 LINQ 的“黑盒”内部是什么样子。虽然我知道 LINQ 很复杂(也很强大),但我主要是想了解 CLR 或与 LINQ 语句功能相当的基本理解。有很棒的网站帮助理解如何创建 LINQ 语句,但很少提供关于它们如何实际编译或运行的任何指导。
附注:我一定会阅读 John Skeet 关于 Linq to Objects 的系列文章。
附注2:我不应该将此标记为 LINQ to SQL。我了解 ORM 和微型ORM的工作原理。这实际上与问题无关。

3
重新实现LINQ to Objects - Jon Skeet 本系列文章介绍了如何重新实现LINQ to Objects,以便更好地理解和掌握该技术。第一部分着重介绍了本系列所需的准备工作和环境,并概述了将要涉及到的主题和问题。 - Habib
1
请访问http://msmvps.com/blogs/jon_skeet/archive/tags/Edulinq/default.aspx,了解有关LINQ to Objects的详细解释。 - SLaks
1
@Hogan 不会的...如果你使用像 JoinGroupByDistinctToLookup 等这样的东西,那当然可以。但是 Where 根本不会使用基于哈希的算法/结构。 - Servy
1
@Hogan 请仔细阅读这些帖子。他在每篇文章中都讨论了微软是如何实现的,以及他何时/为什么选择与他们的实现不同。 - Servy
1
@Hogan,永远不要质疑Jon Skeet :) 链接 - Habib
显示剩余18条评论
2个回答

13
对于LINQ to Objects,这将被编译成一组静态方法调用:
var productNames = 
    from p in products 
    where p.Id > 100 and p.Id < 5000
    select p.ProductName;

请提供需要翻译的内容。

Becomes:

Please provide the content to be translated.

IEnumerable<string> productNames = products
                                       .Where(p => p.Id > 100 and p.Id < 5000)
                                       .Select(p => p.ProductName);

这里使用了在Enumerable类型中定义的扩展方法,因此实际上编译为:

IEnumerable<string> productNames = 
     Enumerable.Select(
        Enumerable.Where(products, p => p.Id > 100 and p.Id < 5000),
        p => p.ProductName
     );
lambda表达式会被编译器转化为方法。在where中的lambda会被转化为一个可以设置为Func<Product, Boolean>的方法,而select则是Func<Product, String>
要详细了解,请参见Jon Skeet的博客系列:重新实现LINQ to Objects。他详细介绍了整个过程,包括编译器的转换(从查询语法到方法调用),如何实现方法等。
请注意,LINQ to Sql和IQueryable<T>实现不同。由lambda生成的Expression<T>传递给查询提供程序,后者以某种方式“转换”(由提供程序决定如何执行),通常在ORM的情况下在服务器上运行。
例如,对于此方法:
    private static IEnumerable<string> ProductNames(IEnumerable<Product> products)
    {
        var productNames =
            from p in products
            where p.Id > 100 && p.Id < 5000
            select p.ProductName;
        return productNames;
    }

编译为以下IL代码:

  .method private hidebysig static class [mscorlib]System.Collections.Generic.IEnumerable`1<string> ProductNames(class [mscorlib]System.Collections.Generic.IEnumerable`1<class ConsoleApplication3.Product> products) cil managed
{
    .maxstack 3
    .locals init (
        [0] class [mscorlib]System.Collections.Generic.IEnumerable`1<string> enumerable,
        [1] class [mscorlib]System.Collections.Generic.IEnumerable`1<string> enumerable2)
    L_0000: nop 
    L_0001: ldarg.0 
    L_0002: ldsfld class [mscorlib]System.Func`2<class ConsoleApplication3.Product, bool> ConsoleApplication3.Program::CS$<>9__CachedAnonymousMethodDelegate3
    L_0007: dup 
    L_0008: brtrue.s L_001d
    L_000a: pop 
    L_000b: ldnull 
    L_000c: ldftn bool ConsoleApplication3.Program::<ProductNames>b__2(class ConsoleApplication3.Product)
    L_0012: newobj instance void [mscorlib]System.Func`2<class ConsoleApplication3.Product, bool>::.ctor(object, native int)
    L_0017: dup 
    L_0018: stsfld class [mscorlib]System.Func`2<class ConsoleApplication3.Product, bool> ConsoleApplication3.Program::CS$<>9__CachedAnonymousMethodDelegate3
    L_001d: call class [mscorlib]System.Collections.Generic.IEnumerable`1<!!0> [System.Core]System.Linq.Enumerable::Where<class ConsoleApplication3.Product>(class [mscorlib]System.Collections.Generic.IEnumerable`1<!!0>, class [mscorlib]System.Func`2<!!0, bool>)
    L_0022: ldsfld class [mscorlib]System.Func`2<class ConsoleApplication3.Product, string> ConsoleApplication3.Program::CS$<>9__CachedAnonymousMethodDelegate5
    L_0027: dup 
    L_0028: brtrue.s L_003d
    L_002a: pop 
    L_002b: ldnull 
    L_002c: ldftn string ConsoleApplication3.Program::<ProductNames>b__4(class ConsoleApplication3.Product)
    L_0032: newobj instance void [mscorlib]System.Func`2<class ConsoleApplication3.Product, string>::.ctor(object, native int)
    L_0037: dup 
    L_0038: stsfld class [mscorlib]System.Func`2<class ConsoleApplication3.Product, string> ConsoleApplication3.Program::CS$<>9__CachedAnonymousMethodDelegate5
    L_003d: call class [mscorlib]System.Collections.Generic.IEnumerable`1<!!1> [System.Core]System.Linq.Enumerable::Select<class ConsoleApplication3.Product, string>(class [mscorlib]System.Collections.Generic.IEnumerable`1<!!0>, class [mscorlib]System.Func`2<!!0, !!1>)
    L_0042: stloc.0 
    L_0043: ldloc.0 
    L_0044: stloc.1 
    L_0045: br.s L_0047
    L_0047: ldloc.1 
    L_0048: ret 
}

请注意,这些是方法调用的普通call指令。Lambda表达式会转换为其他方法,例如:

[CompilerGenerated]
private static bool <ProductNames>b__2(Product p)
{
    return ((p.Id > 100) && (p.Id < 0x1388));
}

这个回答并没有解决 OP 的核心问题“这个语句在 CLR 中实际编译成了什么?” - Shiva
@drew_w 那个额外的细节是否给你想要的东西? - Reed Copsey
@Hogan 他没有按照相同的方式实现它 - 他试图对其工作原理进行全面解释,但他甚至指出他的实现与官方的不同。他只是试图展示如何实现API以使其易于理解。 - Reed Copsey
@ReedCopsey - 这是我的观点,问题是“微软是如何实现的”。虽然Jon的系列文章非常好,但它根本没有回答这个问题。 - Hogan
1
@drew_w 这只是语义学问题 - 我尝试通过展示发生的事情来解释,但实际上只是将其转换为方法调用,并且内部的方法调用大多数情况下只是循环。使用 IQueryable<T>,它有优化,可以在服务器上进行查询,而不是在本地进行,但对于 LINQ to Objects,更多的是使使用变得简化和更清晰。 - Reed Copsey
显示剩余9条评论

-1

查询语法只是方法语法的语法糖,它被有效地编译为以下内容:

var productNames = Products().Where(p => p.Id > 100 && p.Id < 5000).Select(p => productName);

现在这些函数实际上的作用取决于你使用的LINQ版本,例如Linq to Objects(将内存处理器链接在一起)或Linq to SQL(将其转换为SQL查询)等。


  1. 你并没有真正回答自己的问题。添加的澄清说明了你对查询语法如何映射到方法语法不感兴趣,而是关注实际的“Where”、“Select”等方法的实现。
  2. 代码片段后面你说的一切都是错误的。每个 Linq-to-anythingButObjects 提供程序都基于“IQueryable”接口,“Where”、“Select”等方法都是“Queryable.Where”等方法,并不是每个查询提供程序都有不同的方法。区别在于查询提供程序对生成的表达式的处理方式不同。
- Servy

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