筛选与投影
本教程共 100 篇 · 第 69 篇 · 更新于 2026-07-31 · 约 10 分钟阅读
69. 筛选与投影
本节目标:学完你能用
Where按条件挑数据,用Select把它映射成想要的任意形状,并知道匿名类型、元组、record 在投影时该怎么选。
筛选(filtering)和投影(projection)是 LINQ 里最高频的两个操作。一个负责「挑哪些」,一个负责「变成啥」。本章把它们讲透,并补齐适用场景、对比和坑。
Where:按条件筛选
Where 接收一个「谓词(predicate)」——即返回 bool 的 lambda。为 true 的元素留下,为 false 的丢弃。
List<Student> students =
[
new Student(1, "小明", 90),
new Student(2, "小红", 85),
new Student(3, "小刚", 90),
];
var top = students.Where(s => s.Score >= 90);
foreach (var s in top)
{
Console.WriteLine(s.Name);
}
class Student
{
public int Id { get; }
public string Name { get; }
public double Score { get; }
public Student(int id, string name, double score)
{
Id = id; Name = name; Score = score;
}
}
输出:
小明
小刚
s => s.Score >= 90 就是谓词。s 是每个学生的代表,返回「是否优秀」。
Tip谓词里可以写任意复杂的条件。多条件用
&&、||连接:s => s.Score >= 60 && s.Name.Length > 1。也可以用!取反,比如「不及格的」写成s => s.Score < 60。
用到元素下标:Where 的索引重载
Where 还有一个重载,lambda 的第二个参数是元素在序列里的下标(index)。比如「只要偶数位置的学生」:
var alternate = students.Where((s, i) => i % 2 == 0);
foreach (var s in alternate)
{
Console.WriteLine(s.Name);
}
输出:
小明
小刚
i 从 0 开始,i % 2 == 0 挑出下标 0、2……也就是第 1、3 个。这个重载在「按位置取样」「隔一个取一个」时很方便,无需自己维护计数器。
组合多个条件
筛选条件可以用方法串起来,也可以写在同一个 lambda 里。下面只挑「90 分且名字含『小』」:
var result = students
.Where(s => s.Score >= 90)
.Where(s => s.Name.Contains("小"));
foreach (var s in result)
{
Console.WriteLine(s.Name);
}
输出:小明 小刚(两人都含「小」)。拆成两个 Where 更易读,也可合并成一个 &&。
Note串多个
Where和写在一个 lambda 里用&&结果相同。多个Where会多做几层枚举,但 LINQ 是延迟执行、流式处理,性能差异通常可忽略。可读性优先:条件多且各自独立时,拆开更清楚。
Select:映射到新数据
Select 把每个元素「变成」另一个东西。最简单是提取某个字段:
var names = students.Select(s => s.Name);
foreach (var name in names)
{
Console.WriteLine(name);
}
输出:
小明
小红
小刚
这里每个 Student 被「投影」成了 string(名字)。元素数量不变,类型变了。
投影成计算后的值
Select 后面可以是任意表达式。比如把分数换算成百分制等级:
var levels = students.Select(s => s.Score >= 90 ? "优秀" : "良好");
foreach (var lv in levels)
{
Console.WriteLine(lv);
}
输出:优秀 良好 优秀。每个元素经过一次计算,映射到新值。
匿名类型投影
最常用的是「取几个字段、临时组合」。C# 的匿名类型(anonymous type)用 new { ... } 现场造一个只有属性的小类型,连名字都不用起:
var cards = students.Select(s => new
{
s.Name,
Grade = s.Score >= 90 ? "优秀" : "良好"
});
foreach (var c in cards)
{
Console.WriteLine($"{c.Name}:{c.Grade}");
}
输出:
小明:优秀
小红:良好
小刚:优秀
c.Name、c.Grade 是编译器自动生成的属性。匿名类型适合「只在局部用一下」的场景,省去专门定义类。
用元组做轻量投影
除了匿名类型,C# 还可用**元组(tuple / ValueTuple)**做投影,写法是圆括号加字段名:
var pairs = students.Select(s => (s.Name, s.Score));
foreach (var p in pairs)
{
Console.WriteLine($"{p.Name} 考了 {p.Score} 分");
}
输出:
小明 考了 90 分
小红 考了 85 分
小刚 考了 90 分
匿名类型和元组都适合「临时取几个字段」,区别在:元组是值类型、字段名写在括号里,跨方法返回时类型写法更轻;匿名类型属性只读、由编译器生成。想「有名字、可比较、可跨方法传」的长久结构,还是用正式 record 或 class(第 49 章)。
Note三者怎么选:局部临时用→匿名类型或元组皆可;需要值相等比较、语义清晰→用
record;要挂方法或长期复用→用class。别为了一次性投影硬去定义一个完整类,那是过度设计。
筛选与投影连用
真实代码里两者常一起出现:先挑,再变。
var report = students
.Where(s => s.Score >= 85)
.Select(s => new { s.Name, s.Score });
foreach (var r in report)
{
Console.WriteLine($"{r.Name} 考了 {r.Score} 分");
}
输出:
小明 考了 90 分
小红 考了 85 分
小刚 考了 90 分
Tip顺序上通常先
Where后Select:先缩小数据量,再做映射,效率更好。虽然 LINQ 是延迟执行,但少算一点总没错。
适用场景:何时用 Where / Select
- 用
Where:从一堆数据里「按规则留子集」——查及格的、查名字带某字的、查价格区间内的商品。凡是「过滤」都找它。 - 用
Select:需要「换一种形态输出」——只取名字列表、把实体转成给前端的 DTO、把分数算成等级。凡是「变形」都找它。 - 两者连用:最常见于「先过滤、再整形」的报表/列表场景。
Warning
Select是「一进一出」的一对一映射。如果你要的是「一个元素展开成多个」(比如每个订单展开成多件商品),Select做不到——它仍是单序列。那种一对多要用SelectMany(进阶章会讲)。别在Select里返回集合又期望被自动摊平,那样你会得到「集合的集合」。
与其它操作对比
| 操作 | 输入→输出数量 | 用途 |
|---|---|---|
Where | 同量(只减不增) | 按条件过滤 |
Select | 同量(一对一变形) | 投影/映射 |
SelectMany | 一对多展开 | 扁平化嵌套序列 |
TakeWhile | 从头取到条件破 | 条件性截取(与 Where 不同:TakeWhile 一破就停) |
注意 Where 和 TakeWhile 的差别:Where 扫描全部、留下所有满足条件的;TakeWhile 从头开始,一旦某元素不满足条件就立刻停止,后面不再看。按需求选对工具。
新手容易踩的坑
第一,Where 的 lambda 必须返回 bool。写成 s => s.Score(返回数字)会编译报错,因为那不是谓词。
第二,匿名类型只在当前方法内有用。想返回给别的方法,得换成具名类型,否则类型推断会失效。
第三,误以为 Select 会改原集合。Select 只产出新序列,原始 students 一点没变。LINQ 默认不修改数据源。
第四,Select 会「拉平」成单序列。若要一对多展开(如每个订单展开成多个商品),得用 SelectMany,那是进阶话题。
第五,忘记 Where/Select 也是延迟执行。只写不遍历,什么都不会发生;同一查询反复遍历会重复计算,必要时 ToList() 缓存。
常见疑问解答
问:Where 和 if 在循环里筛选有什么区别?
if 要你手写循环、手动把命中的元素塞进新集合;Where 一句话描述规则、返回可枚举序列,少写样板且不易错。Where 底层其实也是遍历,但意图更清楚。
问:Select 之后元素的顺序会变吗?
不会。Select 保持原顺序,只做「逐个变形」。想重排顺序请用 OrderBy(下一章)。
问:匿名类型和元组哪个更好?
没有绝对好坏。局部、只读、字段少,用哪个都行;匿名类型从 C# 早期就有、生态成熟;元组写法更短、是值类型(少一次堆分配)。跨方法返回且想明确类型名时,优先 record。
小结
Where 用谓词筛选元素,Select 把元素映射成新形状,匿名类型/元组让「取部分字段」零成本。两者配合,几乎能表达任何「取数 + 变形」的需求。记住先 Where 后 Select 更高效,也别把 Select 当成能展开集合的万能钥匙。下一章看排序、分组与去重。