CodeGym /课程 /C# SELF /合并多个来源: SelectMany 在...

合并多个来源: SelectMany 在LINQ里的用法

C# SELF
第 33 级 , 课程 2
可用

1. 入门

回忆一下普通的 Select 是怎么工作的:

var names = users.Select(user => user.Name);

每个用户都会被映射成他的名字——得到一个名字列表。很简单!那如果我们想拿到所有用户的所有订单呢?

var ordersList = users.Select(user => user.Orders);

这里会发生什么?每个用户我们都会拿到订单列表。也就是说,结果会是一个集合的集合(IEnumerable<List<Order>>)。就像一排盒子:你要想拿到每个订单,就得手动一个个打开盒子(用户的订单列表)。

我们其实想把所有东西都“拆包”,直接像操作普通订单列表那样用它。这时候 SelectMany 就派上用场了。

类比:拆盒子

想象一下,你所有的东西不是混在一起的,而是放在箱子里。每个箱子就是一个集合,比如某个用户的购物清单。如果你想一次性看到所有东西(比如做个大盘点),你不会先翻所有箱子,再翻里面的东西,一遍又一遍。更方便的做法是把所有箱子的内容倒出来,堆成一大堆——然后再慢慢挑。这就是 SelectMany 的工作方式。

2. 什么是 SelectMany

SelectMany 是LINQ里的一个方法,它能把集合的集合变成一个“扁平”的集合。这里的“扁平”意思是,结果序列里不会再有内部列表——所有嵌套集合里的元素都会被拉到同一级,就像把它们“拽”出来一样。没有套娃,只有一串长长的值。

方法签名长这样:


IEnumerable<TResult> SelectMany<TSource, TResult>(
    this IEnumerable<TSource> source,
    Func<TSource, IEnumerable<TResult>> selector
)

它的工作方式很简单:你有一个集合(比如用户列表),每个元素里面又有另一个集合(比如这个用户的订单列表)。SelectMany 会把每个用户的订单都拿出来,拼成一个统一的订单流——没有“外壳”,没有嵌套。用在你想把“列表的列表”变成普通列表的时候,特别方便。

3. 简单例子

例子1:用户列表和他们的订单

我们从应用的模型开始,这个模型我们之前课上已经写过了:

// 假设这些类是前面课程里已经有的
class User
{
    public string Name { get; set; }
    public List<Order> Orders { get; set; }
}

class Order
{
    public int Id { get; set; }
    public string ProductName { get; set; }
}

假设有个用户列表:

var users = new List<User>
{
    new User
    {
        Name = "伊万",
        Orders = new List<Order>
        {
            new Order { Id = 1, ProductName = "书" },
            new Order { Id = 2, ProductName = "笔" }
        }
    },
    new User
    {
        Name = "玛丽亚",
        Orders = new List<Order>
        {
            new Order { Id = 3, ProductName = "笔记本电脑" }
        }
    }
};

任务:拿到所有用户买过的所有商品的列表。

普通 Select

var ordersPerUser = users.Select(user => user.Orders);
// IEnumerable<List<Order>>

这样出来的结果是一堆“盒子”,每个盒子里是某个用户的订单列表。

SelectMany

var allOrders = users.SelectMany(user => user.Orders);
// IEnumerable<Order>

现在我们拿到的是一个“长长的”所有订单的集合!

可视化

用户 订单
伊万 书, 笔
玛丽亚 笔记本电脑
  • Select之后: [[书, 笔], [笔记本电脑]] —— 列表的列表
  • SelectMany之后: [书, 笔, 笔记本电脑] —— 一个列表

例子2:多层嵌套的操作

比如说,一个订单里还有商品列表(购物车),那 SelectMany 可以用好几次——就像拆套娃一样!

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

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

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

