带有 lambda 表达式的 LINQ where 子句具有 OR 子句和返回不完整结果的空值

Posted

技术标签:

【中文标题】带有 lambda 表达式的 LINQ where 子句具有 OR 子句和返回不完整结果的空值【英文标题】:LINQ where clause with lambda expression having OR clauses and null values returning incomplete results 【发布时间】:2011-07-16 00:05:02 【问题描述】:

简而言之问题

我们在 Where 子句中使用了一个 lambda 表达式,它没有返回“预期”的结果。

快速总结

在 analysisObjectRepository 对象中,有一些对象在名为 Parent 的属性中也包含父关系。我们正在查询这个 analysisObjectRepository 以返回一些对象。

详情

下面的代码应该做的是,返回包含 ID 值的特定对象的根、第一个子项(直接子项)和孙子项。

在下面的代码中,常识表明,使 3 个单独的 OR 条件中的任何一个为真的所有结果都应与结果一样返回。

List<AnalysisObject> analysisObjects = 
    analysisObjectRepository
        .FindAll()
        .Where(x => x.ID               == packageId ||
                    x.Parent.ID        == packageId || 
                    x.Parent.Parent.ID == packageId)
        .ToList();

但上面的代码只返回子孙,而不返回根对象(父值为空),这使得

x.ID == packageId

条件为真。

只有构成第二个的对象

x.Parent.ID == packageId

第三个

x.Parent.Parent.ID == packageId

子句被返回。

如果我们只用下面的代码编写返回根对象的代码,它就会被返回,所以我们完全确定 analysisObjectRepository 包含所有对象

List<AnalysisObject> analysisObjects = 
    analysisObjectRepository
        .FindAll()
        .Where(x => x.ID == packageId )
        .ToList();

但是,当我们将其重写为委托时,我们会得到预期的结果,返回所有预期的对象。

List<AnalysisObject> analysisObjects = 
    analysisObjectRepository
        .FindAll()
        .Where(delegate(AnalysisObject x) 
         
            return 
              (x.ID == packageId) || 
              (x.Parent != null && x.Parent.ID == packageId) || 
                  (x.Parent != null && 
                   x.Parent.Parent != null && 
                   x.Parent.Parent.ID == packageId); )
        .ToList();

问题

我们是否在 lambda 表达式中遗漏了什么?这是一个非常简单的 3 部分 OR 条件,我们认为应该返回使三个条件中的任何一个为真的任何对象。我们怀疑具有 null Parent 值的根对象可能会导致问题,但无法准确解决。

任何帮助都会很棒。

【问题讨论】:

analysisObjectRepositoryList&lt;AnalysisObject&gt; 吗?如果是这样,则无需调用FindAll 这是什么?无论是使用 LINQ-to-Objects 还是 IQueryable 的自定义实现,都可以使 all 有所不同。 你能把它减少到一个显示错误行为的对象吗?出现这种情况时x.ID的值和packageId的值是多少? 【参考方案1】:

您的第二个委托不是以匿名委托(而不是 lambda)格式重写的第一个委托。看看你的条件。

第一:

x.ID == packageId || x.Parent.ID == packageId || x.Parent.Parent.ID == packageId

第二:

(x.ID == packageId) || (x.Parent != null && x.Parent.ID == packageId) || 
(x.Parent != null && x.Parent.Parent != null && x.Parent.Parent.ID == packageId)

对任何x 的 lambda 调用都会引发异常,其中 ID 不匹配且父级为 null 或不匹配且祖父级为 null。将空检查复制到 lambda 中,它应该可以正常工作。

对问题发表评论后编辑

如果你的原始对象不是List&lt;T&gt;,那么我们无法知道FindAll()的返回类型是什么,以及它是否实现了IQueryable接口。如果是这样,那么这很可能解释了这种差异。因为 lambda 可以在编译时转换为 Expression&lt;Func&lt;T&gt;&gt;但匿名委托不能,所以您可能在使用 lambda 版本时使用 IQueryable 的实现,但在使用匿名委托版本。

这也可以解释为什么您的 lambda 不会导致 NullReferenceException。如果您将该 lambda 表达式传递给实现 IEnumerable&lt;T&gt; IQueryable&lt;T&gt; 的东西,则 lambda 的运行时评估(这与其他方法没有什么不同,无论是否匿名)将抛出一个 @ 987654332@ 第一次遇到ID 不等于目标且父或祖父为空的对象。

添加于 2011 年 3 月 16 日上午 8:29 EDT

考虑以下简单示例:

IQueryable<MyObject> source = ...; // some object that implements IQueryable<MyObject>

var anonymousMethod =  source.Where(delegate(MyObject o)  return o.Name == "Adam"; );    
var expressionLambda = source.Where(o => o.Name == "Adam");

这两种方法产生完全不同的结果。

