C# 中的 throw 关键字详解
在 C# 编程中,`throw` 关键字用于显式地引发异常。它不仅用于报告错误,还在控制程序流程和异常传播中扮演关键角色。
1. 基础用法
最基本的形式是抛出一个继承自 `System.Exception` 的对象实例。
public void ValidateUser(string name) { if (name == null) { // 基础用法:实例化并抛出 throw new ArgumentNullException(nameof(name), "用户名不能为空"); } }
注意:一旦 `throw` 语句执行,当前方法的后续代码将不再运行,控制权将转交给调用堆栈中最近的 `catch` 块。
2. 重新抛出异常 (Rethrowing)
这是 C# 开发中最容易出错的地方。当你在 `catch` 块中捕获异常并希望将其传递给上层调用者时,有两种写法,但结果截然不同。
2.1 正确的做法:throw;
使用不带参数的 `throw;` 可以保留原始异常的堆栈跟踪(Stack Trace)。
try { ProcessData(); } catch (Exception ex) { Log(ex); // ✅ 正确:保留原始堆栈信息,就像错误是在 ProcessData 内部发生的一样 throw; }
2.2 错误的做法:throw ex;
如果你抛出了捕获的异常变量(`throw ex;`),堆栈跟踪会被重置。
try { ProcessData(); } catch (Exception ex) { Log(ex); // ❌ 错误:堆栈跟踪被重置,看起来错误好像是发生在这行代码,而不是 ProcessData 内部 throw ex; }
3. throw 表达式 (C# 7.0+)
从 C# 7.0 开始,`throw` 不仅仅是一个语句,还可以作为一个表达式使用。这意味着你可以在赋值、条件运算符(三元运算符)或 Lambda 表达式中直接使用它。
3.1 在空合并运算符中
这是最常见的用法,用于简化参数验证。
public class Person { public string Name { get; } public Person(string name) { // 如果 name 为 null,直接抛出异常,否则赋值 Name = name ?? throw new ArgumentNullException(nameof(name)); } }
3.2 在三元运算符中
string GetGrade(int score) { return (score >= 0 && score <= 100) ? (score >= 60 ? "Pass" : "Fail") : throw new ArgumentOutOfRangeException(nameof(score), "分数必须在0到100之间"); }
3.3 在 Lambda 表达式主体中
Func<string, string> cleaner = s => s ?? throw new Exception("字符串不能为null");
4. 常见异常类型
在 C# 中使用 `throw` 时,应尽量抛出 .NET 框架提供的标准异常,而不是通用的 `Exception`。
| 异常类型 | 适用场景 |
|---|---|
| ArgumentNullException | 参数值为 null,但该方法不允许 null。 |
| ArgumentOutOfRangeException | 参数值超出了允许的范围(例如索引越界)。 |
| ArgumentException | 参数无效,但不属于上述两种情况。 |
| InvalidOperationException | 当对象处于不适合执行该方法的状态时(例如在连接关闭时尝试读取数据库)。 |
| NotImplementedException | 方法尚未实现(通常用于开发阶段)。 |
5. 最佳实践总结
- 保留堆栈:在 `catch` 块中重新抛出时,永远使用 `throw;` 而不是 `throw ex;`。
- 利用 nameof:在抛出参数相关异常时,使用 `nameof(parameterName)` 来获取参数名,这样重构代码时参数名会自动更新。
- `throw new ArgumentNullException(nameof(id));`
- 不要通过异常控制逻辑:异常应该用于“异常”情况,不要用 `throw` 来做普通的流程跳转(例如跳出循环),这会严重影响性能。
- 自定义异常:如果标准异常无法准确描述错误,可以创建继承自 `Exception` 的自定义类。
评论