C# Lambda 表达式类型安全吗?何时检查它们(complile time/runtime)?

Are C# Lambda Expressions Type Safe and when (complile time/runtime) are they checked?

我正在研究 LINQ to XML 查询,并使用过匿名函数和 lambda 表达式。一个简单的例子是 IEnumerables 上的 select 方法。

我理解LINQ查询是延迟执行的,这有点类似于lazy evaluation的概念,但是当VS2012的quick watch不能处理lambda表达式的语句时,我想到了这个问题。

Lambda 表达式在 C# 中是类型安全的吗?

我找不到对此的直接答案,或者可能是因为我不完全了解类型安全。我知道 OCaml 并且 Java 是类型安全的,而 Python 是弱类型的,我能想到的另一种方式是,如果语言是类型安全的,那么该语言中的 lambda 表达式并不特殊。 There is ambiguity in strong/weak typing 但在这里我提到它好像具有错误类型的 lambda 表达式将通过编译器并允许在 运行 时间执行。如果存在抛出异常的错误,它们是否仅在 运行 时被捕获?

他们什么时候检查的?编译时或 运行-time

例如,OCaml 类型在编译时进行检查,并且在解析类型之前不会执行。而 Python 不那么严格,是一种动态语言,即使出现类型错误,它也会编译和执行,仅在 运行 时捕获错误。从这个意义上讲,C# 是如何处理 lambda 表达式的?

在问这个问题之前我做了一些相关的研究:

  1. How are Java lambdas compiled
  2. This blog posts says LINQ is type-safe
  3. Tutorial on using lambda expressions from CodeProject
  4. Difference between C# Anonymous functions and Lambda Expressions

Lambda 的静态类型检查与任何其他 C# 代码一样多。它建立在相同的类型系统上,并强制执行所有相同的编译时类型检查。您当然可以在 lambda 中关闭静态类型检查(例如,通过执行转换),就像在任何其他 C# 代码中一样。

如果 lambda 被编译成可执行代码(而不是 Expression)并且是 运行,将执行完全相同的 运行时间检查,就好像你没有使用 lambada。

事实上,如果您使用编译成可执行代码的 lambda,它会简单地转换成一个新的命名方法,即使它在您的原始代码中是匿名的,在编译器的早期阶段之一。一旦转换为常规命名方法,它就会通过所有相同的类型检查任何其他代码。

在 C# 中存在两种类型 Lambda Expression:

A lambda expression is an anonymous function that you can use to create delegates or expression tree types.

第一种lambda表达式是匿名函数的语法糖:

Func<int, int> myFunc = x => x + 1;

完全等同于:

Func<int, int> myFunc = delegate(int x) { return x + 1; };

所以它显然是类型安全的,因为它是具有不同构造的 C# 代码。

另一种类型的 Lambda 表达式是生成表达式树的类型:

Expression<Func<int, int>> myFunc = x => x + 1;

这是不同的东西。这不是编译为 "code",而是编译为 Expression 类型的某个对象 "describe" x => x + 1 (甚至描述委托的类型)......它是编译为:

ParameterExpression par = Expression.Parameter(typeof(int), "x");
Expression<Func<int, int>> myFunc2 = Expression.Lambda<Func<int, int>>(
       Expression.Add(par, Expression.Constant(1)), 
       par);

现在,这段代码不能直接执行。可以通过.Compile()方法转换为可执行代码。一般来说,.Compile()d 表达式树是类型安全的,但表达式树通常不会被简单地编译。程序倾向于操纵它们以获得有趣的结果。它们可用于各种任务...例如提取属性的 "name" 或 "methods" 而无需在代码中包含具有 属性 或方法名称的字符串,或者转换为其他语言(实体 Framework/LinqToSQL 将表达式树转换为 SQL)。表达式树是非常安全的(可以在运行时 "manually build" 一个无效的表达式,但是当你执行 .Compile() 时你会得到一个异常,并且 C# 编译器接受的表达式树通常是安全的待编译),但如果表达式树用于其他用途,则可能会发生错误,甚至是与类型安全相关的错误。

我将引用:

var l =  (from s in db.Samples
          let action = db.Actions.Where(x => s.SampleID == x.SampleID && x.ActionTypeID == 1).FirstOrDefault()
          where s.SampleID == sampleID
          select new 
          {
             SampleID = s.SampleID,
             SampleDate = action.ActionDate,
          }).ToList();

大致相当于

var l = db.Samples.Select(s => new
    {
        s = s,
        action = db.Actions.Where(x => s.SampleID == x.SampleID && x.ActionTypeID == 1).FirstOrDefault()
    }).Where(x => x.s.SampleID == sampleID).Select(x => new
    {
        SampleID = x.s.SampleID,
        SampleDate = x.action.ActionDate
    }).ToList();

这里ActionDateDateTimeSampleDate也是。这个 LINQ 表达式将被编译器转换为一个大的 Lambda 表达式,并由 Entity Framework SQL 服务器端执行。现在...问题是 action 可能变成 null,所以 action.ActionDate 可能变成 null(因为表达式不会在本地执行,所以不会a NullReferenceException),当 null 被放入 SampleDate 时(我认为是 InvalidCastException),可能会抛出(将被抛出)异常。因此,虽然表达式是类型安全的,但库对它所做的操作会导致表达式生成非类型安全代码(无效转换)

想象一下,你可以写一个 class 是这样的:

public class Foo {
    public Baz DoSomething(Bar b)
    {
        return new Baz(b);
    }
}

很明显,这是在编译时强类型化的。所以现在我可以做一个像这样的委托声明:

public delegate Baz SomeDelegate(Bar b);

然后我可以修改 Foo 并添加 属性:

...
public SomeDelegate MyCall { get { return DoSomething; } }
...

你需要问问自己这样做有什么不同:

Bar b = new Bar();
Foo aFoo = new Foo();
var myDelegate = aFoo.MyCall;
Baz baz = myDelegate(b);

Bar b = new Bar();
var myDelegate = (Bar bar) => new Baz(bar);
Baz baz = myDelegate(b);

因为幕后发生的事情与此非常接近。 lambda 表达式可以通过创建一个匿名 class 并在其中包含一个方法来实现。 (FWIW,在 Java 中有 lambda 表达式之前,我经常使用静态私有内部 class 来模拟它们)。从语义上讲,它比这更复杂,因为变量是 free/bound 以及如何优雅地处理这个烂摊子(提示:Java 不处理它),但最终,C# 中的 lambda 表达式是语法糖给你一个内联定义的委托,没有 C# 可以处理的尽可能多的类型推断,并且委托是强类型的。