CodeGym /课程 /C# SELF /SelectMany对集合进行扁平化...

SelectMany对集合进行扁平化

C# SELF
第 31 级 , 课程 4
可用

1. 引言

当你只处理“平铺”的对象列表(比如我们前面讲过的商品列表或用户列表)时,一切都很简单:过滤、转换、排序都很顺手。但在真实的业务应用、面试(甚至作业!)中,经常会遇到集合里还套着集合的情况。

比如,有个User类,里面有个订单列表。或者说,有个Product类,里面有个评论列表。现在问题来了:怎么拿到所有用户的所有订单?或者收集所有商品的所有评论?再或者,怎么拿到所有订单里的所有商品?

这种情况下,普通的Select,甚至C#的魔法都不够用了。这时候就轮到主角SelectMany登场了,它能把“列表的列表”变成一个“列表”。

厨房类比

假如你有个大盒子,里面还有小盒子,小盒子里装着饼干,你的任务是把所有饼干倒进一个大碗里。你肯定不会一个盒子一个盒子地拿出来,每次只倒一块饼干,而是直接把所有盒子里的饼干全都倒进碗里。

这就是SelectMany干的事:它把“盒子里的盒子”变成一个“碗”。

2. 回顾下应用的例子

在前面的课里,我们写了个简单的用户和商品管理应用。这节课我们要加点“嵌套”元素的集合。

假设我们有这些类:


public class User
{
    public string Name { get; set; }
    public List<Order> Orders { get; set; }
}

public class Order
{
    public int Id { get; set; }
    public List<Product> Products { get; set; }
}

public class Product
{
    public string Name { get; set; }
}

现在我们有一个用户列表,每个用户有订单列表,每个订单又有商品列表。

怎么拿到所有用户所有订单里的所有商品?

假设我们要把所有商品都列出来——不分嵌套,直接全都放进一个“碗”里。如果我们用普通的Select,会发生什么?


List<User> users = ...; // 假设数据已经有了

var allOrders = users.Select(u => u.Orders);

变量allOrders的类型会是...IEnumerable<List<Order>>。得到了“列表的列表”,也就是一堆订单集合。我们其实想要一个平铺的订单列表。

再试试这样:


var allProducts = users.Select(u => u.Orders)
                       .Select(oList => oList.Select(o => o.Products));

现在呢?现在我们得到了“列表的列表的列表”!结构反而更复杂了,没变简单。

3. 解决方案:SelectMany操作符

这就是SelectMany的威力。它的任务就是把嵌套集合“拉直”(flatten)成一个集合。

通用语法:


collection.SelectMany(item => item.内部集合)

在我们的例子里:


var allOrders = users.SelectMany(u => u.Orders);
// 类型: IEnumerable<Order>

现在我们就得到了所有用户所有订单的一个序列。

再深入点!所有用户所有订单里的所有商品:


var allProducts = users
    .SelectMany(u => u.Orders)
    .SelectMany(o => o.Products);
// 类型: IEnumerable<Product>

能不能一行写完?

当然可以:


var allProducts = users.SelectMany(u => u.Orders.SelectMany(o => o.Products));

不过这种链式写法建议分开看,尤其是你不是机器人时。

4. 大例子详细拆解

我们来造点测试数据,直观看看SelectMany怎么用。


var users = new List<User>
{
    new User
    {
        Name = "爱丽丝",
        Orders = new List<Order>
        {
            new Order
            {
                Id = 1,
                Products = new List<Product>
                {
                    new Product { Name = "饼干" },
                    new Product { Name = "牛奶" },
                }
            },
            new Order
            {
                Id = 2,
                Products = new List<Product>
                {
                    new Product { Name = "巧克力" }
                }
            }
        }
    },
    new User
    {
        Name = "鲍勃",
        Orders = new List<Order>
        {
            new Order
            {
                Id = 3,
                Products = new List<Product>
                {
                    new Product { Name = "咖啡" },
                    new Product { Name = "茶" }
                }
            }
        }
    }
};

把所有商品“倒进一个碗”


var allProducts = users
    .SelectMany(u => u.Orders)
    .SelectMany(o => o.Products);

foreach (var product in allProducts)
{
    Console.WriteLine(product.Name);
}

程序输出:

饼干
牛奶
巧克力
咖啡
茶

每一步发生了什么?

  1. users.SelectMany(u => u.Orders) — 把所有用户的订单“摊平”成一个订单集合(现在有3个订单)。
  2. .SelectMany(o => o.Products) — 把所有订单里的商品摊平。

