Skip to Content
Week 02Day 2 - LINQ 基础

Day 2 - LINQ 基础

建议用时:180-210 分钟

你将学会什么

  • LINQ 是什么,为什么它不是新的集合类型
  • Lambda 表达式 x => ... 怎么读
  • WhereSelectOrderBy 分别做什么
  • AnyAllFirstOrDefault 怎么用
  • ToList 为什么经常出现在 LINQ 结尾
  • 延迟执行是什么意思
  • 如何把循环筛选改写成 LINQ 流水线
  • 如何写完整的商品筛选、排序、输出程序

LINQ 的重点不是背方法名,而是读懂这条线:原始集合 -> 筛选 -> 排序 -> 转换 -> 得到结果。

本页固定顺序

  1. 先学第一部分:弄懂今天最小、最重要的知识,并运行短例子。
  2. 再学第二部分:把刚学的知识组合成一个完整例子。
  3. 然后做第三部分:自己跟着敲,再完成重复训练和每日小测。
  4. 最后做第四部分:先独立完成作业,再用完整答案检查。

学习衔接

上一页学习的是“集合体系”,今天继续学习“LINQ 基础”。先使用上一页已经会的写法,再只增加今天这个新知识点;如果前置内容还不能独立敲出,先回上一页复习,不要硬跳。

今天的最低通过线

第一次学习不要求背完整页。完成下面 3 项,就可以继续:

  • 能用自己的话说明“LINQ 基础”解决什么问题。
  • 把第一部分的短例子亲手敲完,并确认每个例子都能运行。
  • 不看完整答案完成第三部分至少前 3 个例子,再主动改一个值观察结果。

第一部分:先学原理和最小知识

这一部分先解决“这是什么、为什么要这样写”。每看到一个短例子就亲手敲一遍并运行,不要先背完整程序。

今天先抓住什么

LINQ 是一套操作集合的写法,不是新的集合类型。

前面你已经会写 foreach

var prices = new List<decimal> { 12m, 88m, 199m, 35m }; var result = new List<decimal>(); foreach (decimal price in prices) { if (price >= 50m) { result.Add(price); } } Console.WriteLine(string.Join(", ", result));

LINQ 可以把“筛选、排序、转换”写成更清楚的流水线:

var prices = new List<decimal> { 12m, 88m, 199m, 35m }; var result = prices .Where(price => price >= 50m) .ToList(); Console.WriteLine(string.Join(", ", result));

两段代码都输出:

88, 199

名词先讲清楚

名词直接解释例子
LINQ操作集合的一套查询写法WhereSelect
Lambda一段短函数写法price => price >= 50m
Where筛选元素只要价格大于 50
Select转换元素商品转成商品名
OrderBy升序排序价格从低到高
OrderByDescending降序排序价格从高到低
Any是否至少有一个满足条件是否有库存
All是否全部满足条件是否全部价格有效
FirstOrDefault找第一个,找不到返回默认值找第一个高价商品
ToList把查询结果固定成 List立即得到结果

LINQ 常用方法速查

LINQ 方法很多,先掌握最常见的这一组。

需求方法返回什么例子
筛选Where多条结果价格大于 100 的商品
转换Select多条新结果商品转成商品名
升序排序OrderBy多条结果价格从低到高
降序排序OrderByDescending多条结果价格从高到低
二级升序ThenBy多条结果先按分类,再按价格
二级降序ThenByDescending多条结果先按分类,再按库存降序
是否存在Any一个 bool是否有库存
是否全部满足All一个 bool是否全部价格有效
找第一条FirstOrDefault一个元素或 null找第一个高价商品
数量Count一个整数统计有库存商品数量
求和Sum一个数字统计库存总数
平均值Average一个数字平均价格
最大值Max一个值最高价格
最小值Min一个值最低价格
固定结果ToListList<T>把查询结果保存下来
转数组ToArray数组需要固定数组时
转字典ToDictionary字典用 Id 快速查商品

完整例子:

var products = new List<Product> { new(1, "Keyboard", "Input", 199m, 10), new(2, "Mouse", "Input", 59m, 0), new(3, "Monitor", "Display", 899m, 5), new(4, "Cable", "Accessory", 19m, 30) }; var visibleProducts = products .Where(product => product.Stock > 0) .OrderBy(product => product.Category) .ThenBy(product => product.Price) .Select(product => $"{product.Category} | {product.Name} | {product.Price:F2}") .ToList(); bool hasExpensiveProduct = products.Any(product => product.Price >= 500m); bool allPricesValid = products.All(product => product.Price > 0); int totalStock = products.Sum(product => product.Stock); Product? keyboard = products.FirstOrDefault(product => product.Name == "Keyboard"); foreach (string line in visibleProducts) { Console.WriteLine(line); } Console.WriteLine($"是否有高价商品: {hasExpensiveProduct}"); Console.WriteLine($"价格是否全部有效: {allPricesValid}"); Console.WriteLine($"库存总数: {totalStock}"); Console.WriteLine($"找到商品: {keyboard?.Name}"); record Product(int Id, string Name, string Category, decimal Price, int Stock);

