目 录CONTENT

文章目录

CSharp(五十二) LINQ 投影与筛选详解

C# LINQ 投影与筛选详解


一、投影和筛选做什么?

1.1 生活比喻

筛选(Where) 就像是:

你有一堆学生的档案袋,现在只想看"分数大于 60 分的"——把不合格的挑出去,这就是筛选。

投影(Select) 就像是:

筛选完后,你不想看每个学生的全部信息,只想看姓名和分数——从每个档案袋里只抽出姓名和分数,这就是投影。

1.2 一句话理解

筛选 = Where = 过滤数据,符合条件的留下,不符合的丢掉
投影 = Select = 改变形状,从每个元素中只抽取你关心的字段

1.3 它俩的关系——先筛选,再投影

// 标准流程:先筛后投
students
    .Where(s => s.Score >= 60)     // 1. 筛选:只要及格的
    .Select(s => new { s.Name, s.Score });  // 2. 投影:只看姓名和分数

二、准备测试数据

以下所有示例都基于这份数据:

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

public class Student
{
    public string Name;
    public int Age;
    public int Score;
    public string City;
    public string Subject;

    public override string ToString()
    {
        return $"{Name,-6} | {Age}岁 | {City,-4} | {Subject,-4} | {Score}分";
    }
}

List<Student> students = new List<Student>
{
    new Student { Name = "张三", Age = 18, Score = 92, City = "北京", Subject = "数学" },
    new Student { Name = "李四", Age = 19, Score = 85, City = "上海", Subject = "数学" },
    new Student { Name = "王五", Age = 18, Score = 76, City = "北京", Subject = "语文" },
    new Student { Name = "赵六", Age = 20, Score = 58, City = "广州", Subject = "数学" },
    new Student { Name = "孙七", Age = 19, Score = 88, City = "上海", Subject = "语文" },
    new Student { Name = "周八", Age = 20, Score = 95, City = "北京", Subject = "英语" },
    new Student { Name = "吴九", Age = 18, Score = 45, City = "广州", Subject = "英语" },
    new Student { Name = "郑十", Age = 19, Score = 72, City = "深圳", Subject = "数学" },
};

第一部分:筛选(Where)


三、Where —— 按条件过滤

3.1 基本用法

// 找出所有及格的
var passed = students.Where(s => s.Score >= 60);

foreach (var s in passed)
    Console.WriteLine(s);

输出:

张三    | 18岁 | 北京  | 数学  | 92分
李四    | 19岁 | 上海  | 数学  | 85分
王五    | 18岁 | 北京  | 语文  | 76分
孙七    | 19岁 | 上海  | 语文  | 88分
周八    | 20岁 | 北京  | 英语  | 95分
郑十    | 19岁 | 深圳  | 数学  | 72分

Where 的工作方式

数据源:    [张三92] [李四85] [王五76] [赵六58] [孙七88] [周八95] [吴九45] [郑十72]
             ↓         ↓         ↓                  ↓         ↓                  ↓
             ✅        ✅        ✅       ❌        ✅        ✅       ❌        ✅
结果:      [张三92] [李四85] [王五76]         [孙七88] [周八95]         [郑十72]

3.2 各种筛选条件

// 简单条件
var fromBeijing = students.Where(s => s.City == "北京");

// 多条件(且)
var result = students.Where(s => s.City == "北京" && s.Score >= 80);

// 多条件(或)
var mathOrEnglish = students.Where(s => s.Subject == "数学" || s.Subject == "英语");

// 不等于
var notBeijing = students.Where(s => s.City != "北京");

// 范围
var topHalf = students.Where(s => s.Score >= 70 && s.Score < 90);

// 集合包含
string[] cities = { "北京", "深圳" };
var inCities = students.Where(s => cities.Contains(s.City));

// 字符串条件
var starts = students.Where(s => s.Name.StartsWith("张"));
var contains = students.Where(s => s.Subject.Contains("语"));

3.3 带索引的 Where

Lambda 可以接收第二个参数——元素的索引位置:

// 只在前 5 个学生中找及格的
var first5Passed = students.Where((s, index) => s.Score >= 60 && index < 5);

// 取奇数位置的学生
var oddIndex = students.Where((s, i) => i % 2 == 1);

3.4 Where 的查询语法写法

// 方法语法
var passed = students.Where(s => s.Score >= 60);

// 查询语法
var passed = from s in students
             where s.Score >= 60
             select s;

// 两种写法完全等价

第二部分:投影(Select / SelectMany)


四、Select —— 一对一投影

4.1 基本用法——取单个字段

// 从 Student 对象中取 Name
IEnumerable<string> names = students.Select(s => s.Name);

foreach (string name in names)
    Console.Write($"{name} ");
// 输出: 张三 李四 王五 赵六 孙七 周八 吴九 郑十

// 取其他字段
var scores = students.Select(s => s.Score);
var cities = students.Select(s => s.City);

4.2 投影成匿名对象——取多个字段

// 把 Student 投影成一个轻量级的匿名对象
var summaries = students.Select(s => new
{
    s.Name,
    s.Score,
    Grade = s.Score >= 90 ? "A" : s.Score >= 80 ? "B" : s.Score >= 60 ? "C" : "D"
});

foreach (var item in summaries)
{
    Console.WriteLine($"{item.Name}: {item.Score}分 → {item.Grade}");
}

输出:

张三: 92分 → A
李四: 85分 → B
王五: 76分 → C
赵六: 58分 → D
孙七: 88分 → B
周八: 95分 → A
吴九: 45分 → D
郑十: 72分 → C

4.3 投影成不同目标类型

// 投影成匿名对象(最常用)
var anon = students.Select(s => new { s.Name, s.Score });

// 投影成元组(C# 7.0+)
var tuple = students.Select(s => (s.Name, s.Score));

// 投影成 KeyValuePair
var kvp = students.Select(s => new KeyValuePair<string, int>(s.Name, s.Score));

// 投影成字符串
var lines = students.Select(s => $"{s.Name}考了{s.Score}分");

// 投影成自定义类
var newStudents = students.Select(s => new StudentSummary
{
    FullName = s.Name,
    TotalScore = s.Score
});

4.4 带索引的 Select

// 给每个学生加上序号
var ranked = students.Select((s, index) => new
{
    Number = index + 1,
    s.Name,
    s.Score
});

foreach (var item in ranked)
{
    Console.WriteLine($"第{item.Number}名: {item.Name} {item.Score}分");
}

输出:

第1名: 张三 92分
第2名: 李四 85分
第3名: 王五 76分
第4名: 赵六 58分
第5名: 孙七 88分
第6名: 周八 95分
第7名: 吴九 45分
第8名: 郑十 72分

4.5 查询语法中的 Select

// 方法语法
var names = students.Select(s => s.Name);

// 查询语法
var names = from s in students
            select s.Name;

// 查询语法投影匿名对象
var summaries = from s in students
                select new
                {
                    s.Name,
                    s.Score,
                    Grade = s.Score >= 90 ? "A" : s.Score >= 60 ? "C" : "D"
                };

五、SelectMany —— 一对多扁平化投影

5.1 什么是 SelectMany?

  • Select:一对一。一个学生 → 一个结果。
  • SelectMany:一对多。一个学生 → 多个结果,然后全部摊平。

打个比喻:

每个学生修了多门课。Select 只能返回每个学生的"第一门课",SelectMany 能把所有学生的所有课"铺平"成一张大表。

5.2 测试数据

public class StudentWithCourses
{
    public string Name;
    public List<CourseScore> Courses;
}

public class CourseScore
{
    public string CourseName;
    public int Score;
}

List<StudentWithCourses> studentList = new List<StudentWithCourses>
{
    new StudentWithCourses
    {
        Name = "张三",
        Courses = new List<CourseScore>
        {
            new CourseScore { CourseName = "数学", Score = 92 },
            new CourseScore { CourseName = "语文", Score = 85 },
            new CourseScore { CourseName = "英语", Score = 78 },
        }
    },
    new StudentWithCourses
    {
        Name = "李四",
        Courses = new List<CourseScore>
        {
            new CourseScore { CourseName = "数学", Score = 56 },
            new CourseScore { CourseName = "物理", Score = 72 },
        }
    },
};

5.3 基本用法——把嵌套铺平

// 所有学生的所有课程,变成 5 条记录
var allCourses = studentList.SelectMany(s => s.Courses);

foreach (var c in allCourses)
{
    Console.WriteLine($"{c.CourseName}: {c.Score}分");
}

输出:

数学: 92分
语文: 85分
英语: 78分
数学: 56分
物理: 72分

图解 SelectMany 做了什么:

张三 → [数学92, 语文85, 英语78]
李四 → [数学56, 物理72]

↓ SelectMany 摊平 ↓

[数学92, 语文85, 英语78, 数学56, 物理72]  ← 一条平铺的大列表

5.4 带结果选择器——保留父信息

// 保留学生名字 + 课程信息
var flat = studentList.SelectMany(
    s => s.Courses,                // 1. 摊平什么
    (student, course) => new       // 2. 每对怎么组合
    {
        student.Name,
        course.CourseName,
        course.Score
    }
);

foreach (var item in flat)
{
    Console.WriteLine($"{item.Name} - {item.CourseName}: {item.Score}分");
}

输出:

张三 - 数学: 92分
张三 - 语文: 85分
张三 - 英语: 78分
李四 - 数学: 56分
李四 - 物理: 72分

5.5 查询语法中的 SelectMany(多 from)

// 查询语法:第二个 from 就是 SelectMany
var flat = from s in studentList
           from c in s.Courses
           select new { s.Name, c.CourseName, c.Score };

// 等价于方法语法
var flat = studentList.SelectMany(
    s => s.Courses,
    (s, c) => new { s.Name, c.CourseName, c.Score }
);

六、Select vs SelectMany —— 核心对比

操作 输入 输出 比喻
Select 一个学生 一个结果 打开档案袋,拿出姓名卡
SelectMany 一个学生 多个结果 打开档案袋,把每门课的成绩单都铺在桌上
// Select:8 个学生 → 8 个名字
students.Select(s => s.Name)      // 8 条结果

// SelectMany:2 个学生 × (3门 + 2门) → 5 门课
studentList.SelectMany(s => s.Courses)  // 5 条结果

第三部分:投影与筛选的组合使用


七、Where + Select —— 先筛后投

这是 LINQ 最标准的组合用法:

7.1 基本组合——筛完再投

// 先筛选,再投影
var result = students
    .Where(s => s.Score >= 60)          // 1. 筛选:只要及格的
    .Select(s => new { s.Name, s.Score });  // 2. 投影:只要姓名和分数

foreach (var item in result)
    Console.WriteLine($"{item.Name}: {item.Score}分");

输出:

张三: 92分
李四: 85分
王五: 76分
孙七: 88分
周八: 95分
郑十: 72分

7.2 完整流程——筛选 + 投影 + 排序 + 分区

// 需求:找出北京的及格学生,按分数降序,只要姓名和分数,取前 2
var top2 = students
    .Where(s => s.City == "北京" && s.Score >= 60)    // 筛选
    .OrderByDescending(s => s.Score)                   // 排序
    .Select(s => new { s.Name, s.Score })              // 投影
    .Take(2);                                          // 取前2

foreach (var item in top2)
    Console.WriteLine($"{item.Name}: {item.Score}分");
// 输出: 周八: 95分  张三: 92分

7.3 查询语法——先筛后投

// 方法语法
var result = students
    .Where(s => s.Score >= 60)
    .Select(s => new { s.Name, s.Score });

// 查询语法(一条语句写完,更直观)
var result = from s in students
             where s.Score >= 60
             select new { s.Name, s.Score };

7.4 筛选后投影成不同格式

// 筛选+投影成字符串
var lines = students
    .Where(s => s.City == "北京")
    .Select(s => $"{s.Name} ({s.Subject}) - {s.Score}分");

foreach (var line in lines)
    Console.WriteLine(line);
// 输出:
// 张三 (数学) - 92分
// 王五 (语文) - 76分
// 周八 (英语) - 95分

// 筛选+投影成元组
var tuples = students
    .Where(s => s.Score >= 85)
    .Select(s => (s.Name, s.Score, s.City));

// 筛选+投影成 KeyValuePair
var kvps = students
    .Where(s => s.Score < 60)
    .Select(s => new KeyValuePair<string, int>(s.Name, s.Score));

7.5 先投后筛——Select 在前,Where 在后

// 先投影成匿名对象,再根据匿名对象的属性筛选
var result = students
    .Select(s => new
    {
        s.Name,
        s.Score,
        Grade = s.Score >= 90 ? "A" : s.Score >= 80 ? "B" : s.Score >= 60 ? "C" : "D"
    })
    .Where(x => x.Grade == "A");   // 根据投影后的字段筛选

foreach (var item in result)
    Console.WriteLine($"{item.Name}: {item.Score}分");
// 输出: 张三: 92分  周八: 95分

八、Where + SelectMany —— 筛选嵌套数据

// 找出所有不及格的课程(嵌套数据中筛选)
var failedCourses = studentList
    .SelectMany(s => s.Courses)         // 摊平
    .Where(c => c.Score < 60);          // 筛选不及格的

foreach (var c in failedCourses)
    Console.WriteLine($"{c.CourseName}: {c.Score}分");
// 输出: 数学: 56分

// 查询语法写法
var failedCourses = from s in studentList
                    from c in s.Courses           // SelectMany
                    where c.Score < 60             // Where
                    select c;

九、Where 中的三种筛选模式对比

9.1 单条件筛选

// 最简单
students.Where(s => s.Score >= 60)

9.2 多条件 AND 筛选

// 方案一:&& 连接
students.Where(s => s.City == "北京" && s.Score >= 80 && s.Age == 18)

// 方案二:多个 Where 串联(效果相同)
students
    .Where(s => s.City == "北京")
    .Where(s => s.Score >= 80)
    .Where(s => s.Age == 18)

结论:多个 Where 串联和 && 效果一样,但链式更易读。

9.3 OR 条件筛选

// 用 || 连接
students.Where(s => s.Subject == "数学" || s.Subject == "英语")

// 或对集合用 Contains
string[] subjects = { "数学", "英语" };
students.Where(s => subjects.Contains(s.Subject))

十、完整实战示例——学生查询系统

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

class Program
{
    static void Main()
    {
        List<Student> students = /* ... 测试数据 ... */;

        Console.WriteLine("========== 学生查询系统 ==========\n");

        // ===== 1. 按条件筛选 =====
        Console.WriteLine("【1. 北京的及格学生】");
        var beijingPassed = students
            .Where(s => s.City == "北京" && s.Score >= 60)
            .Select(s => new { s.Name, s.Subject, s.Score });

        foreach (var s in beijingPassed)
            Console.WriteLine($"  {s.Name} - {s.Subject} - {s.Score}分");

        // ===== 2. 光荣榜 =====
        Console.WriteLine("\n【2. 光荣榜(≥85分)】");
        var honorRoll = students
            .Where(s => s.Score >= 85)
            .OrderByDescending(s => s.Score)
            .Select((s, i) => new
            {
                Rank = i + 1,
                s.Name,
                s.City,
                s.Subject,
                s.Score,
                Medal = s.Score >= 90 ? "🏅" : "⭐"
            });

        foreach (var item in honorRoll)
        {
            Console.WriteLine($"  {item.Medal} 第{item.Rank}名: {item.Name} "
                + $"({item.City} - {item.Subject}) - {item.Score}分");
        }

        // ===== 3. 各级别统计 =====
        Console.WriteLine("\n【3. 各级别人数统计】");

        Console.WriteLine($"  优秀(≥90): {students.Count(s => s.Score >= 90)}人");
        Console.WriteLine($"  良好(80-89): {students.Count(s => s.Score >= 80 && s.Score < 90)}人");
        Console.WriteLine($"  及格(60-79): {students.Count(s => s.Score >= 60 && s.Score < 80)}人");
        Console.WriteLine($"  不及格(<60): {students.Count(s => s.Score < 60)}人");

        // ===== 4. 不及格学生列表 =====
        Console.WriteLine("\n【4. 不及格学生】");
        var failed = students
            .Where(s => s.Score < 60)
            .Select(s => new
            {
                s.Name,
                s.Subject,
                s.Score,
                Gap = 60 - s.Score
            });

        foreach (var s in failed)
        {
            Console.WriteLine($"  {s.Name} - {s.Subject} - {s.Score}分 (差{s.Gap}分)");
        }

        // ===== 5. 各科目成绩一览 =====
        Console.WriteLine("\n【5. 数学成绩(按分数降序)】");
        var mathRank = students
            .Where(s => s.Subject == "数学")
            .OrderByDescending(s => s.Score)
            .Select((s, i) => new { Rank = i + 1, s.Name, s.City, s.Score });

        Console.WriteLine($"  {"排名",-4} {"姓名",-6} {"城市",-6} {"分数"}");
        Console.WriteLine($"  {new string('-', 25)}");
        foreach (var item in mathRank)
        {
            Console.WriteLine($"  {item.Rank,-4} {item.Name,-6} {item.City,-6} {item.Score}");
        }
    }
}

输出:

========== 学生查询系统 ==========

【1. 北京的及格学生】
  张三 - 数学 - 92分
  王五 - 语文 - 76分
  周八 - 英语 - 95分

【2. 光荣榜(≥85分)】
  🏅 第1名: 周八 (北京 - 英语) - 95分
  🏅 第2名: 张三 (北京 - 数学) - 92分
  ⭐ 第3名: 孙七 (上海 - 语文) - 88分
  ⭐ 第4名: 李四 (上海 - 数学) - 85分

【3. 各级别人数统计】
  优秀(≥90): 2人
  良好(80-89): 2人
  及格(60-79): 2人
  不及格(<60): 2人

【4. 不及格学生】
  赵六 - 数学 - 58分 (差2分)
  吴九 - 英语 - 45分 (差15分)

【5. 数学成绩(按分数降序)】
  排名   姓名   城市   分数
  -------------------------
  1    张三   北京   92
  2    李四   上海   85
  3    郑十   深圳   72
  4    赵六   广州   58

十一、常见易错点(避坑指南)

坑1:Where 和 Select 都是延迟执行

// ❌ 错误理解:以为这行就"做"了
students.Where(s => s.Score >= 60);
// 实际上什么都没发生!只是定义了一个查询计划

// ✅ 要遍历或者 ToList() 才真正执行
var result = students.Where(s => s.Score >= 60).ToList();

坑2:Select 不改原集合

// ❌ 错误做法:想通过 Select 修改原集合
students.Select(s => { s.Score += 10; return s; }).ToList();
Console.WriteLine(students[0].Score);  // 没变!Select 是投影,不是修改

// ✅ 正确做法:Select 产生新序列
var modified = students.Select(s => new Student
{
    Name = s.Name,
    Score = s.Score + 10  // 新对象的 Score + 10
});

坑3:Where 写 >= 还是 > 要想清楚

// 需求:找及格的(60 分算及格)
// ✅ 正确的
students.Where(s => s.Score >= 60);   // 60 分也包含

// ❌ 错误的
students.Where(s => s.Score > 60);    // 60 分被排除了!

坑4:Where 后面直接调 First 但集合可能为空

// ❌ 可能抛异常
var first = students.Where(s => s.Score > 100).First();  // 没找到,抛异常!

// ✅ 用 FirstOrDefault
var first = students.Where(s => s.Score > 100).FirstOrDefault();
if (first == null)
    Console.WriteLine("没找到 >100 分的学生");

// ✅ 或者先判断
if (students.Any(s => s.Score > 100))
{
    var first = students.First(s => s.Score > 100);
}

坑5:SelectMany 中集合为 null

// ❌ 如果 Courses 是 null,抛异常
var all = studentList.SelectMany(s => s.Courses);

// ✅ 给默认值
var all = studentList.SelectMany(s => s.Courses ?? new List<CourseScore>());

坑6:多个 Where 的顺序

// 这两个结果一样,但性能可能不同(大数据时)
students.Where(s => s.City == "北京").Where(s => s.Score >= 80);

// 第一个条件如果能把大部分数据筛掉,后面的条件就可以少处理很多数据
// LINQ 会优化这个,但理解这一点有帮助

坑7:Select 后再 Select

// ✅ 可以多次 Select,每层改变形状
var result = students
    .Select(s => new { s.Name, s.City, s.Score })     // 第一层投影
    .Where(x => x.Score >= 60)
    .Select(x => $"{x.Name}({x.City}): {x.Score}分");  // 第二层投影

foreach (var r in result)
    Console.WriteLine(r);

坑8:Where 中使用了被修改的变量

int threshold = 60;
var query = students.Where(s => s.Score >= threshold);

threshold = 90;  // 改了阈值

// 对延迟执行的查询来说,遍历时才取 threshold 的值!
var result = query.ToList();  // 这里用到的 threshold 已经是 90 了!
Console.WriteLine(result.Count);  // 可能和预期不一样

// ✅ 如果要在延迟执行中"锁定"值,先拿到局部变量
int threshold = 60;
int captured = threshold;
var query = students.Where(s => s.Score >= captured);
// 或直接 ToList() 立即执行

十二、总结

筛选速查(Where)

操作 语法 说明
基本筛选 .Where(s => s.Score >= 60) 符合条件的留下
多条件 AND .Where(s => s.Age >= 18 && s.Score >= 60)
多条件 OR .Where(s => s.City == "北京" || s.City == "上海")
不等 .Where(s => s.City != "北京") 不等于
范围 .Where(s => s.Score >= 60 && s.Score <= 100) 区间
集合包含 .Where(s => names.Contains(s.Name)) 在列表中
带索引 .Where((s, i) => s.Score >= 60 && i < 5) 带位置过滤
字符串 .Where(s => s.Name.StartsWith("张")) 字符串条件

投影速查(Select / SelectMany)

操作 语法 说明
取单字段 .Select(s => s.Name) 变成简单类型
取多字段 .Select(s => new { s.Name, s.Score }) 匿名对象
带计算 .Select(s => new { s.Name, Grade = ... }) 计算新字段
带索引 .Select((s, i) => new { i, s.Name }) 带序号
扁平化 .SelectMany(s => s.Courses) 嵌套变平铺
扁平+保留父信息 .SelectMany(s => s.Courses, (s, c) => ...) 保留关联

标准组合套路

数据源 ──→ Where(筛选) ──→ OrderBy(排序) ──→ Select(投影) ──→ Take(取前N) ──→ ToList(执行)

例子:
students
    .Where(s => s.City == "北京")           // 1. 只要北京
    .OrderByDescending(s => s.Score)         // 2. 按分数降序
    .Select(s => new { s.Name, s.Score })    // 3. 只要姓名和分数
    .Take(3)                                 // 4. 取前3
    .ToList();                               // 5. 执行

记忆口诀

Where 筛选过一遍,条件成立才留下
多条件用 && 与,|| 是或者任选一

Select 投影换形状,每个元素变模样
匿名对象最常用,想要什么就取什么

SelectMany 拍扁它,嵌套集合全摊开
先筛后投是套路,再排再取一步完

一句话总结Where 是"把不合要求的挑出去"——按条件过滤数据;Select 是"把每个元素变个样"——从原数据中抽取你关心的字段。标准流程:先 Where 筛选 → 再 OrderBy 排序 → 最后 Select 投影 → ToList() 执行,一步不落。

0

评论区