C#面试题及详细答案120道(41-50)-- 异常处理

前后端面试题》专栏集合了前后端各个知识模块的面试题,包括html,javascript,css,vue,react,java,Openlayers,leaflet,cesium,mapboxGL,threejs,nodejs,mangoDB,SQL,Linux... 。

前后端面试题-专栏总目录

文章目录

  • 一、本文面试题目录
      • 41. C#中的异常处理机制是什么?try/catch/finally的作用
      • [42. throw和throw ex的区别](#42. throw和throw ex的区别)
      • [43. 什么是自定义异常?如何创建和使用?](#43. 什么是自定义异常?如何创建和使用?)
      • [44. 哪些异常不需要显式捕获(非检查异常)?](#44. 哪些异常不需要显式捕获(非检查异常)?)
      • [45. using语句的作用是什么?与IDisposable接口的关系](#45. using语句的作用是什么?与IDisposable接口的关系)
      • [46. 如何处理多线程中的异常?](#46. 如何处理多线程中的异常?)
      • [47. 异常处理对性能有什么影响?](#47. 异常处理对性能有什么影响?)
      • [48. 简述AggregateException的作用](#48. 简述AggregateException的作用)
      • [49. 什么情况下应该使用异常?什么情况下不应该?](#49. 什么情况下应该使用异常?什么情况下不应该?)
      • [50. 如何在代码中实现资源的自动释放?](#50. 如何在代码中实现资源的自动释放?)
  • 二、120道C#面试题目录列表

一、本文面试题目录

41. C#中的异常处理机制是什么?try/catch/finally的作用

C#的异常处理机制通过try/catch/finally语句块捕获和处理程序运行时的错误,防止程序崩溃并提供错误恢复的机会。其核心思想是将可能引发错误的代码与处理错误的代码分离。

各部分作用

  • try块:包含可能引发异常的代码,是异常检测的范围。
  • catch块 :捕获并处理try块中抛出的异常,可指定捕获特定类型的异常。
  • finally块:无论是否发生异常,都会执行的代码,通常用于释放资源。

使用示例

csharp 复制代码
public class ExceptionHandlingExample
{
    public static void ReadFile(string path)
    {
        FileStream stream = null;
        try
        {
            // 可能引发异常的代码
            stream = new FileStream(path, FileMode.Open);
            var reader = new StreamReader(stream);
            Console.WriteLine(reader.ReadToEnd());
        }
        // 捕获特定异常
        catch (FileNotFoundException ex)
        {
            Console.WriteLine($"文件未找到: {ex.Message}");
        }
        // 捕获另一类异常
        catch (IOException ex)
        {
            Console.WriteLine($"IO错误: {ex.Message}");
        }
        // 捕获所有其他异常(通常不推荐)
        catch (Exception ex)
        {
            Console.WriteLine($"发生错误: {ex.Message}");
        }
        finally
        {
            // 确保资源释放,无论是否发生异常
            stream?.Dispose();
            Console.WriteLine("finally块执行完毕");
        }
    }
}

执行流程

  1. 正常执行:try块代码全部执行 → 跳过catch块 → 执行finally块。
  2. 发生异常:try块中异常点后的代码停止执行 → 匹配的catch块执行 → 执行finally块。
  3. 未捕获异常:try块停止执行 → 无匹配catch → 执行finally块 → 异常向上传播。

关键点

  • 一个try块可以搭配多个catch块(按异常类型从具体到抽象排序)。
  • finally块不是必需的,但通常用于释放资源(如文件句柄、数据库连接)。
  • 异常处理机制保证了程序在出错时仍能优雅地处理,而非直接崩溃。

42. throw和throw ex的区别

throwthrow ex都用于抛出异常,但它们在保留异常堆栈信息方面有本质区别,这对调试至关重要。

throw

  • 重新抛出当前捕获的异常,保留原始堆栈跟踪信息
  • 堆栈跟踪会包含异常最初发生的位置,以及重新抛出的位置。

throw ex

  • 重新抛出异常,但重置堆栈跟踪,将当前位置作为异常的起始点。
  • 丢失了原始异常发生的上下文信息,不利于调试。

示例代码

csharp 复制代码
public class ThrowExample
{
    public static void Method1()
    {
        try
        {
            Method2();
        }
        catch (Exception ex)
        {
            Console.WriteLine("Method1中捕获异常:");
            Console.WriteLine(ex.StackTrace); // 打印堆栈跟踪
        }
    }
    
    public static void Method2()
    {
        try
        {
            Method3();
        }
        catch (Exception ex)
        {
            // throw ex; // 重置堆栈跟踪
            throw;      // 保留原始堆栈跟踪
        }
    }
    
    public static void Method3()
    {
        throw new InvalidOperationException("在Method3中发生错误");
    }
}

执行结果对比

  • 使用throw时,堆栈跟踪会显示异常起源于Method3,经过Method2重新抛出,最后在Method1捕获:

    复制代码
    在Method3中发生错误
       在 ThrowExample.Method3() 位置...
       在 ThrowExample.Method2() 位置...
       在 ThrowExample.Method1() 位置...
  • 使用throw ex时,堆栈跟踪会从Method2开始,丢失Method3的原始信息:

    复制代码
    在Method3中发生错误
       在 ThrowExample.Method2() 位置...  // 丢失了Method3的调用信息
       在 ThrowExample.Method1() 位置...

最佳实践

  • 当需要重新抛出异常(如在记录日志后),使用throw以保留完整堆栈信息。
  • 避免使用throw ex,除非明确需要截断堆栈跟踪(极少情况)。
  • 示例场景:在catch块中记录异常日志后,用throw将异常继续向上传播。

43. 什么是自定义异常?如何创建和使用?

自定义异常 是根据业务需求创建的特定异常类型,继承自Exception类,用于区分不同类型的错误,使异常处理更精确。

创建自定义异常的规范

  1. 类名以Exception结尾(如InvalidOrderException)。
  2. 继承自Exception(直接或间接)。
  3. 实现三个构造函数:
    • 无参构造函数。
    • 带消息的构造函数。
    • 带消息和内部异常的构造函数(支持异常链)。
  4. 标记为[Serializable](支持序列化,用于跨应用域传递)。

示例:创建自定义异常

csharp 复制代码
[Serializable]
public class InvalidOrderException : Exception
{
    // 无参构造函数
    public InvalidOrderException() : base("订单无效") { }
    
    // 带消息的构造函数
    public InvalidOrderException(string message) : base(message) { }
    
    // 带消息和内部异常的构造函数
    public InvalidOrderException(string message, Exception innerException) 
        : base(message, innerException) { }
    
    // 序列化支持(必要时)
    protected InvalidOrderException(System.Runtime.Serialization.SerializationInfo info, 
                                   System.Runtime.Serialization.StreamingContext context) 
        : base(info, context) { }
    
    // 可添加自定义属性
    public int OrderId { get; set; }
}

使用自定义异常

csharp 复制代码
public class OrderProcessor
{
    public void ProcessOrder(int orderId)
    {
        try
        {
            if (orderId <= 0)
            {
                // 抛出自定义异常
                throw new InvalidOrderException("订单ID必须为正数") 
                { 
                    OrderId = orderId 
                };
            }
            
            // 处理订单逻辑...
            if (orderId == 999)
            {
                try
                {
                    // 模拟内部错误
                    throw new IOException("数据库连接失败");
                }
                catch (IOException ex)
                {
                    // 包装异常(异常链)
                    throw new InvalidOrderException("处理订单时发生数据错误", ex)
                    {
                        OrderId = orderId
                    };
                }
            }
        }
        catch (InvalidOrderException)
        {
            // 可以选择在此处理,或继续向上抛出
            throw; // 继续传播
        }
    }
}

// 调用方处理
public static void Main()
{
    var processor = new OrderProcessor();
    try
    {
        processor.ProcessOrder(-1);
    }
    // 精确捕获自定义异常
    catch (InvalidOrderException ex)
    {
        Console.WriteLine($"处理订单 {ex.OrderId} 失败: {ex.Message}");
        if (ex.InnerException != null)
        {
            Console.WriteLine($"内部错误: {ex.InnerException.Message}");
        }
    }
}

优势

  • 使异常类型与业务逻辑紧密关联,提高代码可读性。
  • 允许调用方精确捕获特定异常,进行针对性处理。
  • 可携带额外信息(如OrderId),便于错误诊断。

44. 哪些异常不需要显式捕获(非检查异常)?

在C#中,异常分为非检查异常(Unchecked Exceptions)检查异常(Checked Exceptions),但C#仅支持非检查异常,即编译器不强制要求捕获或声明任何异常。不过,某些异常通常被视为"不应该显式捕获"的类型,因为它们代表了严重错误或编程错误。

通常不需要显式捕获的异常类型

  1. NullReferenceException:引用空对象时抛出,通常是编程错误(未初始化对象)。
  2. IndexOutOfRangeException:数组索引超出范围,属于编程错误。
  3. ArgumentNullException :方法参数为null但不允许,通常是调用方错误。
  4. ArgumentOutOfRangeException:参数值超出有效范围,属于调用方错误。
  5. InvalidCastException:类型转换失败,通常是编程错误。
  6. DivideByZeroException:除以零,属于逻辑错误。
  7. StackOverflowException:栈溢出,通常是递归过深等严重错误,无法有效处理。
  8. OutOfMemoryException:内存不足,严重错误,难以恢复。
  9. AccessViolationException:访问无效内存,通常是不安全代码导致的严重错误。

示例:不推荐捕获的情况

csharp 复制代码
public void BadPractice()
{
    // 不推荐:捕获编程错误类异常
    try
    {
        string text = null;
        int length = text.Length; // 会抛出NullReferenceException
    }
    // 不推荐:掩盖了明显的编程错误
    catch (NullReferenceException)
    {
        Console.WriteLine("发生了错误"); // 无法有效恢复
    }
}

原因

  • 这些异常通常由代码缺陷导致(如逻辑错误、参数校验缺失),而非运行时环境问题。
  • 捕获它们可能掩盖潜在的编程错误,导致调试困难。
  • 大多数情况下,这些异常无法在运行时有效恢复,应通过修正代码避免。

处理原则

  • 对于编程错误类异常(如NullReferenceException),应通过代码审查和测试消除,而非捕获。
  • 对于可能恢复的异常(如FileNotFoundException),应显式捕获并处理。
  • 可以在应用程序顶层(如全局异常处理)捕获所有未处理异常,记录日志并友好提示用户。

全局异常处理示例

csharp 复制代码
// 控制台应用
public static void Main()
{
    try
    {
        // 应用程序入口
        RunApplication();
    }
    catch (Exception ex)
    {
        // 记录所有未处理异常
        Log.Fatal("应用程序崩溃", ex);
        Console.WriteLine("发生未预期错误,请联系管理员");
    }
}

// ASP.NET Core
public void Configure(IApplicationBuilder app)
{
    app.UseExceptionHandler(errorApp =>
    {
        errorApp.Run(async context =>
        {
            // 处理全局异常
            var exception = context.Features.Get<IExceptionHandlerFeature>().Error;
            Log.Error("未处理异常", exception);
            context.Response.StatusCode = 500;
            await context.Response.WriteAsync("服务器内部错误");
        });
    });
}

45. using语句的作用是什么?与IDisposable接口的关系

using语句用于确保实现了IDisposable接口的对象在使用后被正确释放资源,是一种简化资源管理的语法糖。

核心作用

  • 自动调用对象的Dispose()方法,释放非托管资源(如文件句柄、数据库连接、网络连接等)。
  • 即使发生异常,也能保证资源释放,替代了try/finally的繁琐写法。

与IDisposable接口的关系

  • using语句仅能用于实现IDisposable接口的类型。
  • IDisposable接口定义了Dispose()方法,用于释放资源。
  • using语句的编译结果本质是try/finally块,在finally中调用Dispose()

使用方式

  1. 声明式using(推荐)
csharp 复制代码
// 语法:using (资源对象创建) { 使用资源 }
public void ReadFile(string path)
{
    // 创建实现IDisposable的对象
    using (var stream = new FileStream(path, FileMode.Open))
    using (var reader = new StreamReader(stream))
    {
        // 使用资源
        string content = reader.ReadToEnd();
        Console.WriteLine(content);
    } // 离开作用域时自动调用Dispose()
}
  1. 语句式using(C# 8.0+)
csharp 复制代码
public void ReadFileModern(string path)
{
    // 无需大括号,作用域为当前方法
    using var stream = new FileStream(path, FileMode.Open);
    using var reader = new StreamReader(stream);
    
    string content = reader.ReadToEnd();
    Console.WriteLine(content);
    // 方法结束时自动调用Dispose()
}

编译后的等效代码

csharp 复制代码
public void ReadFile(string path)
{
    FileStream stream = null;
    try
    {
        stream = new FileStream(path, FileMode.Open);
        StreamReader reader = null;
        try
        {
            reader = new StreamReader(stream);
            string content = reader.ReadToEnd();
            Console.WriteLine(content);
        }
        finally
        {
            reader?.Dispose(); // 释放reader
        }
    }
    finally
    {
        stream?.Dispose(); // 释放stream
    }
}

注意事项

  • using语句中的对象必须实现IDisposable接口,否则编译错误。
  • 多个using语句可以嵌套或并列(如示例1)。
  • 不要在using块外部使用已被Dispose()的对象(可能导致异常)。
  • 值类型(如struct)实现IDisposable时,using语句仍有效,但通常不推荐这样做。

适用场景

  • 文件操作(FileStreamStreamReader)。
  • 数据库连接(SqlConnectionDbContext)。
  • 网络资源(HttpClientSocket)。
  • 任何需要显式释放的非托管资源。

46. 如何处理多线程中的异常?

  • 原理说明 :多线程环境中,线程抛出的异常若未捕获,可能导致程序崩溃。不同线程模型(如ThreadTask)的异常处理方式不同:

    • Thread类:异常需在线程内部捕获,外部无法直接捕获。
    • Task类(.NET 4.0+):异常会被包装为AggregateException,可通过Wait()ResultGetAwaiter().GetResult()捕获,也可在async/await中直接用try/catch
  • 示例代码

    csharp 复制代码
    // 1. Thread类处理异常(必须在内部捕获)
    var thread = new Thread(() => {
        try {
            throw new Exception("线程内部异常");
        }
        catch (Exception ex) {
            Console.WriteLine($"线程内捕获:{ex.Message}");
        }
    });
    thread.Start();
    
    // 2. Task类处理异常(外部捕获)
    try {
        var task = Task.Run(() => {
            throw new Exception("Task内部异常");
        });
        task.Wait(); // 触发AggregateException
    }
    catch (AggregateException ex) {
        // 解开包装的实际异常
        ex.Handle(e => {
            Console.WriteLine($"捕获到Task异常:{e.Message}");
            return true;
        });
    }
    
    // 3. async/await处理异常
    async Task TestAsync() {
        try {
            await Task.Run(() => {
                throw new Exception("Async异常");
            });
        }
        catch (Exception ex) {
            Console.WriteLine($"Async捕获:{ex.Message}");
        }
    }

47. 异常处理对性能有什么影响?

  • 原理说明 :异常处理的性能损耗主要体现在异常抛出时 ,而非try/catch结构本身:

    • try块本身几乎不影响性能,编译器仅标记异常处理范围。
    • 异常抛出时,CLR需收集调用栈信息、查找匹配的catch块,此过程耗时(可能比正常流程慢1000倍以上)。
    • 频繁抛出异常会显著降低程序性能,尤其在循环或高频调用场景中。
  • 示例与建议

    csharp 复制代码
    // 性能差:频繁抛出异常
    for (int i = 0; i < 1000; i++) {
        try {
            if (i % 2 == 0) throw new Exception();
        }
        catch { /* 处理 */ }
    }
    
    // 性能好:用条件判断避免异常
    for (int i = 0; i < 1000; i++) {
        if (i % 2 == 0) {
            // 直接处理,不抛异常
        }
    }
    • 建议:异常仅用于意外错误,可预见的情况(如参数验证)用条件判断处理。

48. 简述AggregateException的作用

  • 原理说明AggregateException是.NET中专门用于包装多个异常 的类型,常见于并行操作(如TaskParallel)中,当多个任务同时抛出异常时,所有异常会被汇总到AggregateException中。

  • 主要作用

    • 统一管理多任务中的多个异常,避免单个异常覆盖其他异常。
    • 通过InnerExceptions属性获取所有异常列表。
    • 提供Handle()方法批量处理内部异常。
  • 示例代码

    csharp 复制代码
    try {
        // 并行执行多个可能抛异常的任务
        var task1 = Task.Run(() => throw new Exception("任务1失败"));
        var task2 = Task.Run(() => throw new Exception("任务2失败"));
        Task.WaitAll(task1, task2);
    }
    catch (AggregateException ex) {
        // 遍历所有内部异常
        foreach (var innerEx in ex.InnerExceptions) {
            Console.WriteLine($"捕获异常:{innerEx.Message}");
        }
    
        // 用Handle()处理异常(返回true表示已处理)
        ex.Handle(innerEx => {
            Console.WriteLine($"处理异常:{innerEx.Message}");
            return true;
        });
    }

49. 什么情况下应该使用异常?什么情况下不应该?

  • 应该使用异常的情况

    • 意外错误:如文件不存在、网络中断、数据库连接失败等超出正常流程的错误。
    • 不可恢复的错误:如内存不足、权限不足等导致功能无法继续执行的情况。
    • 跨层错误传递:在多层架构中(如业务层到UI层),用异常传递错误信息更简洁。
  • 不应该使用异常的情况

    • 可预见的控制流:如输入验证("用户名不能为空"应通过条件判断提示,而非抛异常)。
    • 性能敏感场景:高频操作(如循环)中抛异常会严重影响性能。
    • 正常业务逻辑分支:如"用户登录失败(密码错误)"属于预期结果,无需抛异常。
  • 示例对比

    csharp 复制代码
    // 不推荐:用异常处理可预见情况
    bool IsPositive(int num) {
        try {
            if (num <= 0) throw new ArgumentException();
            return true;
        }
        catch { return false; }
    }
    
    // 推荐:用条件判断
    bool IsPositive(int num) {
        return num > 0;
    }

50. 如何在代码中实现资源的自动释放?

  • 原理说明 :资源(如文件句柄、数据库连接)需显式释放,.NET通过IDisposable接口定义释放逻辑,配合using语句可实现自动释放(编译时转为try/finally)。

  • 实现方式

    1. 类实现IDisposable接口,在Dispose()方法中释放非托管资源。
    2. using语句包裹资源对象,确保离开作用域时自动调用Dispose()
  • 示例代码

    csharp 复制代码
    // 1. 实现IDisposable的类
    public class ResourceHolder : IDisposable {
        private bool _disposed = false;
        private IntPtr _unmanagedResource; // 非托管资源(如文件句柄)
    
        public void Dispose() {
            Dispose(true);
            GC.SuppressFinalize(this); // 告诉GC无需调用析构函数
        }
    
        protected virtual void Dispose(bool disposing) {
            if (_disposed) return;
            if (disposing) {
                // 释放托管资源(如其他IDisposable对象)
            }
            // 释放非托管资源
            if (_unmanagedResource != IntPtr.Zero) {
                // 释放逻辑(如CloseHandle等)
                _unmanagedResource = IntPtr.Zero;
            }
            _disposed = true;
        }
    
        // 析构函数:仅用于释放非托管资源(防止Dispose未被调用)
        ~ResourceHolder() {
            Dispose(false);
        }
    }
    
    // 2. 使用using自动释放
    public void UseResource() {
        using (var resource = new ResourceHolder()) {
            // 使用资源
        } // 离开作用域时自动调用resource.Dispose()
    }
    • 注意:using可用于任何实现IDisposable的对象(如FileStreamSqlConnection等内置类型)。

二、120道C#面试题目录列表

文章序号 C#面试题120道
1 C#面试题及详细答案120道(01-10)
2 C#面试题及详细答案120道(11-20)
3 C#面试题及详细答案120道(21-30)
4 C#面试题及详细答案120道(31-40)
5 C#面试题及详细答案120道(41-50)
6 C#面试题及详细答案120道(51-60)
7 C#面试题及详细答案120道(61-75)
8 C#面试题及详细答案120道(76-85)
9 C#面试题及详细答案120道(86-95)
10 C#面试题及详细答案120道(96-105)
11 C#面试题及详细答案120道(106-115)
12 C#面试题及详细答案120道(116-120)
相关推荐
Hare_bai8 小时前
WPF的MVVM模式核心架构与实现细节
ui·架构·c#·wpf·交互·xaml·mvvm
星夜泊客8 小时前
C# 中的空条件运算符(?.)与空合并运算符(??)详解
c#·空条件运算符·语法糖
作孽就得先起床9 小时前
c#调Lua返回个字符串
unity·c#·lua·xlua
Jackson@ML9 小时前
在macOS上搭建C#集成开发环境指南
开发语言·macos·c#
月光双刀9 小时前
给旧版 .NET 也开一扇“私有之门”——ILAccess.Fody 实现原理与设计
c#·.net·fody·il·mono.cecil
张人玉9 小时前
WPF 静态样式与动态样式的定义及使用详解
ui·c#·wpf
山间点烟雨11 小时前
2. WPF程序打包成一个单独的exe文件
c#·wpf·独立打包exe
m0_7369270412 小时前
使用 Python 将 PowerPoint 转换为 Word 文档
java·开发语言·后端·职场和发展·c#
莫生灬灬12 小时前
[特殊字符] FBro工作流自动化平台 - 让浏览器自动化更简单
运维·chrome·c#·自动化