====== 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 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` 的自定义类。