【问题标题】:How does the compiler know what datatype the lambda expression should be编译器如何知道 lambda 表达式应该是什么数据类型
【发布时间】:2019-10-02 10:35:53
【问题描述】:

因此,当您使用 EF Core 并使用大多数 Linq 扩展时,您实际上使用的是 System.Linq.Expressions 而不是通常的 Func

假设您在DbSet 上使用FirstOrDefault

DbContext.Foos.FirstOrDefault(x=> x.Bar == true);

当你在FirstOrDefaultctrl + lmb 时,它会显示以下重载:

public static TSource FirstOrDefault<TSource>(this IQueryable<TSource> source, Expression<Func<TSource, bool>> predicate)

Func 也有过载:

public static TSource FirstOrDefault<TSource>(this IEnumerable<TSource> source, Func<TSource, bool> predicate)

当您想将表达式存储在变量中时,您可以执行以下操作:

Func<Entity, bool> = x => x.Bar == true;

Expression<Func<Entity, bool>> = x => x.Bar == true;

那么编译器如何决定在使用这些扩展方法时应该使用哪个重载呢?

【问题讨论】:

  • 如果 func 作为 FirstAndDefault() 的输入参数,编译器只能使用 IEnumerable 重载,因为 func 无法转换为 SQL 以传递给数据库.这可能与此有关。
  • @DavidG 我猜这一般适用于.Net?
  • @Bensjero 好吧,这仍然没有意义。
  • 我意识到我链接到 VB 文档而不是 C#。 C# 有它自己的规则。

标签: c# lambda entity-framework-core ef-core-3.0


【解决方案1】:

接受的答案是一个合理的解释,但我想我可能会提供更多细节。

假设您在 DbSet 上使用 FirstOrDefault。 DbContext.Foos.FirstOrDefault(x=&gt; x.Bar == true);

首先,我希望你不要写那个。如果你想问“下雨了吗?”你问“下雨了吗?”还是您问“正在下雨的说法是真实的说法吗?”直接说FirstOrDefault(x =&gt; x.Bar)

接下来,给定这些重载:

public static TSource FirstOrDefault<TSource>(
    this IQueryable<TSource> source, 
    Expression<Func<TSource, bool>> predicate)

public static TSource FirstOrDefault<TSource>(
    this IEnumerable<TSource> source, 
    Func<TSource, bool> predicate)

编译器如何选择最好的重载?

首先我们进行类型推断来确定TSource 在每个中是什么。类型推断算法的细节很复杂;如果您对此有疑问,请提出更集中的问题。

如果类型推断无法确定TSource 的类型,则从候选集中丢弃失败的推断方法。在您的示例中,TSource 可以确定为Foo,大概。

接下来,在剩下的候选者中,我们检查它们是否参数对形式的适用性。也就是说,我们可以将每个提供的参数转换为其相应的形式参数类型吗? (当然,提供的参数数量是否正确,等等。)在您的示例中,这两种方法都适用。

在剩下的适用候选人中,我们现在进入一轮更好的检查。更好的检查是如何工作的?同样,我们逐个参数进行。在这种情况下,我们有两个问题需要回答:

  • DbContext.Foos 可以转换为 IEnumerable&lt;Foo&gt;IQueryable&lt;Foo&gt;。如果有的话,哪个是更好的转换?
  • lambda 可以转换为委托或表达式树。如果有的话,哪个是更好的转换?

第二个问题很容易回答:两者都不是更好。我们没有从这个关于更好的论点中学到任何东西。

为了回答第一个问题,我们应用规则转换为具体优于转换为一般。如果可以选择转换为长颈鹿或哺乳动物,转换为长颈鹿会更好。所以现在的问题是更具体IQueryable&lt;Foo&gt; 还是IEnumerable&lt;Foo&gt;

特异性检查的规​​则很简单:如果 X 可以隐式转换为 Y,但 Y 不能隐式转换为 X,则 X 更具体。长颈鹿可以用在需要动物的地方,但动物不能用在需要长颈鹿的地方,所以长颈鹿更具体。或者:每只长颈鹿都是动物,但不是每只动物都是长颈鹿,所以长颈鹿更具体。

通过这种衡量,IQueryable&lt;T&gt;IEnumerable&lt;T&gt; 更具体,因为每个可查询对象都是可枚举的,但并非每个可枚举对象都是可查询的。

所以可查询更具体,因此转换更好。

