C# SelectMany 完全指南:从入门到精通
C# SelectMany 完全指南:从入门到精通
大家好!今天我们来深入探讨C#中一个非常强大但经常被初学者忽视的方法:SelectMany。作为一个热爱分享的程序员,我来和大家分享!
🎯 SelectMany 是什么?
简单来说,SelectMany就是处理"集合的集合"的利器。它可以把**多层嵌套的集合"拍平"**成一个单层集合。
基础概念对比
让我们先通过一个表格理解Select和SelectMany的区别:
| 方法 | 输入 → 输出 | 形象比喻 | 结果结构 |
|---|---|---|---|
| Select | List<A> → List<B> | 给每个苹果贴标签 🍎→ 🏷️ | 保持原有层级 |
| SelectMany | List<List<A>> → List<A> | 打开多个水果篮 🧺🧺→ 🍎🍊🍌 | 减少嵌套层级 |
🔍 基础语法解析
// 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:基础使用 - 展开学生和课程
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 中的联表查询
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:处理多层嵌套数据
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)
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:处理可能为空的集合
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 }, // 课程列表为nullnew 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();}
}
🚀 性能优化建议
-
与 Select 的对比:对于单层集合,
Select性能更好;对于嵌套集合,SelectMany更合适 -
延迟执行:
SelectMany也是延迟执行的,只有在遍历结果时才会真正执行 -
数据库查询:在EF Core中,
SelectMany会被翻译成SQL的JOIN语句,在数据库层面执行
// 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! 🎉