注意:如果用普通的Select,你会得到集合的集合(比如IEnumerable<List<Product>>)。用SelectMany,你得到的是“平铺”的商品集合:IEnumerable<Product>

5. 图解

SelectMany之前


users (List<User>)
   └── User #1
   │      └── Orders (List<Order>)
   │           ├── Order #1 -> Products (List<Product>)
   │           └── Order #2 -> Products (List<Product>)
   └── User #2
          └── Orders (List<Order>)
               └── Order #3 -> Products (List<Product>)

第一次SelectMany(u => u.Orders)之后


IEnumerable<Order>: [Order #1, Order #2, Order #3]

第二次SelectMany(o => o.Products)之后


IEnumerable<Product>: [饼干, 牛奶, 巧克力, 咖啡, 茶]

6. 对比:Select vs SelectMany

Select SelectMany
返回 “集合的集合” 平铺集合
例子
users.Select(u => u.Orders)
IEnumerable<List<Order>>
users.SelectMany(u => u.Orders)
IEnumerable<Order>
适用场景 需要保留嵌套结构时 需要一个平铺序列时

表格:什么时候用哪个操作符

需求 用什么 结果
得到列表的列表(不扁平化) Select
IEnumerable<ICollection<T>>
得到一个“平铺”的序列 SelectMany
IEnumerable<T>
得到“父-子”对 SelectMany带结果选择器
IEnumerable<CustomType>
只转换“平铺”集合的元素 Select
IEnumerable<TransformedType>

7. 有用的小细节

Query Syntax语法(喜欢“SQL风格”的同学)

如果你喜欢表达力强的写法——LINQ还支持类似SQL的查询语法:


var allProducts = from user in users
                  from order in user.Orders
                  from product in order.Products
                  select product;

这段代码和SelectMany链式写法效果一样,只是看起来像多层“from”。

“扁平化”矩阵

SelectMany的经典用法——处理二维数组或列表的列表。


List<List<int>> matrix = new List<List<int>>
{
    new List<int> { 1, 2, 3 },
    new List<int> { 4, 5 },
    new List<int> { 6 }
};

怎么得到所有数字的一个列表?


var flat = matrix.SelectMany(row => row);
foreach (var value in flat)
{
    Console.Write(value + " "); // 1 2 3 4 5 6
}

8. 进阶语法

有时候,用SelectMany的重载,可以返回内部集合元素的同时,还能带上外部的信息。

比如:要得到每个订单里每个商品的“用户名—商品名”对。


var userProductPairs = users.SelectMany(
    user => user.Orders.SelectMany(
        order => order.Products,
        (order, product) => new { UserName = user.Name, ProductName = product.Name }
    )
);

foreach (var pair in userProductPairs)
{
    Console.WriteLine($"{pair.UserName} 订购了 {pair.ProductName}");
}

这里lambda的第二个参数就是外部集合的元素。这样就不会“丢失”外层的信息了。

9. 实用场景

很多订单,很多商品

你是电商开发者。想统计所有客户买过多少种不同的商品?这样写:


var uniqueProductNames = users
    .SelectMany(user => user.Orders)
    .SelectMany(order => order.Products)
    .Select(product => product.Name)
    .Distinct();

foreach (var name in uniqueProductNames)
{
    Console.WriteLine(name);
}

从层级结构生成平铺集合

你有一个公司部门列表,每个部门有员工。想得到全公司的员工列表:


var allEmployees = departments.SelectMany(d => d.Employees);

字符串处理:把单词数组“扁平化”为字符


string[] words = { "hello", "world" };
var allChars = words.SelectMany(w => w.ToCharArray());
foreach (var c in allChars)
{
    Console.Write(c + " "); // h e l l o w o r l d
}

10. 常见错误和细节

现在你已经被SelectMany的威力圈粉了,是时候提醒你几个常见的坑。

最常见的——以为Select能得到“平铺”集合。实际上它只要内部函数返回集合,就一定返回集合的集合。结果你得写双重循环,或者类型搞混。

第二个坑——没注意到“扁平化”会丢失外层信息:比如你只处理商品时,如果不用带结果选择器的重载,你就不知道这个商品属于哪个订单或哪个用户了。

第三个细节——如果你的内部列表可能是null,用SelectMany前一定要加保护:要么过滤,要么替换成空列表,否则你会比按F5还快遇到NullReferenceException

1
调查/小测验
LINQ入门第 31 级,课程 4
不可用
LINQ入门
LINQ基础:简单查询和数据过滤
评论
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION