Day 7 - 复盘与巩固
建议用时:120-150 分钟
你将学会什么
- 怎么把 Week 9 的知识连成一条主线
- 怎么判断一个功能有没有进入“可交付”状态
- 怎么用一个小程序复盘性能、持久化和日志
- 怎么写验收清单
- 怎么从“能运行”提升到“能检查、能排错、能发布”
本页不再新增 API。今天把本周内容串起来:数据量变大怎么处理、文件怎么保存、错误怎么记录、发布后怎么检查。能把流程说清楚,比只记住某个命令更重要。
本页固定顺序
- 先学第一部分:弄懂今天最小、最重要的知识,并运行短例子。
- 再学第二部分:把刚学的知识组合成一个完整例子。
- 然后做第三部分:自己跟着敲,再完成重复训练和每日小测。
- 最后做第四部分:先独立完成作业,再用完整答案检查。
学习衔接
上一页学习的是“错误处理与日志”,今天继续学习“复盘与巩固”。先使用上一页已经会的写法,再只增加今天这个新知识点;如果前置内容还不能独立敲出,先回上一页复习,不要硬跳。
今天的最低通过线
第一次学习不要求背完整页。完成下面 3 项,就可以继续:
- 能用自己的话说明“复盘与巩固”解决什么问题。
- 把第一部分的短例子亲手敲完,并确认每个例子都能运行。
- 不看完整答案完成第三部分至少前 3 个例子,再主动改一个值观察结果。
第一部分:先学原理和最小知识
这一部分从最小知识开始。先读解释,再把紧跟着的短例子敲一遍。复盘不是把前 6 天重新读一遍,而是把它们变成一条可以复用的开发路线。
Week 9 的主线
本周主题可以用一句话概括:
程序不只要能写出来,还要能在真实运行时保持可用、可保存、可排查、可发布。前几周更多关注“功能怎么写”。Week 9 开始关注“功能运行久了、数据多了、发布出去了,会不会出问题”。
每天对应的问题
| 日期 | 主题 | 解决的问题 |
|---|---|---|
| Day 1 | 性能认知 | 知道慢在哪里,不靠猜 |
| Day 2 | 大列表和虚拟化 | 数据多时不要一次显示太多 |
| Day 3 | 文件与对话框 | 用户选择文件,程序安全读写 |
| Day 4 | 数据持久化 | 程序关闭后数据还能恢复 |
| Day 5 | 打包与发布 | 从开发目录变成发布目录 |
| Day 6 | 错误处理与日志 | 出错后有提示,也有排查线索 |
| Day 7 | 复盘与巩固 | 把这些点连成完整流程 |
可交付功能要有 5 个部分
一个功能只写正常流程是不够的。
可交付功能至少包含:
| 部分 | 要回答的问题 | 例子 |
|---|---|---|
| 输入 | 数据从哪里来 | 输入框、文件、列表 |
| 处理 | 程序做什么 | 搜索、保存、读取 |
| 输出 | 结果到哪里去 | 界面、JSON、日志 |
| 失败处理 | 出错时怎么办 | 提示、拦截、catch |
| 验收 | 怎么证明可用 | 正常跑一次,错误跑一次 |
本页的 Program.cs 就是按这 5 个部分组织的。
复盘时不要只看“会不会写”
复盘要问 6 个问题:
- 这个功能输入是什么?
- 处理规则是什么?
- 输出结果是什么?
- 数据量变大时会不会卡?
- 失败时用户看到什么?
- 失败细节保存在哪里?
如果这 6 个问题都能回答,说明这部分知识已经能用于项目。
为什么要写验收清单
验收清单是为了防止“我以为好了”。
例如“保存 JSON”不能只看代码里有 WriteAllTextAsync,还要检查:
- 文件是否真的生成。
- 文件内容是不是 JSON。
- 读取回来条数对不对。
- 文件不存在时会不会崩溃。
- 发布后保存路径还能不能写。
没有验收清单,很多问题要到交付后才暴露。
本周最重要的判断方法
遇到功能问题,按这个顺序判断:
- 先看是否能复现。
- 再用耗时或日志定位位置。
- 数据多时先减少界面显示量。
- 文件读写先处理取消、缺失、权限。
- 保存数据先明确路径。
- 发布后必须从发布目录运行。
这就是 Week 9 的实践路线。
Week 9 性能与发布常用操作总表
| 主题 | 必须会的操作 |
|---|---|
| 性能测量 | Stopwatch |
| 大列表 | Take、Where、VisibleProducts |
| 文件对话框 | StorageProvider、OpenReadAsync、OpenWriteAsync |
| JSON 保存 | JsonSerializer、File.ReadAllText、WriteAllText |
| 发布 | dotnet publish -c Release |
| 日志 | 时间、级别、操作、错误原因 |
第二部分:把知识组合成完整例子
今天用一个控制台程序复盘 Week 9。
为什么用控制台程序?因为今天的目标是把流程跑清楚,不让界面代码干扰理解。
这个程序会做 6 件事:
- 生成 5000 条商品。
- 只取前 100 条作为首屏数据。
- 用
Stopwatch记录处理耗时。 - 把首屏数据保存成 JSON。
- 再从 JSON 读取回来。
- 故意读取不存在的文件,把错误写入日志。
完整 Program.cs
文件位置:
Program.csusing System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Text.Json;
string appFolder = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
"Week09Review");
Directory.CreateDirectory(appFolder);
string jsonPath = Path.Combine(appFolder, "products.json");
string logPath = Path.Combine(appFolder, "app.log");
var watch = Stopwatch.StartNew();
List<Product> allProducts = Enumerable
.Range(1, 5000)
.Select(i => new Product($"Product {i:0000}", i * 1.5m))
.ToList();
List<Product> firstPage = allProducts
.Take(100)
.ToList();
string json = JsonSerializer.Serialize(firstPage, new JsonSerializerOptions
{
WriteIndented = true
});
await File.WriteAllTextAsync(jsonPath, json);
string loadedJson = await File.ReadAllTextAsync(jsonPath);
List<Product> loadedProducts = JsonSerializer.Deserialize<List<Product>>(loadedJson)
?? new List<Product>();
watch.Stop();
Console.WriteLine($"全部商品:{allProducts.Count}");
Console.WriteLine($"首屏商品:{firstPage.Count}");
Console.WriteLine($"读取回来:{loadedProducts.Count}");
Console.WriteLine($"耗时:{watch.ElapsedMilliseconds}ms");
Console.WriteLine($"JSON 文件:{jsonPath}");
Console.WriteLine($"日志文件:{logPath}");
try
{
string missingPath = Path.Combine(appFolder, "missing.json");
await File.ReadAllTextAsync(missingPath);
}
catch (Exception ex)
{
Task LogAsync(string level, string message, Exception? ex = null)
{
string detail = ex is null
? message
: $"{message}{Environment.NewLine}{ex}";
string line = $"{DateTimeOffset.Now:yyyy-MM-dd HH:mm:ss.fff zzz} [{level}] {detail}{Environment.NewLine}";
return File.AppendAllTextAsync(logPath, line);
}
await LogAsync("ERROR", "读取 missing.json 失败", ex);
Console.WriteLine("故意制造的错误已写入日志。");
}
record Product(string Name, decimal Price);正常运行后应该看到
全部商品:5000
首屏商品:100
读取回来:100
耗时:若干 ms
JSON 文件:...
日志文件:...
故意制造的错误已写入日志。这个小程序把 Week 9 的主线全部串起来了:数据量、性能测量、JSON、文件、日志、异常。
第三部分:跟着敲代码
从这里开始动手。今天用控制台项目完成复盘程序。
第 1 步:创建控制台项目
在一个练习目录里执行:
dotnet new console -n Week09Review
cd Week09Review打开 Program.cs,清空原内容。
第 2 步:写 using
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Text.Json;解释:
Diagnostics用Stopwatch。IO用文件读写。Linq用Range、Select、Take。Text.Json用 JSON 序列化。
第 3 步:准备保存目录
string appFolder = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
"Week09Review");
Directory.CreateDirectory(appFolder);
string jsonPath = Path.Combine(appFolder, "products.json");
string logPath = Path.Combine(appFolder, "app.log");解释:
appFolder是本程序的数据目录。jsonPath是 JSON 文件路径。logPath是日志文件路径。CreateDirectory保证目录存在。
第 4 步:写日志函数
Task LogAsync(string level, string message, Exception? ex = null)
{
string detail = ex is null
? message
: $"{message}{Environment.NewLine}{ex}";
string line = $"{DateTimeOffset.Now:yyyy-MM-dd HH:mm:ss.fff zzz} [{level}] {detail}{Environment.NewLine}";
return File.AppendAllTextAsync(logPath, line);
}解释:
level是 INFO 或 ERROR。message是动作说明。ex是异常详情,可有可无。AppendAllTextAsync是追加日志,不覆盖旧日志。
第 5 步:生成 5000 条商品
var watch = Stopwatch.StartNew();
List<Product> allProducts = Enumerable
.Range(1, 5000)
.Select(i => new Product($"Product {i:0000}", i * 1.5m))
.ToList();解释:
Stopwatch.StartNew()开始计时。Range(1, 5000)生成 5000 个数字。Select把数字变成商品。ToList生成商品列表。
第 6 步:只取首屏 100 条
List<Product> firstPage = allProducts
.Take(100)
.ToList();
await LogAsync("INFO", $"准备商品 {allProducts.Count} 条,首屏 {firstPage.Count} 条");解释:
allProducts是全部数据。firstPage是当前处理的数据。- 这对应 Day 2 的大列表思路。
第 7 步:保存 JSON
string json = JsonSerializer.Serialize(firstPage, new JsonSerializerOptions
{
WriteIndented = true
});
await File.WriteAllTextAsync(jsonPath, json);
await LogAsync("INFO", $"已保存 JSON:{jsonPath}");解释:
Serialize把商品列表变成 JSON。WriteIndented = true让 JSON 文件更容易阅读。- 保存成功后写 INFO 日志。
第 8 步:读取 JSON
string loadedJson = await File.ReadAllTextAsync(jsonPath);
List<Product> loadedProducts = JsonSerializer.Deserialize<List<Product>>(loadedJson)
?? new List<Product>();解释:
- 先从文件读取 JSON 字符串。
- 再把 JSON 转回
List<Product>。 ?? new List<Product>()防止结果为空。
第 9 步:输出结果
watch.Stop();
Console.WriteLine($"全部商品:{allProducts.Count}");
Console.WriteLine($"首屏商品:{firstPage.Count}");
Console.WriteLine($"读取回来:{loadedProducts.Count}");
Console.WriteLine($"耗时:{watch.ElapsedMilliseconds}ms");
Console.WriteLine($"JSON 文件:{jsonPath}");
Console.WriteLine($"日志文件:{logPath}");解释:
- 输出总数、首屏数量、读取数量。
- 如果读取数量不是 100,就说明保存或读取有问题。
第 10 步:故意制造错误并写日志
try
{
string missingPath = Path.Combine(appFolder, "missing.json");
await File.ReadAllTextAsync(missingPath);
}
catch (Exception ex)
{
await LogAsync("ERROR", "读取 missing.json 失败", ex);
Console.WriteLine("故意制造的错误已写入日志。");
}解释:
missing.json不存在,所以会抛异常。catch捕获异常。LogAsync("ERROR", ..., ex)把错误详情写入日志。
第 11 步:添加 Product 记录类型
record Product(string Name, decimal Price);解释:
Product有商品名和价格。record适合这种只保存数据的类型。
第 12 步:运行并检查
执行:
dotnet run检查:
- 控制台显示全部商品 5000。
- 控制台显示首屏商品 100。
- 控制台显示读取回来 100。
- JSON 文件路径能打开。
- 日志文件路径能打开。
- 日志里有 INFO 和 ERROR。
小白重复敲写训练
复盘日完成“测量、保存、处理错误”三个独立小程序。
训练 1:测量耗时
var watch = Stopwatch.StartNew();
await Task.Delay(300);
watch.Stop();
Console.WriteLine(watch.ElapsedMilliseconds);训练 2:保存设置
var settings = new { Theme = "Dark", PageSize = 20 };
string json = JsonSerializer.Serialize(settings);
await File.WriteAllTextAsync("settings.json", json);训练 3:安全读取
try
{
Console.WriteLine(await File.ReadAllTextAsync("settings.json"));
}
catch (Exception ex)
{
Console.WriteLine($"读取失败: {ex.Message}");
}关掉答案,把三段组合成“保存后再读取”的完整程序。
每日小测
做完本页后,用这 5 题检查是否真的掌握。
1. 判断题
本页的目标不是只把代码运行起来,还要能说清楚“为什么这样写”。
答案:对。能运行只是第一步,能解释原理、常用操作和常见错误,才说明本页内容进入了可复用能力。
2. 填空题
本页主题是:复盘与巩固。今天至少要掌握的 3 个点是:
1. 怎么把 Week 9 的知识连成一条主线
2. 怎么判断一个功能有没有进入“可交付”状态
3. 怎么用一个小程序复盘性能、持久化和日志答案:以上 3 点必须能用自己的代码跑通,不能只停留在阅读。
3. 流程题
遇到本页相关功能时,先按什么顺序处理?
答案:先看完整例子,确认最终效果;再读原理和名词;然后跟着第三部分从空项目敲代码;最后对照作业答案检查。
4. 找错误题
如果本页代码运行失败,第一步应该做什么?
答案:先看终端或 IDE 里的第一条错误,找到文件名和行号;不要同时改很多地方。再回到本页的“常见错误和修法”表格,对照错误类型逐项排查。
5. 改需求题
在本页完整例子跑通后,至少改一个小需求。
可选改法:
- 改一个字段名称。
- 多加一个校验条件。
- 多输出一行结果。
- 把固定数据改成用户输入。
- 把一次处理改成多条数据处理。
答案标准:修改后能重新运行,并能说明这次修改影响了哪一段逻辑。重点检查:怎么把 Week 9 的知识连成一条主线。
上位机专项练习
本周过关标准是大数据列表不卡、配置能保存、文件能导入导出、Release 包能在目标电脑运行。
下面 3 个例子都要亲手敲。先运行原代码,再完成每个例子后面的改动任务。
专项例子 1:复习性能检查
Console.WriteLine("[ ] 10000 条列表滚动正常");
Console.WriteLine("[ ] UI 刷新频率受控");
Console.WriteLine("[ ] 后台采集不阻塞界面");运行结果或界面效果:
三项性能检查清楚改动任务: 实际测试后把方框改成完成。
专项例子 2:复习数据持久化
await SaveSettingsAsync(settings);
AppSettings loaded = await LoadSettingsAsync();
Console.WriteLine(loaded.LastDeviceId);运行结果或界面效果:
保存后能再次读回相同设置改动任务: 故意破坏 JSON,处理读取失败。
专项例子 3:复习发布验收
string[] checks = ["可启动", "配置可改", "日志可找", "异常有提示"];
foreach (string check in checks) Console.WriteLine($"验收: {check}");运行结果或界面效果:
发布包有四项明确验收改动任务: 在另一台 Windows 电脑执行检查。
第四部分:作业完整答案
作业要求:用一个控制台程序复盘 Week 9,程序要包含大数据、首屏限制、JSON 保存、JSON 读取、耗时记录、错误日志。
答案文件:Program.cs
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Text.Json;
string appFolder = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
"Week09Review");
Directory.CreateDirectory(appFolder);
string jsonPath = Path.Combine(appFolder, "products.json");
string logPath = Path.Combine(appFolder, "app.log");
var watch = Stopwatch.StartNew();
List<Product> allProducts = Enumerable
.Range(1, 5000)
.Select(i => new Product($"Product {i:0000}", i * 1.5m))
.ToList();
List<Product> firstPage = allProducts
.Take(100)
.ToList();
string json = JsonSerializer.Serialize(firstPage, new JsonSerializerOptions
{
WriteIndented = true
});
await File.WriteAllTextAsync(jsonPath, json);
string loadedJson = await File.ReadAllTextAsync(jsonPath);
List<Product> loadedProducts = JsonSerializer.Deserialize<List<Product>>(loadedJson)
?? new List<Product>();
watch.Stop();
Console.WriteLine($"全部商品:{allProducts.Count}");
Console.WriteLine($"首屏商品:{firstPage.Count}");
Console.WriteLine($"读取回来:{loadedProducts.Count}");
Console.WriteLine($"耗时:{watch.ElapsedMilliseconds}ms");
Console.WriteLine($"JSON 文件:{jsonPath}");
Console.WriteLine($"日志文件:{logPath}");
try
{
string missingPath = Path.Combine(appFolder, "missing.json");
await File.ReadAllTextAsync(missingPath);
}
catch (Exception ex)
{
Task LogAsync(string level, string message, Exception? ex = null)
{
string detail = ex is null
? message
: $"{message}{Environment.NewLine}{ex}";
string line = $"{DateTimeOffset.Now:yyyy-MM-dd HH:mm:ss.fff zzz} [{level}] {detail}{Environment.NewLine}";
return File.AppendAllTextAsync(logPath, line);
}
await LogAsync("ERROR", "读取 missing.json 失败", ex);
Console.WriteLine("故意制造的错误已写入日志。");
}
record Product(string Name, decimal Price);验收结果
运行后必须看到这些结果:
- 控制台显示
全部商品:5000。 - 控制台显示
首屏商品:100。 - 控制台显示
读取回来:100。 - 控制台显示 JSON 文件路径。
- 控制台显示日志文件路径。
- 日志文件里有
[INFO]。 - 日志文件里有
[ERROR]。 - JSON 文件里能看到商品名称和价格。
为什么这个答案是对的
这个答案把本周知识连成了一条完整链路:
| Week 9 知识 | 程序里的位置 | 作用 |
|---|---|---|
| 性能认知 | Stopwatch | 记录耗时 |
| 大列表处理 | Take(100) | 控制首屏数量 |
| 文件读写 | File.WriteAllTextAsync / File.ReadAllTextAsync | 保存和读取文件 |
| 数据持久化 | JsonSerializer | 对象和 JSON 互转 |
| 错误处理 | try/catch | 捕获失败 |
| 日志 | LogAsync | 保存排查线索 |
能把这条链路跑通,Week 9 的重点就不是散的:数据多了要控制显示量,保存数据要明确路径,失败要有提示和日志,发布后要重新验收。