var users = new List<User>
{
    new User
    {
        Name = "彼得",
        Orders = new List<Order>
        {
            new Order
            {
                Id = 10,
                Products = new List<Product>
                {
                    new Product { Name = "手机" },
                    new Product { Name = "充电器" }
                }
            }
        }
    }
};

要拿到所有用户所有订单里的所有商品,用两次 SelectMany

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

这样你先拆一层“盒子”,再拆下一层。

例子3:在Query Syntax里用 SelectMany

如果你更喜欢SQL风格的LINQ(Query Syntax),那 SelectMany 就是连续写几个 from

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

这里每个 from 都是从上一个返回的集合里拿元素。


users
  └─ user1
        └─ order1 
             └─ product1, product2
        └─ order2
             └─ product3
  └─ user2
        └─ order3
             └─ product4
SelectMany 的嵌套集合结构图

Query Syntax 会遍历所有路径,返回一个长长的商品列表。

4. 用法和细节

SelectMany做转换和过滤

SelectMany 不只能简单拆包,还能同时做过滤或投影。比如说,从所有订单里只选出价格大于 1000 欧元的商品:

var expensiveProducts = users
    .SelectMany(u => u.Orders)
    .SelectMany(o => o.Products)
    .Where(p => p.Price > 1000);

或者——直接在 SelectMany 里加点逻辑(用嵌套selector):

var expensiveProducts = users
    .SelectMany(u => u.Orders.SelectMany(o => o.Products))
    .Where(p => p.Price > 1000);

怎么写都行:这两种写法都是把问题拆成几个连续的步骤。

用带投影的SelectMany重载

还有个“进阶”重载,可以一边拆包一边直接生成结果。比如:拿到“用户名—商品名”对儿:

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

怎么理解?第一个参数——user => user.Orders.SelectMany(order => order.Products)——是把用户的所有商品都拆出来,第二个参数是在内部enumerator里把原始用户对象和商品组合起来。

5. 重点和常见错误

有时候你会遇到这种情况:忘了加 SelectMany,结果不是扁平序列而是一堆“盒子”。比如你用了普通的 Select,但其实应该用 SelectMany 来一次性拆所有元素。这样你就没法直接遍历或处理元素了。

嵌套多的时候很容易搞混,尤其是嵌套层级多的情况。记住一条:如果你想要的不是列表的列表,而是扁平列表——就用 SelectMany

实际场景会用到哪里?

只要你有对象集合,每个元素又有自己的集合(比如“用户—订单”,“课程—学生”,“新闻—评论”),你就会用到 SelectMany。面试时这也是个“常考点”:很多初学者分不清 SelectSelectMany,也不会正确拆集合。

在实际项目里,这让你的代码又简洁又快,没有多余的嵌套层。在.NET平台上,这就是处理“多层集合”的标准、优化方式。

SelectSelectMany 的区别

Select 方法会保留结构:你对有订单列表的用户集合用它,结果还是集合的集合——比如 [[A, B], [C]]。如果你想按组(比如按用户)继续处理数据,这很方便。

SelectMany 会“拍扁”——把所有嵌套集合合成一个总序列:[A, B, C]。如果你不关心分组,只想把所有内部元素当成一坨来处理(比如找所有商品,不管属于哪个用户或订单),这就很有用。


+---------------------+         +--------------------+
| users               |         | allOrders          |
+---------------------+         +--------------------+
| 伊万: [A, B]        | --+   +-> Order A           |
| 玛丽亚: [C]         | --+ | +-> Order B           |
+---------------------+   | | +-> Order C           |
                          | | +--------------------+
     Select:              | |
   [[A, B], [C]] <--------+ +
                          |
     SelectMany: <--------+
   [A, B, C]
SelectSelectMany 区别的结构图

简单说:

  • Select → “给我每个用户的订单列表”;
  • SelectMany → “直接给我所有订单”。
2
任务
C# SELF, 第 33 级, 课程 2
已锁定
使用 SelectMany 合并集合
使用 SelectMany 合并集合
评论
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION