有效吗?就书面代码而言,当然,但我们称其为“干净”。在执行方面,这不是你现在应该问的问题。专注于用可以理解的代码完成工作,然后“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
};
...或任何您想对c、p 和ci 执行的操作。
编辑: 将解决 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();
x1 和 x2 lambda 参数是从前面的 SelectMany 调用投影的容器。我喜欢称它们为“不透明的标识符”。如果您明确引用它们,它们将不再透明。
c、p 和 ci 范围变量现在是这些容器的属性。
作为额外说明,当您使用let clause 时,编译器会做同样的事情,创建一个包含所有可用范围变量的容器和引入的新变量。
我想用一句忠告来结束这一切:尽可能多地使用 LINQ 语法。它更容易编写和正确,也更容易阅读,因为您没有编译器可以为您做的所有预测。如果您必须求助于扩展方法,请分部分进行。这两种技术可以混合使用。防止它看起来一团糟是一种艺术。