普通视图

发现新文章,点击刷新页面。
昨天 — 2025年12月7日首页

代码的“病历本”:深入解读C#常见异常

作者 烛阴
2025年12月7日 12:32

异常的常见几种类型

1. NullReferenceException:空指针引用异常

```csharp
// 场景1: 未初始化的对象
List<string> names = null;
Console.WriteLine(names.Count); // 异常: names 是 null

// 场景2: 方法返回了 null
string FindUserById(int id)
{
    // 假设数据库里没找到id=999的用户
    if (id == 999) return null;
    return "Admin";
}
string user = FindUserById(999);
Console.WriteLine(user.ToUpper()); // 异常: user 是 null
```
  • 解决方案:
    1. 防御性检查:在访问任何可能为 null 的对象前,进行显式的 null 检查。
      if (user != null) {
          Console.WriteLine(user.ToUpper());
      }
      
    2. 空条件运算符 ?.??
      // 如果 user 是 null,整个表达式直接返回 null,而不是抛出异常
      string upperUser = user?.ToUpper(); 
      
      // 如果 user 是 null,则使用 "Guest" 作为默认值
      string displayName = user ?? "Guest"; 
      
      // 组合使用
      Console.WriteLine(user?.ToUpper() ?? "USER NOT FOUND");
      
    3. 可空引用类型:通过静态分析,在编译时就警告你潜在的 NullReferenceException
      #nullable enable // 开启可空引用类型检查
      string? user = FindUserById(999); // ? 表示 user 可以为 null
      Console.WriteLine(user.ToUpper()); // 编译器会在这里发出警告!
      

2. IndexOutOfRangeException:索引越界异常

  • 当你试图用一个无效的索引来访问数组、列表(List<T>)或其他基于索引的集合的元素时,此异常就会被抛出。无效索引指的是小于0,或大于等于集合的元素数量。

    int[] scores = { 98, 76, 100 };
    // 场景1: 访问不存在的索引
    Console.WriteLine(scores[3]); //  有效索引是 0, 1, 2
    
    // 场景2: 循环条件错误
    for (int i = 0; i <= scores.Length; i++) { // 错误在于 i <= Length
        Console.WriteLine(scores[i]); // 当 i 等于 scores.Length (3) 时 BOOM!
    }
    
    // 场景3: 对空集合进行索引访问
    var emptyList = new List<string>();
    Console.WriteLine(emptyList[0]); // BOOM!
    
  • 解决方案:

    1. 正确的循环:在 for 循环中,永远使用 < 而不是 <= 来比较索引和集合长度。
      for (int i = 0; i < scores.Length; i++) { /* 安全 */ }
      
    2. 优先使用 foreachforeach 循环在内部处理了迭代逻辑,完全避免了手动操作索引,从而根除了此类错误。
      foreach (var score in scores) { /* 绝对安全 */ }
      
    3. 边界检查:在直接通过索引访问前,检查索引是否在有效范围内。
      int index = GetUserInput();
      if (index >= 0 && index < scores.Length) {
          Console.WriteLine(scores[index]);
      } else {
          Console.WriteLine("索引无效!");
      }
      

3. FormatException:格式异常

  • 当一个方法的参数格式不符合预期时抛出,最常见于字符串向其他数据类型(如数字、日期)的转换。

    string userInput = "twelve";
    int number = int.Parse(userInput); // "twelve" 不是有效的整数格式
    
    string dateString = "2023-30-01"; // 无效的日期 (30月)
    DateTime date = DateTime.Parse(dateString); //
    
  • 解决方案:

    1. 使用 TryParse 模式:这是应对 FormatException最佳实践TryParse 方法会尝试转换,如果成功,返回 true 并通过 out 参数提供结果;如果失败,返回 false不会抛出异常。这遵循了“先看后跳”(LBYL)的原则。
      string userInput = Console.ReadLine();
      if (int.TryParse(userInput, out int number)) {
          Console.WriteLine($"转换成功: {number}");
      } else {
          Console.WriteLine("输入无效,请输入一个数字。");
      }
      

