LINQ是如何编译成CIL的?

17
例如:
var query = from c in db.Cars select c;
foreach(Car aCar in query)
{
     Console.WriteLine(aCar.Name);
}
这段代码编译后会变成什么样?背后会发生什么事情?

我假设这是一个LINQ-to-SQL查询,而不仅仅是对集合的过滤?显然,前者在幕后要做更多的工作。 - mqp
1
实际上,让我们在集合上使用LINQ-to-Objects过滤器。 - Liggi
2个回答

29

编译过程如下:

  1. 首先,LINQ查询表达式被转换为方法调用:

    public static void Main()
    {
        var query = db.Cars.Select<Car, Car>(c => c);
        foreach (Car aCar in query)
        {
             Console.WriteLine(aCar.Name);
        }
    }
    
    如果 db.Cars 的类型是 IEnumerable<Car>(这在LINQ-to-Objects中是这样的),那么lambda表达式会被转换为一个单独的方法:
  2. private Car lambda0(Car c)
    {
        return c;
    }
    private Func<Car, Car> CachedAnonymousMethodDelegate1;
    public static void Main()
    {
        if (CachedAnonymousMethodDelegate1 == null)
            CachedAnonymousMethodDelegate1 = new Func<Car, Car>(lambda0);
        var query = db.Cars.Select<Car, Car>(CachedAnonymousMethodDelegate1);
        foreach // ...
    }
    

    实际上,这个方法不叫lambda0,而是类似于<Main>b__0的名称(其中Main是包含该方法的名称)。同样地,缓存的委托实际上被称为CS$<>9__CachedAnonymousMethodDelegate1

    如果您使用的是LINQ-to-SQL,则db.Cars将是IQueryable<Car>类型,此步骤非常不同。它会将 lambda 表达式转换为表达式树:

    public static void Main()
    {
        var parameter = Expression.Parameter(typeof(Car), "c");
        var lambda = Expression.Lambda<Func<Car, Car>>(parameter, new ParameterExpression[] { parameter }));
        var query = db.Cars.Select<Car, Car>(lambda);
        foreach // ...
    }
    
  3. foreach循环会被转换为一个try/finally块(两者相同):

  4. IEnumerator<Car> enumerator = null;
    try
    {
        enumerator = query.GetEnumerator();
        Car aCar;
        while (enumerator.MoveNext())
        {
            aCar = enumerator.Current;
            Console.WriteLine(aCar.Name);
        }
    }
    finally
    {
        if (enumerator != null)
            ((IDisposable)enumerator).Dispose();
    }
    
  5. 最终,这将被编译成预期的IL代码。以下是针对 IEnumerable<Car> 的内容:

  6. // Put db.Cars on the stack
    L_0016: ldloc.0 
    L_0017: callvirt instance !0 DatabaseContext::get_Cars()
    
    
    // “if” starts here
    L_001c: ldsfld Func<Car, Car> Program::CachedAnonymousMethodDelegate1
    L_0021: brtrue.s L_0034
    L_0023: ldnull 
    L_0024: ldftn Car Program::lambda0(Car)
    L_002a: newobj instance void Func<Car, Car>::.ctor(object, native int)
    L_002f: stsfld Func<Car, Car> Program::CachedAnonymousMethodDelegate1
    
    
    // Put the delegate for “c => c” on the stack
    L_0034: ldsfld Func<Car, Car> Program::CachedAnonymousMethodDelegate1
    
    
    // Call to Enumerable.Select()
    L_0039: call IEnumerable<!!1> Enumerable::Select<Car, Car>(IEnumerable<!!0>, Func<!!0, !!1>)
    L_003e: stloc.1
    
    
    // “try” block starts here
    L_003f: ldloc.1 
    L_0040: callvirt instance IEnumerator<!0> IEnumerable<Car>::GetEnumerator()
    L_0045: stloc.3
    
    
    // “while” inside try block starts here
    L_0046: br.s L_005a
    L_0048: ldloc.3   // body of while starts here
    L_0049: callvirt instance !0 IEnumerator<Car>::get_Current()
    L_004e: stloc.2 
    L_004f: ldloc.2 
    L_0050: ldfld string Car::Name
    L_0055: call void Console::WriteLine(string)
    L_005a: ldloc.3   // while condition starts here
    L_005b: callvirt instance bool IEnumerator::MoveNext()
    L_0060: brtrue.s L_0048  // end of while
    L_0062: leave.s L_006e   // end of try
    
    
    // “finally” block starts here
    L_0064: ldloc.3 
    L_0065: brfalse.s L_006d
    L_0067: ldloc.3 
    L_0068: callvirt instance void IDisposable::Dispose()
    L_006d: endfinally 
    

    IQueryable<Car>版本的编译代码也符合预期。以下是与上述内容不同的重要部分(现在本地变量将具有不同的偏移量和名称,但让我们忽略这一点):

    // typeof(Car)
    L_0021: ldtoken Car
    L_0026: call Type Type::GetTypeFromHandle(RuntimeTypeHandle)
    
    
    // Expression.Parameter(typeof(Car), "c")
    L_002b: ldstr "c"
    L_0030: call ParameterExpression Expression::Parameter(Type, string)
    L_0035: stloc.3 
    
    
    // Expression.Lambda(...)
    L_0036: ldloc.3 
    L_0037: ldc.i4.1           // var paramArray = new ParameterExpression[1]
    L_0038: newarr ParameterExpression
    L_003d: stloc.s paramArray
    L_003f: ldloc.s paramArray
    L_0041: ldc.i4.0                    // paramArray[0] = parameter;
    L_0042: ldloc.3 
    L_0043: stelem.ref 
    L_0044: ldloc.s paramArray
    L_0046: call Expression<!!0> Expression::Lambda<Func<Car, Car>>(Expression, ParameterExpression[])
    
    
    // var query = Queryable.Select(...);
    L_004b: call IQueryable<!!1> Queryable::Select<Car, Car>(IQueryable<!!0>, Expression<Func<!!0, !!1>>)
    L_0050: stloc.1 
    

非常好的答案!我认为这里步骤2是最重要的。为了完整起见,你可以补充一下关于哪些过程中没有使用 Linq(例如 Linq2Sql)的信息吗? - Preet Sangha
@Preet:不确定你的意思,但我添加了关于LINQ to SQL的注释。 - Timwi
@Andrey:我不明白。 这篇帖子中没有任何声称存储在内存中的内容和未存储的内容。这篇帖子回答了问题,即查询表达式如何编译为IL。 - Timwi
@Timwi,好答案!不过我有一个评论。在第一部分中,您写道它被转换为Select<Car, Car>(c => c)。您确定它在那里进行了类型推断吗?我认为第一步是只进行“愚蠢”的翻译,让编译器在第二步中进行所有的类型推断。因此,在第一步中,它将是没有类型的选择。 - Anton
@Anton:你说得对,类型推断与LINQ语法转换为方法语法是分开的。后者确实是一种“愚蠢”的翻译。但是,它发生在我标记为“步骤2”的东西之前。我只是将它们混为一体成为一个“步骤”。 - Timwi
显示剩余3条评论

0

你应该编译它并运行ildasm来查找生成的可执行文件。


我已经做了这个,但是我希望能得到更全面和详细的解释。 :) - Liggi
3
如果你想要更加用户友好的东西,可以使用反光板。请注意,这句话已经被翻译成中文。 - Kirk Woll

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