LINQ:SelectMany

你想掌握 C# LINQ 中的 SelectMany 方法,我会从核心作用、使用场景、代码示例到和 Select 的对比,帮你彻底理解这个高频且易混淆的方法。

核心含义先厘清

SelectMany 的核心作用是:将“集合的集合”扁平化(拆包)为一维集合,同时可以按需投影元素。简单说,它能把「每个元素对应一个子集合」的结构,转换成「所有子集合元素合并后的单一集合」。

举个通俗例子:

  • 有 3 个宠物主人,每个主人有 2 只宠物 → Select 会返回「3 个主人,每个主人带自己的宠物列表」(3 个元素,每个元素是列表);
  • SelectMany 会返回「所有宠物的扁平列表」(6 个元素,直接是宠物对象)。

一、SelectMany 基础用法(核心场景)

先定义基础数据(贴合你之前的“宠物主人+宠物”场景):

using System;
using System.Collections.Generic;
using System.Linq;

// 宠物类
public class Pet
{
    public string Name { get; set; }
    public string Type { get; set; } // 宠物类型(猫/狗)
}

// 宠物主人类
public class PetOwner
{
    public string Name { get; set; } // 主人姓名
    public List<Pet> Pets { get; set; } // 主人的宠物列表(子集合)
}

// 测试数据
List<PetOwner> owners = new()
{
    new PetOwner 
    { 
        Name = "张三", 
        Pets = new List<Pet> { new Pet { Name = "咪咪", Type = "猫" }, new Pet { Name = "旺财", Type = "狗" } } 
    },
    new PetOwner 
    { 
        Name = "李四", 
        Pets = new List<Pet> { new Pet { Name = "啾啾", Type = "鸟" }, new Pet { Name = "来福", Type = "狗" } } 
    },
    new PetOwner 
    { 
        Name = "王五", 
        Pets = new List<Pet>() // 无宠物
    }
};

1. 基础扁平化:提取所有宠物

将每个主人的 Pets 子集合合并为一个扁平的宠物列表:

// SelectMany:拆分子集合,返回所有宠物的扁平列表
IEnumerable<Pet> allPets = owners.SelectMany(owner => owner.Pets);

// 遍历结果(共4个宠物)
foreach (var pet in allPets)
{
    Console.WriteLine($"宠物:{pet.Name}(类型:{pet.Type})");
}

输出结果

宠物:咪咪(类型:猫)
宠物:旺财(类型:狗)
宠物:啾啾(类型:鸟)
宠物:来福(类型:狗)

2. 带投影:关联父元素(主人+宠物)

SelectMany 支持第二个参数,能同时访问「父元素(主人)」和「子元素(宠物)」,投影成自定义结构:

// SelectMany:拆分子集合,同时关联主人信息
var ownerPetDetails = owners.SelectMany(
    owner => owner.Pets, // 第一步:指定要扁平化的子集合
    (owner, pet) => new  // 第二步:投影(主人+宠物的组合信息)
    {
        OwnerName = owner.Name,
        PetName = pet.Name,
        PetType = pet.Type
    });

// 遍历结果
foreach (var item in ownerPetDetails)
{
    Console.WriteLine($"主人:{item.OwnerName} → 宠物:{item.PetName}({item.PetType})");
}

输出结果

主人:张三 → 宠物:咪咪(猫)
主人:张三 → 宠物:旺财(狗)
主人:李四 → 宠物:啾啾(鸟)
主人:李四 → 宠物:来福(狗)

二、SelectMany 进阶用法

1. 结合 Where 过滤(先过滤再扁平化)

需求:只提取“狗”类型的宠物,并关联主人信息:

var dogPets = owners.SelectMany(
    owner => owner.Pets.Where(pet => pet.Type == "狗"), // 先过滤子集合(只留狗)
    (owner, pet) => new { OwnerName = owner.Name, PetName = pet.Name }
);

foreach (var item in dogPets)
{
    Console.WriteLine($"主人:{item.OwnerName} → 狗:{item.PetName}");
}

输出结果

主人:张三 → 狗:旺财
主人:李四 → 狗:来福