3.1 ArgumentException 家族 (ArgumentNullException, ArgumentOutOfRangeException)

  • ArgumentException:通用的参数错误,表示传递给方法的某个参数不合法。

  • ArgumentNullExceptionArgumentException 的子类,特指一个不应为 null 的参数被传入了 null

  • ArgumentOutOfRangeExceptionArgumentException 的子类,特指一个参数的值超出了可接受的范围。

  • 示例:

    public void SetUserName(string name) 
    {
        // 卫语句:主动防御,而不是等着用到 name 时再爆炸
        if (string.IsNullOrWhiteSpace(name)) 
        {
            throw new ArgumentException("用户名不能为空或仅包含空白字符。", nameof(name));
        }
        this.UserName = name;
    }
    
    public void SetDiscount(double percentage) 
    {
        if (percentage < 0 || percentage > 1) 
        {
            throw new ArgumentOutOfRangeException(nameof(percentage), "折扣必须在0和1之间。");
        }
        this.Discount = percentage;
    }
    
    // 调用者犯错
    myObject.SetUserName(null); // 会立即捕获到 ArgumentException,而不是后面的 NullReferenceException
    

4. InvalidCastException:无效转换异常

  • 在运行时执行了一个显式的类型转换,但源类型无法被转换为目标类型时发生

  • 示例:

    object myObject = "Hello World";
    // 这是一个字符串,不能被强制转换为一个 StringBuilder
    StringBuilder sb = (StringBuilder)myObject; 
    
  • 解决方案:

    1. 使用 as 运算符as 运算符尝试进行转换,如果成功,返回转换后的对象;如果失败,它会返回 null不是抛出异常。然后你可以配合 null 检查来安全地执行后续操作。
      StringBuilder sb = myObject as StringBuilder;
      if (sb != null) {
          sb.Append("...");
      } else {
          Console.WriteLine("对象不是 StringBuilder 类型。");
      }
      
    2. 使用 is 运算符和模式匹配is 运算符检查一个对象是否兼容某个类型
      if (myObject is StringBuilder sb) {
          sb.Append("...");
      }
      

    as/is vs. 强制转换:与 TryParse vs. Parse 类似,如果你不确定转换能否成功,就应该使用 asis。只有在你100%确定类型兼容时,才使用强制转换,因为它能更早地暴露逻辑错误。

5. InvalidOperationException:无效操作异常

  • 当方法调用对于对象的当前状态无效时抛出。错误不在于参数,而在于对象“还没准备好”或“已处于不当状态”。

  • 示例:

    // 场景1: 修改正在迭代的集合
    var numbers = new List<int> { 1, 2, 3, 4 };
    foreach (var number in numbers) {
        if (number == 2) {
            numbers.Remove(number); //  不能在 foreach 循环中修改集合
        }
    }
    
    // 场景2: 使用已耗尽的迭代器
    var enumerator = numbers.GetEnumerator();
    while(enumerator.MoveNext()) { }
    // 迭代器已到末尾
    Console.WriteLine(enumerator.Current); // BOOM! (或返回默认值,取决于实现)
    

6. IOException 家族 (FileNotFoundException, DirectoryNotFoundException, etc.)

  • 所有与输入/输出(I/O)操作相关的错误的基类。
    • FileNotFoundException:尝试访问一个不存在的文件。
    • DirectoryNotFoundException:文件路径中的某个目录不存在。
    • UnauthorizedAccessException:程序没有足够的权限去访问文件或目录。
  • 核心特点:这类异常是典型的必须使用 try-catch 来处理的场景。因为即使你在操作前用 File.Exists 检查,也无法保证在检查和实际操作之间的瞬间,文件不会被用户或其他程序删除(这被称为“竞态条件”)。
  • 最佳实践
    try
    {
        string content = File.ReadAllText(@"C:\secret\data.txt");
        // ... process content ...
    }
    catch (FileNotFoundException ex)
    {
        Console.WriteLine("错误:文件未找到。请确认文件路径是否正确。");
    }
    catch (UnauthorizedAccessException ex)
    {
        Console.WriteLine("错误:权限不足。请尝试以管理员身份运行程序。");
    }
    catch (IOException ex) // 兜底处理其他I/O错误
    {
        Console.WriteLine($"发生了一个I/O错误: {ex.Message}");
    }
    

结语

点个赞,关注我获取更多实用 C# 技术干货!如果觉得有用,记得收藏本文

昨天以前首页
❌
❌