Day 2 - LINQ 基础
建议用时:180-210 分钟
你将学会什么
- LINQ 是什么,为什么它不是新的集合类型
- Lambda 表达式
x => ...怎么读 Where、Select、OrderBy分别做什么Any、All、FirstOrDefault怎么用ToList为什么经常出现在 LINQ 结尾- 延迟执行是什么意思
- 如何把循环筛选改写成 LINQ 流水线
- 如何写完整的商品筛选、排序、输出程序
LINQ 的重点不是背方法名,而是读懂这条线:原始集合 -> 筛选 -> 排序 -> 转换 -> 得到结果。
本页固定顺序
- 先学第一部分:弄懂今天最小、最重要的知识,并运行短例子。
- 再学第二部分:把刚学的知识组合成一个完整例子。
- 然后做第三部分:自己跟着敲,再完成重复训练和每日小测。
- 最后做第四部分:先独立完成作业,再用完整答案检查。
学习衔接
上一页学习的是“集合体系”,今天继续学习“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 | 操作集合的一套查询写法 | Where、Select |
| 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 | 一个值 | 最低价格 |
| 固定结果 | ToList | List<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控制留下哪些数据。OrderBy和ThenBy控制排序。Select控制最后变成什么样子。Any、All、Sum返回一个统计结果。FirstOrDefault可能找不到,所以用keyboard?.Name安全读取。
Lambda 怎么读
看这段:
price => price >= 50m可以读成:
给我一个 price,判断 price 是否大于等于 50再看:
product => product.Name可以读成:
给我一个 product,取出 product 的 NameLambda 通常配合 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, 4Where 不会改变原来的 numbers,它返回一个新的查询结果。
Select:转换形状
var names = new List<string> { "Keyboard", "Mouse" };
var lines = names
.Select(name => $"商品: {name}")
.ToList();
Console.WriteLine(string.Join(" | ", lines));输出:
商品: Keyboard | 商品: MouseSelect 不是筛选,它会把每个元素转换成另一个样子。
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。
Any 和 All
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
TrueAny 是“有没有至少一个”,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);输出:
KeyboardFirstOrDefault 找不到时会返回 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 时按这个顺序
- 先确认原始集合是什么。
- 先写
Where筛选。 - 再写
OrderBy或OrderByDescending排序。 - 再写
Select转换输出形状。 - 最后用
ToList()固定结果。 - 如果只判断有没有,用
Any。 - 如果只取第一个,用
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);输出顺序是 Cable、Mouse、Keyboard。
例子 4:组合 Where、OrderBy、Select
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 改成在线。
第四部分:作业完整答案
这一部分给出当天作业的完整答案。先按第三部分做一遍,再用这里检查。
作业要求
写一个商品筛选程序,要求:
- 准备至少 5 个商品。
- 每个商品有名称、价格、库存。
- 只保留有库存的商品。
- 只保留价格大于等于 50 的商品。
- 按价格从高到低排序。
- 输出格式为
商品名: 价格, 库存。 - 判断是否存在缺货商品。
- 找出第一个价格低于 30 的商品。
作业拆解
按这个顺序写:
- 先创建
Product记录类型。 - 准备商品列表。
- 写
Where过滤库存。 - 再写
Where过滤价格。 - 写
OrderByDescending排序。 - 写
Select转成输出文本。 - 用
Any判断缺货。 - 用
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可以把查询结果立即固定下来。