2. 结合 GroupBy:按宠物类型分组(贴合你之前的分组需求)

需求:先扁平化所有宠物(关联主人),再按宠物类型分组,统计每个类型的宠物及对应的主人:

var petTypeGroups = owners.SelectMany(
        owner => owner.Pets,
        (owner, pet) => new { owner.Name, pet.Name, pet.Type }
    )
    .GroupBy(x => x.Type) // 按宠物类型分组
    .Select(g => new
    {
        PetType = g.Key,
        Count = g.Count(),
        Pets = g.Select(x => $"{x.Name}(主人:{x.Name})").ToList()
    });

// 遍历分组结果
foreach (var group in petTypeGroups)
{
    Console.WriteLine($"宠物类型:{group.PetType}(共{group.Count}只)");
    foreach (var pet in group.Pets)
    {
        Console.WriteLine($"  - {pet}");
    }
}

输出结果

宠物类型:猫(共1只)
  - 咪咪(主人:张三)
宠物类型:狗(共2只)
  - 旺财(主人:张三)
  - 来福(主人:李四)
宠物类型:鸟(共1只)
  - 啾啾(主人:李四)

3. 处理空集合(无宠物的主人)

SelectMany 会自动忽略空的子集合(如王五无宠物,不会产生任何元素),无需额外处理空值。

三、SelectMany vs Select(核心对比)

这是最易混淆的点,用表格和示例清晰区分:

方法 作用 返回结果结构 示例返回元素数
Select 投影每个元素为新值(含子集合) 外层集合(每个元素是子集合) 3 个(3 个主人,每个带宠物列表)
SelectMany 扁平化子集合为一维集合 一维集合(子集合元素) 4 个(所有宠物)

对比示例:

// Select:返回3个元素,每个元素是主人的宠物列表
var selectResult = owners.Select(owner => owner.Pets);
Console.WriteLine($"Select 返回元素数:{selectResult.Count()}"); // 3

// SelectMany:返回4个元素,所有宠物的扁平列表
var selectManyResult = owners.SelectMany(owner => owner.Pets);
Console.WriteLine($"SelectMany 返回元素数:{selectManyResult.Count()}"); // 4

四、SelectMany 在 LINQ to SQL/EF Core 中的使用

SelectMany 也能用于数据库查询(如关联一对多关系),会被翻译成 SQL 的 JOIN 语句,实现“主表+子表”的扁平查询:

示例(EF Core):

// 数据库上下文(EF Core)
public class PetDbContext : DbContext
{
    public DbSet<PetOwner> PetOwners { get; set; }
    public DbSet<Pet> Pets { get; set; }
}

// LINQ to SQL/EF Core 查询:关联主人和宠物,扁平化结果
using (var db = new PetDbContext())
{
    var ownerPetQuery = db.PetOwners
        .SelectMany(
            owner => owner.Pets, // 关联宠物子表
            (owner, pet) => new { owner.Name, pet.Name, pet.Type }
        );

    // 执行查询(生成JOIN SQL),返回扁平结果
    var result = ownerPetQuery.ToList();
}

对应的 SQL 核心逻辑:

SELECT o.Name, p.Name, p.Type
FROM PetOwners o
LEFT JOIN Pets p ON o.Id = p.OwnerId

总结

  1. SelectMany 核心是扁平化集合的集合:把「每个元素对应子集合」的结构,转为「所有子集合元素的一维列表」;
  2. 关键用法:
    • 基础用法:SelectMany(父元素 => 子集合) → 提取所有子元素;
    • 进阶用法:SelectMany(父元素 => 子集合, (父, 子) => 投影结构) → 关联父/子元素;
  3. 核心区别:Select 保留层级,SelectMany 扁平化层级;
  4. 适用场景:一对多关系的扁平查询、合并多个子集合、关联父/子元素的自定义投影。

记住一句话:只要需要“拆包”集合的集合,优先用 SelectMany;只要需要保留层级投影,用 Select

posted @ 2025-12-25 21:43  【唐】三三  阅读(34)  评论(0)    收藏  举报