这段代码一次性展示了 LINQ 最常用的几类操作:

  • Where 控制留下哪些数据。
  • OrderByThenBy 控制排序。
  • Select 控制最后变成什么样子。
  • AnyAllSum 返回一个统计结果。
  • FirstOrDefault 可能找不到,所以用 keyboard?.Name 安全读取。

Lambda 怎么读

看这段:

price => price >= 50m

可以读成:

给我一个 price,判断 price 是否大于等于 50

再看:

product => product.Name

可以读成:

给我一个 product,取出 product 的 Name

Lambda 通常配合 LINQ 使用,用来告诉 LINQ 筛选规则、排序规则或转换规则。

Where:筛选,不修改原集合

var numbers = new List<int> { 1, 2, 3, 4 }; var evenNumbers = numbers .Where(number => number % 2 == 0) .ToList(); Console.WriteLine(string.Join(", ", numbers)); Console.WriteLine(string.Join(", ", evenNumbers));

输出:

1, 2, 3, 4 2, 4

Where 不会改变原来的 numbers,它返回一个新的查询结果。

Select:转换形状

var names = new List<string> { "Keyboard", "Mouse" }; var lines = names .Select(name => $"商品: {name}") .ToList(); Console.WriteLine(string.Join(" | ", lines));

输出:

商品: Keyboard | 商品: Mouse

Select 不是筛选,它会把每个元素转换成另一个样子。

OrderBy:排序

var prices = new List<decimal> { 9.9m, 3.5m, 20m }; var sorted = prices .OrderBy(price => price) .ToList(); Console.WriteLine(string.Join(", ", sorted));

输出:

3.5, 9.9, 20

降序用 OrderByDescending

AnyAll

var scores = new List<int> { 80, 95, 62 }; bool hasExcellent = scores.Any(score => score >= 90); bool allPassed = scores.All(score => score >= 60); Console.WriteLine(hasExcellent); Console.WriteLine(allPassed);

输出:

True True

Any 是“有没有至少一个”,All 是“是不是全部都满足”。

FirstOrDefault

var products = new List<Product> { new("Keyboard", 199m), new("Mouse", 59m), new("Cable", 19m) }; Product? expensive = products.FirstOrDefault(product => product.Price >= 100m); if (expensive == null) { Console.WriteLine("没有找到商品"); } else { Console.WriteLine(expensive.Name); } record Product(string Name, decimal Price);

输出:

Keyboard

FirstOrDefault 找不到时会返回 null,所以后面要判断。

延迟执行是什么意思

很多 LINQ 查询不是写出来就马上执行,而是在你真正遍历它时才执行。

var numbers = new List<int> { 1, 2, 3 }; var query = numbers.Where(number => number >= 2); numbers.Add(4); foreach (int number in query) { Console.WriteLine(number); }

输出:

2 3 4

因为 query 没有马上固定结果,遍历时才执行,所以后来加进去的 4 也被包含了。

如果想马上固定结果,用 ToList()

var numbers = new List<int> { 1, 2, 3 }; var fixedResult = numbers .Where(number => number >= 2) .ToList(); numbers.Add(4); Console.WriteLine(string.Join(", ", fixedResult));

输出:

2, 3

今天写 LINQ 时按这个顺序

  1. 先确认原始集合是什么。
  2. 先写 Where 筛选。
  3. 再写 OrderByOrderByDescending 排序。
  4. 再写 Select 转换输出形状。
  5. 最后用 ToList() 固定结果。
  6. 如果只判断有没有,用 Any
  7. 如果只取第一个,用 FirstOrDefault,并判断 null

第二部分:把知识组合成完整例子

前面已经学过最小知识。现在把它们组合起来,先读懂执行顺序,再完整敲一遍。今天最终要写一个商品查询程序:筛选有库存且价格不低于 50 的商品,按价格排序,再输出文本。

最终你要写出这个程序

var products = new List<Product> { new("Keyboard", 199m, true), new("Mouse", 59m, true), new("Monitor", 899m, false), new("Cable", 19m, true) }; var result = products .Where(product => product.InStock) .Where(product => product.Price >= 50m) .OrderBy(product => product.Price) .Select(product => $"{product.Name}: {product.Price:F2}") .ToList(); foreach (string line in result) { Console.WriteLine(line); } record Product(string Name, decimal Price, bool InStock);

输出:

Mouse: 59.00 Keyboard: 199.00

这段 LINQ 从上往下读就是:

从 products 开始 -> 只保留有库存的商品 -> 只保留价格不低于 50 的商品 -> 按价格从低到高排序 -> 转成可打印文本 -> 固定成 List

第三部分:跟着敲代码

从这里开始动手。每个例子都可以直接放进 Program.cs 运行。

例子 1:Where 过滤成绩

var scores = new List<int> { 55, 60, 80, 45 }; var passed = scores .Where(score => score >= 60) .ToList(); foreach (int score in passed) { Console.WriteLine(score); }

预期输出:

60 80

例子 2:Select 转换商品名

var products = new List<Product> { new("Keyboard", 199m), new("Mouse", 99m) }; var names = products .Select(product => product.Name) .ToList(); Console.WriteLine(string.Join(", ", names)); record Product(string Name, decimal Price);

预期输出:

Keyboard, Mouse

例子 3:按价格排序

var products = new List<Product> { new("Keyboard", 199m), new("Mouse", 59m), new("Cable", 19m) }; var sorted = products .OrderBy(product => product.Price) .ToList(); foreach (Product product in sorted) { Console.WriteLine($"{product.Name}: {product.Price:F2}"); } record Product(string Name, decimal Price);

输出顺序是 CableMouseKeyboard

例子 4:组合 WhereOrderBySelect

var products = new List<Product> { new("Keyboard", 199m, true), new("Mouse", 59m, true), new("Monitor", 899m, false), new("Cable", 19m, true) }; var result = products .Where(product => product.InStock) .Where(product => product.Price >= 50m) .OrderBy(product => product.Price) .Select(product => $"{product.Name}: {product.Price:F2}") .ToList(); foreach (string line in result) { Console.WriteLine(line); } record Product(string Name, decimal Price, bool InStock);

预期输出:

Mouse: 59.00 Keyboard: 199.00

例子 5:Any 判断是否存在

var products = new List<Product> { new("Keyboard", 199m, 5), new("Mouse", 59m, 0), new("Cable", 19m, 20) }; bool hasSoldOut = products.Any(product => product.Stock == 0); Console.WriteLine(hasSoldOut); record Product(string Name, decimal Price, int Stock);

预期输出:

True

例子 6:FirstOrDefault 查找第一个

var products = new List<Product> { new("Keyboard", 199m), new("Mouse", 59m), new("Cable", 19m) }; Product? product = products.FirstOrDefault(item => item.Price < 50m); if (product == null) { Console.WriteLine("没有找到商品"); } else { Console.WriteLine($"{product.Name}: {product.Price:F2}"); } record Product(string Name, decimal Price);

预期输出:

Cable: 19.00

例子 7:观察延迟执行

var numbers = new List<int> { 1, 2, 3 }; var query = numbers.Where(number => number >= 2); numbers.Add(4); Console.WriteLine(string.Join(", ", query));

预期输出:

2, 3, 4

query 后面加上 .ToList(),再运行一次,输出会变成 2, 3

常见错误和修法

错误为什么错修法
Where 后以为已经是列表LINQ 查询通常是延迟执行需要列表时调用 ToList()
First 查不到时报错没有元素时会抛异常不确定存在时用 FirstOrDefault()
排序后原列表没变OrderBy 返回新序列接收结果:items = items.OrderBy(...).ToList()
字符串搜索大小写敏感可能搜不到目标比较前统一 ToLower()
一条 LINQ 写太长难读难排错先拆成多行,每一步一个含义

小白重复敲写训练

每段至少敲两遍。第二遍只看题目,不看代码。

训练 1:筛选大于等于 60 的成绩

var scores = new List<int> { 45, 80, 62, 30, 95 }; var passed = scores.Where(score => score >= 60); foreach (int score in passed) { Console.WriteLine(score); }

改动任务:把及格线改成 70,运行前先写下你预测的结果。

训练 2:把价格统一加 10

var prices = new List<decimal> { 20m, 35m, 50m }; var newPrices = prices.Select(price => price + 10m); foreach (decimal price in newPrices) { Console.WriteLine(price); }

改动任务:不要加 10,改成打八折。

训练 3:筛选后排序

var numbers = new List<int> { 8, 3, 10, 1, 6 }; var result = numbers .Where(number => number >= 5) .OrderBy(number => number); Console.WriteLine(string.Join(", ", result));

改动任务:改成从大到小,提示是 OrderByDescending

每日小测

做完本页后,用这 5 题检查是否真的掌握。

1. 判断题

本页的目标不是只把代码运行起来,还要能说清楚“为什么这样写”。

答案:对。能运行只是第一步,能解释原理、常用操作和常见错误,才说明本页内容进入了可复用能力。

2. 填空题

本页主题是:LINQ 基础。今天至少要掌握的 3 个点是:

1. LINQ 是什么,为什么它不是新的集合类型 2. Lambda 表达式 `x => ...` 怎么读 3. `Where`、`Select`、`OrderBy` 分别做什么

