C# SelectMany 完全指南:从入门到精通

C# SelectMany 完全指南:从入门到精通

大家好!今天我们来深入探讨C#中一个非常强大但经常被初学者忽视的方法:SelectMany。作为一个热爱分享的程序员,我来和大家分享!

🎯 SelectMany 是什么?

简单来说,SelectMany就是处理"集合的集合"的利器。它可以把**多层嵌套的集合"拍平"**成一个单层集合。

基础概念对比

让我们先通过一个表格理解SelectSelectMany的区别:

方法 输入 → 输出 形象比喻 结果结构
Select List<A>List<B> 给每个苹果贴标签 🍎→ 🏷️ 保持原有层级
SelectMany List<List<A>>List<A> 打开多个水果篮 🧺🧺→ 🍎🍊🍌 减少嵌套层级

🔍 基础语法解析

csharp 复制代码
// SelectMany 方法签名
public static IEnumerable<TResult> SelectMany<TSource, TResult>(
    this IEnumerable<TSource> source,                    // 源集合
    Func<TSource, IEnumerable<TResult>> selector         // 选择器函数
)

// 重载版本(包含结果选择器)
public static IEnumerable<TResult> SelectMany<TSource, TCollection, TResult>(
    this IEnumerable<TSource> source,                    // 源集合
    Func<TSource, IEnumerable<TCollection>> selector,    // 集合选择器
    Func<TSource, TCollection, TResult> resultSelector   // 结果选择器
)

📚 实际应用示例

示例1:基础使用 - 展开学生和课程

csharp 复制代码
using System;
using System.Collections.Generic;
using System.Linq;

// 定义数据模型
public class Student
{
    public string Name { get; set; }
    public List<string> Courses { get; set; }  // 每个学生有多个课程
}

class Program
{
    static void Main()
    {
        // 创建测试数据
        var students = new List<Student>
        {
            new Student { Name = "张三", Courses = new List<string> { "数学", "英语", "物理" } },
            new Student { Name = "李四", Courses = new List<string> { "语文", "历史" } },
            new Student { Name = "王五", Courses = new List<string> { "化学", "生物", "地理" } }
        };

        Console.WriteLine("=== 学生选课情况 ===");
        
        // 方法1:使用 Select - 得到的是"集合的集合"
        var coursesWithSelect = students.Select(s => s.Courses);
        Console.WriteLine("\n1. 使用 Select 结果:");
        foreach (var courseList in coursesWithSelect)
        {
            // courseList 是 List<string>,需要再次遍历
            foreach (var course in courseList)
            {
                Console.WriteLine($"   - {course}");
            }
        }

        // 方法2:使用 SelectMany - 直接得到"拍平"的课程列表
        var coursesWithSelectMany = students.SelectMany(s => s.Courses);
        Console.WriteLine("\n2. 使用 SelectMany 结果:");
        foreach (var course in coursesWithSelectMany)
        {
            // 直接遍历课程字符串,不需要嵌套循环
            Console.WriteLine($"   - {course}");
        }

        // 方法3:SelectMany 带索引 - 显示学生和课程的对应关系
        var studentCourses = students.SelectMany(
            student => student.Courses,                    // 选择课程集合
            (student, course) => new                      // 结果选择器:组合学生和课程信息
            {
                StudentName = student.Name,
                CourseName = course
            }
        );

        Console.WriteLine("\n3. 学生-课程对应关系:");
        foreach (var item in studentCourses)
        {
            Console.WriteLine($"   {item.StudentName} 选择了 {item.CourseName}");
        }
    }
}

输出结果:

复制代码
=== 学生选课情况 ===

1. 使用 Select 结果:
   - 数学
   - 英语
   - 物理
   - 语文
   - 历史
   - 化学
   - 生物
   - 地理

2. 使用 SelectMany 结果:
   - 数学
   - 英语
   - 物理
   - 语文
   - 历史
   - 化学
   - 生物
   - 地理

3. 学生-课程对应关系:
   张三 选择了 数学
   张三 选择了 英语
   张三 选择了 物理
   李四 选择了 语文
   李四 选择了 历史
   王五 选择了 化学
   王五 选择了 生物
   王五 选择了 地理

示例2:EF Core 中的联表查询

csharp 复制代码
using System;
using System.Linq;
using Microsoft.EntityFrameworkCore;

// 实体类
public class Department
{
    public int DepartmentId { get; set; }
    public string DepartmentName { get; set; }
    public List<Employee> Employees { get; set; }  // 一个部门有多个员工
}

public class Employee
{
    public int EmployeeId { get; set; }
    public string EmployeeName { get; set; }
    public string Position { get; set; }
    public int DepartmentId { get; set; }
    public Department Department { get; set; }
}

public class CompanyContext : DbContext
{
    public DbSet<Department> Departments { get; set; }
    public DbSet<Employee> Employees { get; set; }
}

class Program
{
    static void Main()
    {
        using var context = new CompanyContext();
        
        Console.WriteLine("=== EF Core 中使用 SelectMany ===");

        // 场景1:获取所有部门的所有员工(扁平化)
        var allEmployees = context.Departments
            .SelectMany(dept => dept.Employees)  // 将各部门的员工集合合并成一个集合
            .ToList();

        Console.WriteLine("\n1. 公司所有员工:");
        foreach (var emp in allEmployees)
        {
            Console.WriteLine($"   - {emp.EmployeeName} ({emp.Position})");
        }

        // 场景2:使用结果选择器 - 获取员工及其部门信息
        var employeesWithDept = context.Departments
            .SelectMany(
                dept => dept.Employees,                    // 选择员工集合
                (dept, emp) => new                         // 结果选择器:组合部门和员工信息
                {
                    DepartmentName = dept.DepartmentName,
                    EmployeeName = emp.EmployeeName,
                    Position = emp.Position
                }
            )
            .ToList();

        Console.WriteLine("\n2. 员工及所属部门:");
        foreach (var item in employeesWithDept)
        {
            Console.WriteLine($"   {item.DepartmentName} - {item.EmployeeName} ({item.Position})");
        }

        // 场景3:结合 Where 条件过滤
        var managersOnly = context.Departments
            .SelectMany(
                dept => dept.Employees.Where(emp => emp.Position.Contains("经理")),
                (dept, emp) => new 
                { 
                    DepartmentName = dept.DepartmentName,
                    ManagerName = emp.EmployeeName,
                    Position = emp.Position
                }
            )
            .ToList();

        Console.WriteLine("\n3. 各部门经理:");
        foreach (var manager in managersOnly)
        {
            Console.WriteLine($"   {manager.DepartmentName}: {manager.ManagerName} ({manager.Position})");
        }
    }
}

示例3:处理多层嵌套数据

csharp 复制代码
using System;
using System.Collections.Generic;
using System.Linq;

// 多层嵌套的数据结构
public class Company
{
    public string CompanyName { get; set; }
    public List<Department> Departments { get; set; }
}

public class Department
{
    public string DeptName { get; set; }
    public List<Team> Teams { get; set; }
}

public class Team
{
    public string TeamName { get; set; }
    public List<Employee> Members { get; set; }
}

public class Employee
{
    public string Name { get; set; }
    public string Email { get; set; }
}

class Program
{
    static void Main()
    {
        // 创建测试数据
        var company = new Company
        {
            CompanyName = "科技公司",
            Departments = new List<Department>
            {
                new Department
                {
                    DeptName = "技术部",
                    Teams = new List<Team>
                    {
                        new Team
                        {
                            TeamName = "前端团队",
                            Members = new List<Employee>
                            {
                                new Employee { Name = "前端张三", Email = "zhangsan@email.com" },
                                new Employee { Name = "前端李四", Email = "lisi@email.com" }
                            }
                        },
                        new Team
                        {
                            TeamName = "后端团队", 
                            Members = new List<Employee>
                            {
                                new Employee { Name = "后端王五", Email = "wangwu@email.com" },
                                new Employee { Name = "后端赵六", Email = "zhaoliu@email.com" }
                            }
                        }
                    }
                },
                new Department
                {
                    DeptName = "市场部",
                    Teams = new List<Team>
                    {
                        new Team
                        {
                            TeamName = "营销团队",
                            Members = new List<Employee>
                            {
                                new Employee { Name = "营销钱七", Email = "qianqi@email.com" }
                            }
                        }
                    }
                }
            }
        };

        Console.WriteLine("=== 处理多层嵌套数据 ===\n");

        // 使用多个 SelectMany 展开多层嵌套
        var allEmployees = company.Departments
            .SelectMany(dept => dept.Teams)           // 第一层:部门 → 团队
            .SelectMany(team => team.Members)         // 第二层:团队 → 成员
            .ToList();

        Console.WriteLine("1. 公司所有员工:");
        foreach (var emp in allEmployees)
        {
            Console.WriteLine($"   - {emp.Name} ({emp.Email})");
        }

        // 单次查询完成多层展开(使用结果选择器保持上下文)
        var employeesWithStructure = company.Departments
            .SelectMany(
                dept => dept.Teams,                   // 部门展开为团队
                (dept, team) => new { dept, team }    // 保留部门和团队信息
            )
            .SelectMany(
                combo => combo.team.Members,          // 团队展开为成员
                (combo, employee) => new              // 组合所有信息
                {
                    Department = combo.dept.DeptName,
                    Team = combo.team.TeamName,
                    EmployeeName = employee.Name,
                    Email = employee.Email
                }
            )
            .ToList();

        Console.WriteLine("\n2. 员工完整组织架构:");
        foreach (var item in employeesWithStructure)
        {
            Console.WriteLine($"   {item.Department} -> {item.Team} -> {item.EmployeeName}");
        }
    }
}

