Skip to Content
Week 12Day 1 - 边界与异常

Day 1 - 边界与异常

建议用时:180-210 分钟

你将学会什么

  • 什么是边界场景
  • 输入错误、业务失败、系统异常有什么区别
  • 为什么优先用校验处理用户输入
  • 什么时候才用 try/catch
  • 怎么一次输出多个错误
  • 怎么把崩溃变成可理解的失败

交付前最后一周,重点不是继续加功能,而是把已有功能变稳。今天先检查边界:空值、超长、格式错误、负数、文件不存在。

本页固定顺序

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

今天只抓住 3 件事

  1. 输入错误优先用校验处理,不要一上来就靠 try/catch
  2. 业务失败要返回明确错误,让调用方知道哪里不合格。
  3. 系统异常要捕获、提示、记录,不能让程序直接崩掉。

学习衔接

上一页学习的是“复盘与巩固”,今天继续学习“边界与异常”。先使用上一页已经会的写法,再只增加今天这个新知识点;如果前置内容还不能独立敲出,先回上一页复习,不要硬跳。

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

这一部分从最小知识开始。先读解释,再把紧跟着的短例子敲一遍。边界处理的第一步,是把错误分清类型。

三类问题

类型例子推荐处理
输入错误价格是 abcTryParse + 提示
业务失败没有删除权限返回失败结果
系统异常文件不存在、权限不足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 步:运行检查

运行后确认:

  1. 合法商品成功。
  2. 空名称失败。
  3. 超长名称失败。
  4. 错误价格失败。
  5. 负库存失败。
  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; }

测试 -101_000_0001_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");

验收结果

运行后必须看到:

  1. 合法商品保存成功。
  2. 空名称被拦住。
  3. 超长名称被拦住。
  4. 非数字价格被拦住。
  5. 小于等于 0 的价格被拦住。
  6. 非整数库存被拦住。
  7. 小于 0 的库存被拦住。
  8. 文件不存在有明确提示。

为什么这个答案是对的

这个答案把边界处理分成了两类:

问题处理方式
用户输入错误ValidateProduct 收集错误
文件系统异常try/catch 捕获并提示

交付前要做的不是继续加功能,而是把这些失败情况都变成可理解、可处理的结果。