答案:以上 3 点必须能用自己的代码跑通,不能只停留在阅读。

3. 流程题

遇到本页相关功能时,先按什么顺序处理?

答案:先看完整例子,确认最终效果;再读原理和名词;然后跟着第三部分从空项目敲代码;最后对照作业答案检查。

4. 找错误题

如果本页代码运行失败,第一步应该做什么?

答案:先看终端或 IDE 里的第一条错误,找到文件名和行号;不要同时改很多地方。再回到本页的“常见错误和修法”表格,对照错误类型逐项排查。

5. 改需求题

在本页完整例子跑通后,至少改一个小需求。

可选改法:

  • 改一个字段名称。
  • 多加一个校验条件。
  • 多输出一行结果。
  • 把固定数据改成用户输入。
  • 把一次处理改成多条数据处理。

答案标准:修改后能重新运行,并能说明这次修改影响了哪一段逻辑。重点检查:LINQ 是什么,为什么它不是新的集合类型。

上位机专项练习

LINQ 可以从大量设备和采集值中筛选报警、排序状态并生成界面需要的数据。

下面 3 个例子都要亲手敲。先运行原代码,再完成每个例子后面的改动任务。

专项例子 1:筛选超限采集值

var readings = new List<double> { 72.5, 81.2, 79.0, 90.1 }; List<double> alarms = readings .Where(value => value >= 80) .ToList(); foreach (double alarm in alarms) { Console.WriteLine(alarm); }

运行结果或界面效果:

81.2 90.1

改动任务: 把条件改成 value < 80,查看正常数据。

专项例子 2:按采集值从高到低排序

var readings = new List<double> { 72.5, 81.2, 79.0 }; List<double> sorted = readings .OrderByDescending(value => value) .ToList(); Console.WriteLine(string.Join(", ", sorted));

运行结果或界面效果:

81.2, 79, 72.5

改动任务: 改成 OrderBy,观察顺序。

专项例子 3:只取设备名称

var devices = new[] { new { Name = "PLC-01", IsOnline = true }, new { Name = "PLC-02", IsOnline = false } }; List<string> onlineNames = devices .Where(device => device.IsOnline) .Select(device => device.Name) .ToList(); Console.WriteLine(string.Join(", ", onlineNames));

运行结果或界面效果:

PLC-01

改动任务: 把 PLC-02 改成在线。

第四部分:作业完整答案

这一部分给出当天作业的完整答案。先按第三部分做一遍,再用这里检查。

作业要求

写一个商品筛选程序,要求:

  1. 准备至少 5 个商品。
  2. 每个商品有名称、价格、库存。
  3. 只保留有库存的商品。
  4. 只保留价格大于等于 50 的商品。
  5. 按价格从高到低排序。
  6. 输出格式为 商品名: 价格, 库存
  7. 判断是否存在缺货商品。
  8. 找出第一个价格低于 30 的商品。

作业拆解

按这个顺序写:

  1. 先创建 Product 记录类型。
  2. 准备商品列表。
  3. Where 过滤库存。
  4. 再写 Where 过滤价格。
  5. OrderByDescending 排序。
  6. Select 转成输出文本。
  7. Any 判断缺货。
  8. FirstOrDefault 找低价商品。

完整代码

var products = new List<Product> { new("Keyboard", 199m, 5), new("Mouse", 59m, 0), new("Monitor", 899m, 2), new("Cable", 19m, 30), new("Notebook", 79m, 10) }; var result = products .Where(product => product.Stock > 0) .Where(product => product.Price >= 50m) .OrderByDescending(product => product.Price) .Select(product => $"{product.Name}: {product.Price:F2}, 库存 {product.Stock}") .ToList(); Console.WriteLine("=== 筛选结果 ==="); foreach (string line in result) { Console.WriteLine(line); } bool hasSoldOut = products.Any(product => product.Stock == 0); Console.WriteLine($"是否存在缺货商品: {hasSoldOut}"); Product? cheapProduct = products.FirstOrDefault(product => product.Price < 30m); if (cheapProduct == null) { Console.WriteLine("没有低价商品"); } else { Console.WriteLine($"第一个低价商品: {cheapProduct.Name}"); } record Product(string Name, decimal Price, int Stock);

正确运行结果

你应该能看到:

Monitor: 899.00, 库存 2 Keyboard: 199.00, 库存 5 Notebook: 79.00, 库存 10 是否存在缺货商品: True 第一个低价商品: Cable

你要能说清楚这 6 件事

  • Where 是筛选,不会修改原集合。
  • Select 是转换,不是筛选。
  • OrderBy 是升序,OrderByDescending 是降序。
  • Any 判断是否至少有一个满足条件。
  • FirstOrDefault 找不到时可能返回 null
  • ToList 可以把查询结果立即固定下来。