Day 1 - 边界与异常
建议用时:180-210 分钟
你将学会什么
- 什么是边界场景
- 输入错误、业务失败、系统异常有什么区别
- 为什么优先用校验处理用户输入
- 什么时候才用
try/catch - 怎么一次输出多个错误
- 怎么把崩溃变成可理解的失败
交付前最后一周,重点不是继续加功能,而是把已有功能变稳。今天先检查边界:空值、超长、格式错误、负数、文件不存在。
本页固定顺序
- 先学第一部分:弄懂今天最小、最重要的知识,并运行短例子。
- 再学第二部分:把刚学的知识组合成一个完整例子。
- 然后做第三部分:自己跟着敲,再完成重复训练和每日小测。
- 最后做第四部分:先独立完成作业,再用完整答案检查。
今天只抓住 3 件事
- 输入错误优先用校验处理,不要一上来就靠
try/catch。 - 业务失败要返回明确错误,让调用方知道哪里不合格。
- 系统异常要捕获、提示、记录,不能让程序直接崩掉。
学习衔接
上一页学习的是“复盘与巩固”,今天继续学习“边界与异常”。先使用上一页已经会的写法,再只增加今天这个新知识点;如果前置内容还不能独立敲出,先回上一页复习,不要硬跳。
第一部分:先学原理和最小知识
这一部分从最小知识开始。先读解释,再把紧跟着的短例子敲一遍。边界处理的第一步,是把错误分清类型。
三类问题
| 类型 | 例子 | 推荐处理 |
|---|---|---|
| 输入错误 | 价格是 abc | TryParse + 提示 |
| 业务失败 | 没有删除权限 | 返回失败结果 |
| 系统异常 | 文件不存在、权限不足 | try/catch + 日志 |
用户输入错误不要先靠异常。
能在入口校验的,就在入口校验。
为什么一次输出多个错误
如果用户同时输入空名称和错误价格,只提示一个错误,用户修完后还要再提交一次才知道第二个错误。
更好的做法是收集多个错误:
var errors = new List<string>();最后一起显示。
为什么用 TryParse
这段代码不稳:
decimal price = decimal.Parse(priceText);如果输入 abc,程序会抛异常。
更稳的是:
decimal.TryParse(priceText, out decimal price)它能告诉你转换是否成功,不会直接崩掉。
try/catch 应该放在哪里
try/catch 适合包住不可控操作:
- 文件读写。
- 网络请求。
- 数据库连接。
- 第三方库调用。
不适合用来代替普通输入校验。
空 catch 是危险的
不要写:
try
{
File.ReadAllText(path);
}
catch
{
}这样错误被吞掉,用户不知道,日志也没有线索。
至少要给出提示或记录日志。
第二部分:把知识组合成完整例子
今天做一个边界检查程序。
它会验证:
- 合法商品可以保存。
- 空名称会失败。
- 名称超过 50 个字符会失败。
- 价格不是数字会失败。
- 价格小于等于 0 会失败。
- 库存不是整数会失败。
- 库存小于 0 会失败。
- 文件不存在时给出明确提示。
完整 Program.cs
using System;
using System.Collections.Generic;
using System.IO;
List<string> ValidateProduct(string nameText, string priceText, string stockText)
{
var errors = new List<string>();
string name = nameText.Trim();
if (string.IsNullOrWhiteSpace(name))
{
errors.Add("商品名称不能为空。");
}
else if (name.Length > 50)
{
errors.Add("商品名称不能超过 50 个字符。");
}
if (!decimal.TryParse(priceText, out decimal price))
{
errors.Add("价格必须是数字。");
}
else if (price <= 0)
{
errors.Add("价格必须大于 0。");
}
if (!int.TryParse(stockText, out int stock))
{
errors.Add("库存必须是整数。");
}
else if (stock < 0)
{
errors.Add("库存不能小于 0。");
}
return errors;
}
void CheckProduct(string nameText, string priceText, string stockText)
{
List<string> errors = ValidateProduct(nameText, priceText, stockText);
if (errors.Count == 0)
{
Console.WriteLine($"保存成功:{nameText.Trim()}");
return;
}
Console.WriteLine("保存失败:");
foreach (string error in errors)
{
Console.WriteLine($"- {error}");
}
}
void ReadFileSafely(string path)
{
try
{
string text = File.ReadAllText(path);
Console.WriteLine(text);
}
catch (FileNotFoundException)
{
Console.WriteLine($"读取失败:文件不存在,路径:{path}");
}
catch (Exception ex)
{
Console.WriteLine($"读取失败:{ex.Message}");
}
}
CheckProduct("Keyboard", "199", "10");
CheckProduct("", "199", "10");
CheckProduct(new string('A', 60), "199", "10");
CheckProduct("Mouse", "abc", "10");
CheckProduct("Mouse", "-1", "10");
CheckProduct("Monitor", "899", "abc");
CheckProduct("Monitor", "899", "-1");
ReadFileSafely("missing-products.json");
正常运行结果
你会看到 1 条成功,多条失败提示,最后还有文件不存在提示。
第三部分:跟着敲代码
从这里开始动手。
第 1 步:创建控制台项目
dotnet new console -n BoundaryDemo
cd BoundaryDemo第 2 步:写验证方法
List<string> ValidateProduct(string nameText, string priceText, string stockText)
{
var errors = new List<string>();
string name = nameText.Trim();
if (string.IsNullOrWhiteSpace(name))
{
errors.Add("商品名称不能为空。");
}
else if (name.Length > 50)
{
errors.Add("商品名称不能超过 50 个字符。");
}
return errors;
}先写名称,再补价格和库存。
第 3 步:补价格校验
if (!decimal.TryParse(priceText, out decimal price))
{
errors.Add("价格必须是数字。");
}
else if (price <= 0)
{
errors.Add("价格必须大于 0。");
}第 4 步:补库存校验
if (!int.TryParse(stockText, out int stock))
{
errors.Add("库存必须是整数。");
}
else if (stock < 0)
{
errors.Add("库存不能小于 0。");
}第 5 步:写文件安全读取
try
{
string text = File.ReadAllText(path);
Console.WriteLine(text);
}
catch (FileNotFoundException)
{
Console.WriteLine($"读取失败:文件不存在,路径:{path}");
}第 6 步:运行检查
运行后确认:
- 合法商品成功。
- 空名称失败。
- 超长名称失败。
- 错误价格失败。
- 负库存失败。
- 文件不存在有明确提示。
边界和异常常用操作速查
| 需求 | 写法 | 什么时候用 |
|---|---|---|
| 判断文本是否为空 | string.IsNullOrWhiteSpace(name) | 名称、备注、路径输入 |
| 去掉前后空格 | name.Trim() | 保存文本前统一格式 |
| 检查长度 | name.Length > 50 | 限制名称、备注长度 |
| 文本转金额 | decimal.TryParse(text, out decimal price) | 价格输入 |
| 文本转整数 | int.TryParse(text, out int stock) | 库存输入 |
| 收集多个错误 | errors.Add("错误内容") | 一次显示多条校验失败 |
| 文件是否存在 | File.Exists(path) | 读取文件前检查 |
| 捕获指定异常 | catch (FileNotFoundException) | 文件缺失单独提示 |
| 捕获兜底异常 | catch (Exception ex) | 未预料错误统一处理 |
常见错误和修法
| 错误 | 为什么错 | 修法 |
|---|---|---|
用 Parse 直接转换输入 | 用户输入非法时程序会中断 | 改成 TryParse,失败时返回错误 |
catch 里什么都不写 | 错误被吞掉,后面无法定位 | 至少输出或记录 ex.Message |
try/catch 包住整页逻辑 | 校验失败和系统异常混在一起 | 输入先校验,真正 IO 操作再捕获异常 |
| 校验失败后还继续保存 | 错误数据会进入后续流程 | 有错误就 return 或返回失败结果 |
| 错误提示只写“失败” | 不知道该改哪一项 | 写清字段和规则,例如“价格必须大于 0” |
小白重复敲写训练
边界问题要主动输入坏数据,确认程序能给出明确结果。
训练 1:空值边界
string NormalizeName(string? name)
{
if (string.IsNullOrWhiteSpace(name))
throw new ArgumentException("名称不能为空", nameof(name));
return name.Trim();
}分别传入 null、空字符串、空格和正常名称。
训练 2:数值边界
bool IsValidStock(int stock)
{
return stock >= 0 && stock <= 1_000_000;
}测试 -1、0、1_000_000、1_000_001。
训练 3:安全执行并转换提示
try
{
await service.SaveAsync(product);
StatusMessage = "保存成功";
}
catch (ArgumentException ex)
{
StatusMessage = ex.Message;
}
catch (Exception ex)
{
logger.LogError(ex, "保存失败");
StatusMessage = "系统错误,请稍后重试";
}第三遍为文件写入失败增加专门处理。
每日小测
做完本页后,用这 5 题检查是否真的掌握。
1. 判断题
本页的目标不是只把代码运行起来,还要能说清楚“为什么这样写”。
答案:对。能运行只是第一步,能解释原理、常用操作和常见错误,才说明本页内容进入了可复用能力。
2. 填空题
本页主题是:边界与异常。今天至少要掌握的 3 个点是:
1. 什么是边界场景
2. 输入错误、业务失败、系统异常有什么区别
3. 为什么优先用校验处理用户输入答案:以上 3 点必须能用自己的代码跑通,不能只停留在阅读。
3. 流程题
遇到本页相关功能时,先按什么顺序处理?
答案:先看完整例子,确认最终效果;再读原理和名词;然后跟着第三部分从空项目敲代码;最后对照作业答案检查。
4. 找错误题
如果本页代码运行失败,第一步应该做什么?
答案:先看终端或 IDE 里的第一条错误,找到文件名和行号;不要同时改很多地方。再回到本页的“常见错误和修法”表格,对照错误类型逐项排查。
5. 改需求题
在本页完整例子跑通后,至少改一个小需求。
可选改法:
- 改一个字段名称。
- 多加一个校验条件。
- 多输出一行结果。
- 把固定数据改成用户输入。
- 把一次处理改成多条数据处理。
答案标准:修改后能重新运行,并能说明这次修改影响了哪一段逻辑。重点检查:什么是边界场景。
上位机专项练习
最后一周先冻结边界:完成能交付的最小上位机,不在收尾阶段继续无限加功能。
下面 3 个例子都要亲手敲。先运行原代码,再完成每个例子后面的改动任务。
专项例子 1:冻结功能清单
本版包含:
- 模拟设备轮询
- 设备总览与详情
- 报警与确认
- 配置持久化
- 日志与审计
下版再做:
- 真实 Modbus
- SQLite 历史趋势运行结果或界面效果:
本版和下版边界清楚改动任务: 把你实际未完成项移到下版。
专项例子 2:拆分剩余任务
任务: 完成报警确认
输入: AlarmItem
完成标准: 点击后状态、人员、时间被保存
预计: 2 小时运行结果或界面效果:
任务有输入、完成标准和估时改动任务: 为发布测试写同样任务卡。
专项例子 3:只修阻塞问题
P0: 程序无法启动
P1: 设备数据错误、配置丢失
P2: 文字或间距问题
P3: 可选增强运行结果或界面效果:
优先级防止收尾失控改动任务: 给当前缺陷逐个标级。
第四部分:作业完整答案
作业要求:写一个边界检查程序,覆盖商品输入校验和文件读取异常。
答案文件:Program.cs
using System;
using System.Collections.Generic;
using System.IO;
List<string> ValidateProduct(string nameText, string priceText, string stockText)
{
var errors = new List<string>();
string name = nameText.Trim();
if (string.IsNullOrWhiteSpace(name))
{
errors.Add("商品名称不能为空。");
}
else if (name.Length > 50)
{
errors.Add("商品名称不能超过 50 个字符。");
}
if (!decimal.TryParse(priceText, out decimal price))
{
errors.Add("价格必须是数字。");
}
else if (price <= 0)
{
errors.Add("价格必须大于 0。");
}
if (!int.TryParse(stockText, out int stock))
{
errors.Add("库存必须是整数。");
}
else if (stock < 0)
{
errors.Add("库存不能小于 0。");
}
return errors;
}
void CheckProduct(string nameText, string priceText, string stockText)
{
List<string> errors = ValidateProduct(nameText, priceText, stockText);
if (errors.Count == 0)
{
Console.WriteLine($"保存成功:{nameText.Trim()}");
return;
}
Console.WriteLine("保存失败:");
foreach (string error in errors)
{
Console.WriteLine($"- {error}");
}
}
void ReadFileSafely(string path)
{
try
{
string text = File.ReadAllText(path);
Console.WriteLine(text);
}
catch (FileNotFoundException)
{
Console.WriteLine($"读取失败:文件不存在,路径:{path}");
}
catch (Exception ex)
{
Console.WriteLine($"读取失败:{ex.Message}");
}
}
CheckProduct("Keyboard", "199", "10");
CheckProduct("", "199", "10");
CheckProduct(new string('A', 60), "199", "10");
CheckProduct("Mouse", "abc", "10");
CheckProduct("Mouse", "-1", "10");
CheckProduct("Monitor", "899", "abc");
CheckProduct("Monitor", "899", "-1");
ReadFileSafely("missing-products.json");
验收结果
运行后必须看到:
- 合法商品保存成功。
- 空名称被拦住。
- 超长名称被拦住。
- 非数字价格被拦住。
- 小于等于 0 的价格被拦住。
- 非整数库存被拦住。
- 小于 0 的库存被拦住。
- 文件不存在有明确提示。
为什么这个答案是对的
这个答案把边界处理分成了两类:
| 问题 | 处理方式 |
|---|---|
| 用户输入错误 | ValidateProduct 收集错误 |
| 文件系统异常 | try/catch 捕获并提示 |
交付前要做的不是继续加功能,而是把这些失败情况都变成可理解、可处理的结果。