【问题标题】:Querying nested lists with LINQ instead of loops使用 LINQ 而不是循环查询嵌套列表
【发布时间】:2019-06-18 13:45:24
【问题描述】:

假设我有以下设置

大陆
--国家
----省份
------城市

一个大陆包含许多国家的列表,其中包含许多省份的列表,其中包含许多城市的列表。 对于每个嵌套列表,假设我要进行检查(名称长度大于 5)

而不是使用这种循环结构

var countries = dbSet.Countries.Where(c => c.Name.Length > 5);
foreach (var country in countries)
{
   country.Provinces = country.Provinces.Where(p => p.Name.Length > 5);
   foreach (var province in country.Provinces)
   {
      province.Cities = province.Cities.Where(ci => ci.Name.Length() > 5);
   }
}

如何使用 LINQ 高效地完成同样的任务?

【问题讨论】:

  • 把它变成 linq 的可读性会降低
  • 不要求可读性,我想了解此类查询的基本概念,因为在其他场景中使用嵌套循环效率不高。
  • 如果你想有更好的性能,我建议使用for循环。 [for loop vs link] (stackoverflow.com/questions/14893924/…) 参考这个
  • 谢谢,但我正在尝试理解 LINQ 方法

标签: c# entity-framework linq


【解决方案1】:

有效吗?就书面代码而言,当然,但我们称其为“干净”。在执行方面,这不是你现在应该问的问题。专注于用可以理解的代码完成工作,然后“race your horses”看看你是否真的需要改进它。

我应该注意的一件事是 LINQ 是关于查询的,它不会改变源序列。您将过滤后的序列分配回属性,这与 LINQ 原则相反。该标签显示您正在使用实体框架,因此这样做绝对不是一个好主意,因为它在后台使用了自己的集合类型。

为了回答您的问题,SelectMany 扩展方法在投影序列上循环。当它转换为数据库查询时,它会转换为连接。

dbSet.Countries
    .Where(c => c.Names.Length > 5)
    .SelectMany(c => c.Provinces)
    .Where(p => p.Name.Length > 5)
    .SelectMany(p => p.Cities)
    .Where(ci => ci.Name.Length > 5)
    .Select(ci => ci.Name);

这将为您提供国家、省和城市名称都超过 5 个字符的所有城市的名称。

但这只会给你城市的名称。如果您想了解每个级别的信息,则很难使用扩展方法,因为您必须在每个步骤中都投射“透明标识符”,并且可能会变得非常混乱。让编译器使用 LINQ 语法为您完成这项工作。

from c in dbSet.Countries
where c.Name.Length > 5
from p in c.Provinces
where p.Name.Length > 5
from ci in p.Cities
where ci.Name.Length > 5

这将执行与上述相同的操作,除了现在,所有范围变量都通过表达式进行,因此您可以这样做:

select new
{
    CountryName = c.Name,
    ProvinceName = p.Name,
    CityName = ci.Name
};

...或任何您想对cpci 执行的操作。


编辑: 将解决 cmets 中问题的第二个答案合并到这个答案中。

为了通过查询保留父级别,您需要在每次循环遍历子对象集合时为父级和子级投影一个容器。当您使用 LINQ 语法时,编译器会以“透明标识符”的形式为您执行此操作。它是透明的,因为您对范围变量的引用“直接通过”它,而您永远看不到它。 Jon Skeet 在Reimplementing LINQ to Objects: Part 19 – Join 结尾处触及了它们。

为了实现这一点,这次你想使用不同的重载 SelectMany,它也需要一个 lambda 来投影你需要的容器。通过子项的每次迭代,都会调用该 lambda 并传递两个参数,即父项和当前迭代的子项。

var result = dbSet.Countries
    .Where(c => c.Names.Length > 5)
    .SelectMany(c => c.Provinces, (c, p) => new { c, p })
    .Where(x1 => x1.p.Name.Length > 5)
    .SelectMany(x1 => x1.p.Cities, (x1, ci) => new { x1.c, x1.p, ci })
    .Where(x2 => x2.ci.Name.Length > 5)
    .Select(x2 => new
    {
        Country = x2.c,
        Province = x2.p,
        City = x2.ci
    })
    .ToList();

x1x2 lambda 参数是从前面的 SelectMany 调用投影的容器。我喜欢称它们为“不透明的标识符”。如果您明确引用它们,它们将不再透明。

cpci 范围变量现在是这些容器的属性。

作为额外说明,当您使用let clause 时,编译器会做同样的事情,创建一个包含所有可用范围变量的容器引入的新变量。

我想用一句忠告来结束这一切:尽可能多地使用 LINQ 语法。它更容易编写和正确,也更容易阅读,因为您没有编译器可以为您做的所有预测。如果您必须求助于扩展方法,请分部分进行。这两种技术可以混合使用。防止它看起来一团糟是一种艺术。

【讨论】:

  • 太棒了,一切都说得通了,最后我可以选择一个新大陆吗?
  • 您的原始代码没有明确显示大陆,所以我从国家开始。如果dbSet 是您的大陆收藏,您需要稍微调整一下,但是是的,您应该可以这样做。
  • 仔细观察,dbSet 看起来像是一个大陆,而不是它们的集合。您需要提供更多信息,但我怀疑您现在知道的足够多,可以在层次结构中再上一层。
  • 所以我很好奇如果我想使用 selectmany 方法但保留父级别需要做什么。感谢您的解释,这非常有帮助
猜你喜欢
  • 1970-01-01
  • 2017-11-29
  • 2021-06-12
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2018-01-06
  • 2017-09-06
  • 1970-01-01
相关资源
最近更新 更多