💡 SelectMany 的高级用法

用法1:交叉连接(Cartesian Product)

csharp 复制代码
using System;
using System.Linq;

class Program
{
    static void Main()
    {
        var colors = new[] { "红", "蓝", "绿" };
        var sizes = new[] { "S", "M", "L" };

        // 使用 SelectMany 实现交叉连接
        var products = colors.SelectMany(
            color => sizes,                           // 为每个颜色匹配所有尺寸
            (color, size) => $"{color}色-{size}码"    // 组合结果
        );

        Console.WriteLine("=== 颜色和尺寸的交叉组合 ===");
        foreach (var product in products)
        {
            Console.WriteLine($"   {product}");
        }

        // 等价于嵌套循环:
        // foreach (var color in colors)
        //     foreach (var size in sizes)
        //         Console.WriteLine($"{color}色-{size}码");
    }
}

用法2:处理可能为空的集合

csharp 复制代码
using System;
using System.Collections.Generic;
using System.Linq;

class Program
{
    static void Main()
    {
        var students = new List<Student>
        {
            new Student { Name = "张三", Courses = new List<string> { "数学", "英语" } },
            new Student { Name = "李四", Courses = null },  // 课程列表为null
            new Student { Name = "王五", Courses = new List<string>() }  // 空课程列表
        };

        // 安全的 SelectMany - 处理null集合
        var allCourses = students
            .Where(s => s.Courses != null)              // 过滤掉null集合
            .SelectMany(s => s.Courses)                 // 展开课程
            .DefaultIfEmpty("无课程")                   // 如果没有课程,提供默认值
            .ToList();

        Console.WriteLine("=== 所有课程(安全处理) ===");
        foreach (var course in allCourses)
        {
            Console.WriteLine($"   - {course}");
        }

        // 更简洁的写法:使用空集合合并运算符
        var safeCourses = students
            .SelectMany(s => s.Courses ?? Enumerable.Empty<string>())
            .ToList();
    }
}

🚀 性能优化建议

  1. 与 Select 的对比 :对于单层集合,Select性能更好;对于嵌套集合,SelectMany更合适

  2. 延迟执行SelectMany也是延迟执行的,只有在遍历结果时才会真正执行

  3. 数据库查询 :在EF Core中,SelectMany会被翻译成SQL的JOIN语句,在数据库层面执行

csharp 复制代码
// EF Core 中的高效用法
var efficientQuery = context.Departments
    .Where(dept => dept.IsActive)
    .SelectMany(dept => dept.Employees.Where(emp => emp.IsActive))
    .Select(emp => new { emp.Name, emp.Salary })
    .ToList();

📊 总结

场景 推荐方法 优点
单层集合转换 Select 简单直观,性能好
嵌套集合展开 SelectMany 代码简洁,避免嵌套循环
数据库联表查询 SelectMany 生成高效SQL,减少数据库往返
复杂数据转换 SelectMany + 结果选择器 保持数据关联,灵活组合

核心要点:

  • SelectMany 是处理"集合的集合"的最佳工具
  • 它可以把多层嵌套的数据结构"拍平"成单层结构
  • 在EF Core中,它通常对应SQL的JOIN操作
  • 使用结果选择器可以保持原始数据的上下文信息

希望这篇详细的教程能帮助你彻底掌握SelectMany的用法!在实际开发中多多练习,你会发现它的强大之处。Happy coding! 🎉

相关推荐
weixin_307779136 小时前
C#程序实现将Teradata的存储过程转换为Azure Synapse Dedicated SQL pool的存储过程
数据库·数据分析·c#·云计算·azure
Aevget8 小时前
界面控件DevExpress WPF v25.2预览 - 模板工具包全新升级
c#·wpf·界面控件·devexpress·ui开发
TheWindofFate8 小时前
C# List集合
c#·list
星河队长9 小时前
C#实现智能提示输入,并增色显示
开发语言·c#
海木漄10 小时前
C# 内存是绝对自动清理吗?
开发语言·c#
我是唐青枫10 小时前
C#.NET PeriodicTimer 深入解析:高效异步定时器的正确打开方式
c#·.net
技术支持者python,php11 小时前
ModbusRtc与ModbusTCP,esp32
c#
咕白m62512 小时前
如何用 C# 将 Excel 文件转换为 HTML 格式?
c#·.net
weixin_3077791312 小时前
C#程序实现将Teradata的存储过程转换为Amazon Redshift的pgsql的存储过程
数据库·c#·云计算·运维开发·aws