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()执行,一步不落。
评论区