第一个查询是简单版本。匿名方法产生一个委托,然后传递给IEnumerable&lt;MyObject&gt;.Where 扩展方法,其中source 的全部内容将根据您的委托进行检查(使用普通编译代码手动在内存中)。换句话说,如果您熟悉 C# 中的迭代器块,则可以这样做:

public IEnumerable<MyObject> MyWhere(IEnumerable<MyObject> dataSource, Func<MyObject, bool> predicate)

    foreach(MyObject item in dataSource)
    
        if(predicate(item)) yield return item;
    

这里的重点是您实际上是在客户端执行过滤内存。例如,如果您的源是某个 SQL ORM,则查询中将没有 WHERE 子句;整个结果集将被带回客户端并在那里过滤。

使用 lambda 表达式的第二个查询被转换为 Expression&lt;Func&lt;MyObject, bool&gt;&gt; 并使用 IQueryable&lt;MyObject&gt;.Where() 扩展方法。这会产生一个也键入为IQueryable&lt;MyObject&gt; 的对象。所有这些都通过将 表达式 传递给底层提供者来实现。 这就是为什么您没有收到NullReferenceException。完全取决于查询提供者如何将表达式(而不是它可以调用的实际编译函数,是使用对象的表达式的 逻辑 的表示)转换成它的东西可以用。

查看区别(或至少存在)区别的一种简单方法是在 lambda 中调用 AsEnumerable() 之前调用 AsEnumerable()版本。这将强制您的代码使用 LINQ-to-Objects(这意味着它像匿名委托版本一样在 IEnumerable&lt;T&gt; 上运行,而不是像当前的 lambda 版本那样在 IQueryable&lt;T&gt; 上运行),并且您将按预期得到异常。

TL;DR 版本

总而言之,您的 lambda 表达式正在被转换为针对您的数据源的某种查询,而匿名方法版本正在评估内存中的整个数据源。将 lambda 转换为查询的任何操作都不能代表您所期望的逻辑,这就是为什么它没有产生您所期望的结果。

【讨论】:

lambda 表达式执行完美,但只返回不完整的结果。存在具有不匹配 ID 值和 null 父值的对象。 Adam 您的评论阐明了这个问题,因为您已经猜到它实现了 IQueryable 接口。我们已经尝试了在 lambda 表达式中工作的委托的完全匹配,但它仍然没有以“预期”的方式工作,并且它没有抛出 NullReferenceException。我们不想在这个问题上浪费时间,我们将在这部分使用代表,但它为什么不能以“预期”的方式工作仍然是一个谜。 @Cihan:它不会抛出异常,因为 lambda(在 this 情况下)和匿名方法的工作方式完全不同。查看我的编辑。 很明显,它们以完全不同的方式工作。我们必须深入挖掘提供程序类结构,以找到导致这种看似“奇怪”​​行为的实现细微差别。感谢您的支持。【参考方案2】:

尝试使用与委托相同的条件编写 lambda。像这样:

  List<AnalysisObject> analysisObjects = 
    analysisObjectRepository.FindAll().Where(
    (x => 
       (x.ID == packageId)
    || (x.Parent != null && x.Parent.ID == packageId)
    || (x.Parent != null && x.Parent.Parent != null && x.Parent.Parent.ID == packageId)
    ).ToList();

【讨论】:

我们已经尝试过了,但它没有以“预期”的方式工作。【参考方案3】:

您正在检查代理中的 Parent 属性是否为 null。这同样适用于 lambda 表达式。

List<AnalysisObject> analysisObjects = analysisObjectRepository
        .FindAll()
        .Where(x => 
            (x.ID == packageId) || 
            (x.Parent != null &&
                (x.Parent.ID == packageId || 
                (x.Parent.Parent != null && x.Parent.Parent.ID == packageId)))
        .ToList();

【讨论】:

我们已经尝试过了,但没有成功。 lambda 表达式不应该处理这个问题吗? @yaqari:为什么 lambda 表达式会处理空值检查? 我想说 lambda 表达式没有抛出异常,并且第一个 OR 条件为真,因此 lambda 表达式应该“处理”并返回正确的结果。 如果没有空检查,当x.ID != packegeId &amp;&amp; x.Parent == null 时它将失败。

以上是关于带有 lambda 表达式的 LINQ where 子句具有 OR 子句和返回不完整结果的空值的主要内容,如果未能解决你的问题,请参考以下文章

如何使用 linq lambda 扩展方法执行带有 where 子句的左外连接

如何为CriteriaOperator过滤对象转换为lambda表达式,即:linq to xpo的动态where语句

错误:对于带有 from 的 Linq,“表达式树 lambda 可能不包含空传播运算符”

如何将带有内连接的 sql 查询转换为 linq lambda 表达式?

在 lambda 表达式中检查 null - linq

Java 8:带有 where 条件的 Lambda 表达式