现在我们问一个问题“是否有一种独特的适用的候选方法,与每个其他候选方法相比,至少有一个转换更好 没有转化更糟?”有;可查询候选具有这样的特性,即它在一个参数中比其他参数都好,并且在所有其他参数中都不差,并且它是具有此属性的唯一方法。

因此重载决议选择该方法。

如果您有更多问题,我鼓励您阅读规范。

【讨论】:

  • 这是很多有用和有趣的信息,我真的很感激。没想到背后竟然有这么多逻辑!
  • @Twenty:不客气。我们努力在算法设计中坚持原则。
【解决方案2】:

继承的类接近度比精确的方法参数类型更重要

注意Expression&lt;Func&lt;T,bool&gt;&gt; 变体适用于IQueryable&lt;T&gt;,而Func&lt;T, bool&gt; 变体适用于IEnumerable&lt;T&gt;
在寻找匹配方法时,编译器将始终选择最接近对象类型的方法。继承层次如下:

DbSet<T> : IQueryable<T> : IEnumerable<T>

注意:中间可能还有其他继承,但这并不重要。重要的是最接近DbSet&lt;T&gt;IQueryable&lt;T&gt;DbSet&lt;T&gt; 的关系比 IEnumerable&lt;T&gt; 更密切。

因此,编译器会尝试在IQueryable&lt;T&gt; 中寻找匹配的方法。它提出了两个问题:

  • 这种类型有同名的方法吗?
  • 方法参数类型是否匹配/映射?

IQueryable&lt;T&gt; 有一个FirstOrDefault 方法,所以满足要点1);并且由于x =&gt; x.MyBoolean 可以隐式转换为Expression&lt;Func&lt;T, bool&gt;&gt;,因此也满足了要点2。

因此,您最终会得到在IQueryable&lt;T&gt; 上定义的Expression&lt;Func&lt;T,bool&gt;&gt; 变体。

假设x =&gt; x.MyBoolean可以隐式转换为Expression&lt;Func&lt;T,bool&gt;&gt;,但可以转换为Func&lt;T,bool&gt;(注意:情况并非如此,但这其他类型/值可能会发生),那么项目符号点 2 将得到满足。
此时,由于编译器在IQueryable&lt;T&gt; 中没有找到匹配项,它会继续查找,在IEnumerable&lt;T&gt; 上绊倒并问自己同样的问题(要点)。两个要点都会得到满足。

因此,在这种情况下,您最终会得到在 IEnumerable&lt;T&gt; 上定义的 Func&lt;T,bool&gt; 变体。

更新

Here's a dotnetfiddle example.

请注意,即使我传递了int 值(基本方法签名使用),Derived 类的double 签名也适合(因为int 隐式转换为double)并且编译器永远不会在Base 类中查找。

但是,Derived2 并非如此。由于int 不会隐式转换为string,因此在Derived2 中找不到匹配项,编译器在Base 中进一步查找并使用来自Baseint 方法。

【讨论】:

  • 现在有一个问题可能会让你有点摸不着头脑。假设 Base 有一个方法 public void M(Derived),而 Derived 有一个方法 public void M(Base)。如果您调用derived.M(derived),会调用哪个方法,Base.MDerived.M,为什么?答案可能会让您大吃一惊,但很有道理。
  • @EricLippert 按照同样的逻辑,我会说Derived.M 被调用(因为Derived 隐式转换为Base,类似于我的doubleint 示例小提琴)和that seems to hold true。根据您的评论,我开始期望它会适得其反:)
  • 大多数人忘记看接收器,只看参数;仅基于您希望调用 Base.M(Derived) 的参数,因为该参数更具体,但 C# 认为 receiver 的特异性比 参数的特异性更重要 因为您正在调用的实现在接收器内部!
【解决方案3】:

我认为 C# 规范中最有用的地方是 Anonymous Function Expressions:

匿名函数本身没有值或类型,但可以转换为兼容的委托或表达式树类型

...

在隐式类型参数列表中,参数的类型是从匿名函数发生的上下文中推断出来的——具体来说,当匿名函数转换为兼容的委托类型或表达式树类型时,该类型提供参数类型。

然后将我们带到Anonymous Function Conversions

如果 F 与委托类型 D 兼容,则 lambda 表达式 F 与表达式树类型 Expression&lt;D&gt; 兼容。请注意,这不适用于匿名方法,仅适用于 lambda 表达式。


这些是规范中的干货。不过,阅读 Eric Lippert 的 How do we ensure that type inference terminates 也很有帮助。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2012-08-01
    • 2019-03-14
    • 1970-01-01
    • 2020-09-11
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多