Linq 通过复合键过滤列表(包含和不包含)

Linq filtering a list by a composite key (contains and not contains)

我需要了解如何通过条件为“包含”和“不包含”的复合键过滤列表。

我有一个列表

List<Entity>();

其中实体是 class

public class Entity
{
    int id;
    string docNum;
    int docVersion;
}

有一个类似的第二个列表。我需要获取列表中符合以下条件的那些元素: "获取第一个列表的元素,与第二个列表的文档编号相同,相同文档编号的文档版本不同"。

我尝试将 Contains()Select() 结合使用,例如:

firstList
    .Where(x => secondList.Select(y => y.docNum).Contains(x.docNum))
    .Where(x => !secondList.Select(y => y.docVersion).Contains(x.docVersion))

但恐怕没有考虑到我也需要比较文件编号。

在我看来,这应该通过 GroupBy()ToDictionary() 来解决,但我找不到解决方案。

如果可能的话,请展示一个关于标准 linq 元素的解决方案示例,因为我使用 NHibernate

如果我明白你想要什么,应该这样做:

var results = list1
    .Where(l1 => list2.Select(l2 => l2.DocNum).Contains(l1.DocNum))
    .GroupJoin(list2, l1 => l1.DocNum, l2 => l2.DocNum, (e, c) => new KeyValuePair<string, IEnumerable<Entity>>(e.DocNum, c.Prepend(e)))
    .ToArray();

var results = list1
    .GroupJoin(list2, l1 => l1.DocNum, l2 => l2.DocNum, (e, c) => new KeyValuePair<string, IEnumerable<Entity>>(e.DocNum, c.Prepend(e)))
    .Where(x => x.Value.Count() > 1)
    .ToArray();

然后您可以进一步 select 从结果组中获取您需要的实体,也许是获取版本最大的实体,或者所有实体...

不幸的是我从未使用过 NHibernate 所以我不确定是否支持它,也许你需要先 select 来自数据库,枚举它们,然后在软件端执行此操作...

根据您的要求,使用 LINQ 查询语法可能更容易做到这一点,您所做的方式和我发布的解决方案仍然是 LINQ,只是实现方式不同。

List<Entity> firstList = new List<Entity>
        {
            new Entity
            {
                id = 1,
                docNum = "two",
                docVersion = 3
            },
             new Entity
            {
                id = 2,
                docNum = "four",
                docVersion = 4
            },
              new Entity
            {
                id = 3,
                docNum = "ten",
                docVersion = 6
            }
        };
        List<Entity> ListTwo = new List<Entity>
        {
            new Entity
            {
                id = 1,
                docNum = "two",
                docVersion = 5
            },
             new Entity
            {
                id = 2,
                docNum = "eight",
                docVersion = 7
            },
              new Entity
            {
                id = 3,
                docNum = "ten",
                docVersion = 5
            }
        };
        var resultList = from list in firstList
                         from list2 in ListTwo
                         where list.docNum == list2.docNum
                         where list.docVersion != list2.docVersion
                         select list;
  1. 首先,我们创建了两个列表,我们的结果列表将从中创建。这些是 firstListListTwo

  2. 然后声明一个 var 并将我们的 LINQ 查询结果赋值给它

  3. from list in firstList from list2 in ListTwo 用于 select 我们的两个数据存储,我们给一个名称来引用这些数据存储中的单个元素 listlist2

  4. where list.docNum == list2.docNum where list.docVersion != list2.docVersion用来满足我们的条件。第一个 where 子句检查 listlist2 中的 docNum 是否匹配。然后,第二个 where 子句检查以确保 list1 和 `list2``` 中的 docVersion 不相等。

  5. select list 用于 select 匹配我们 where 子句的项目。编辑 如果您需要从两个来源匹配的实体select list 更改为 select new {list,list2}

如果您要调试此代码,您会看到resultList 有两个值(见图)。

您可以使用 Any() 而不是 Select().Contains() 进行更复杂的比较。例如,如果组合键存在于 secondList 中,则过滤如下:

.Where(x => !secondList.Any(y => y.docNum == x.docNum && y.docVersion == x.docVersion)

在这种情况下,这比 Select().Contains() 稍微更有效,因为没有中间对象构造。在中间对象更复杂的某些情况下,节省会增加。

我建议您也使用 Any() 作为第一个条件,例如:

firstList
.Where(x => x.Any(y => y.docNum == x.docNum))
.Where(x => !secondList.Any(y => y.docNum == x.docNum && y.docVersion == x.docVersion)

第一个 Where() 将过滤掉列表中没有匹配 docNum 的任何项目,第二个将过滤剩余的项目以删除具有完全复合匹配的项目第二个列表。

当然这是非常低效的,因为你必须扫描 secondList 两次 - 部分扫描,但仍然是两次扫描。最好查找以文档编号为键的版本号,并使用它来进行更有效的过滤:

var lu = secondList.ToLookup(y => y.docNum, y => y.docVersion);

firstList.Where
(
    x => 
    { 
        var l = lu[x.docNum]; 
        return l.Any() && !l.Any(v => v == x.docVersion);
    }
)

由于 ILookup<> 得到了相当好的优化,因此大大减少了执行过滤器所需的实际时间。使用一些非常简单的(主要是)随机列表生成和相当高的冲突,我测试了对 1,000,000 个项目的列表过滤 10,000 个项目,看看有什么不同。第一个选项 49 秒,查找 1.8 秒。

也就是说,这实际上只适用于 IEnumerable<>。如果您在 IQueryable<> 上操作,则坚持使用 Any() 和良好的指数。