《前后端面试题》专栏集合了前后端各个知识模块的面试题,包括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块执行完毕");
        }
    }
}执行流程:
- 正常执行:try块代码全部执行 → 跳过catch块 → 执行finally块。
- 发生异常:try块中异常点后的代码停止执行 → 匹配的catch块执行 → 执行finally块。
- 未捕获异常:try块停止执行 → 无匹配catch→ 执行finally块 → 异常向上传播。
关键点:
- 一个try块可以搭配多个catch块(按异常类型从具体到抽象排序)。
- finally块不是必需的,但通常用于释放资源(如文件句柄、数据库连接)。
- 异常处理机制保证了程序在出错时仍能优雅地处理,而非直接崩溃。
42. throw和throw ex的区别
throw和throw 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类,用于区分不同类型的错误,使异常处理更精确。
创建自定义异常的规范:
- 类名以Exception结尾(如InvalidOrderException)。
- 继承自Exception(直接或间接)。
- 实现三个构造函数:
- 无参构造函数。
- 带消息的构造函数。
- 带消息和内部异常的构造函数(支持异常链)。
 
- 标记为[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#仅支持非检查异常,即编译器不强制要求捕获或声明任何异常。不过,某些异常通常被视为"不应该显式捕获"的类型,因为它们代表了严重错误或编程错误。
通常不需要显式捕获的异常类型:
- NullReferenceException:引用空对象时抛出,通常是编程错误(未初始化对象)。
- IndexOutOfRangeException:数组索引超出范围,属于编程错误。
- ArgumentNullException:方法参数为- null但不允许,通常是调用方错误。
- ArgumentOutOfRangeException:参数值超出有效范围,属于调用方错误。
- InvalidCastException:类型转换失败,通常是编程错误。
- DivideByZeroException:除以零,属于逻辑错误。
- StackOverflowException:栈溢出,通常是递归过深等严重错误,无法有效处理。
- OutOfMemoryException:内存不足,严重错误,难以恢复。
- 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()。
使用方式:
- 声明式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()
}- 语句式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语句仍有效,但通常不推荐这样做。
适用场景:
- 文件操作(FileStream、StreamReader)。
- 数据库连接(SqlConnection、DbContext)。
- 网络资源(HttpClient、Socket)。
- 任何需要显式释放的非托管资源。
46. 如何处理多线程中的异常?
- 
原理说明 :多线程环境中,线程抛出的异常若未捕获,可能导致程序崩溃。不同线程模型(如 Thread、Task)的异常处理方式不同:- Thread类:异常需在线程内部捕获,外部无法直接捕获。
- Task类(.NET 4.0+):异常会被包装为- AggregateException,可通过- Wait()、- Result或- GetAwaiter().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中专门用于包装多个异常 的类型,常见于并行操作(如Task、Parallel)中,当多个任务同时抛出异常时,所有异常会被汇总到AggregateException中。
- 
主要作用 : - 统一管理多任务中的多个异常,避免单个异常覆盖其他异常。
- 通过InnerExceptions属性获取所有异常列表。
- 提供Handle()方法批量处理内部异常。
 
- 
示例代码 : csharptry { // 并行执行多个可能抛异常的任务 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)。
- 
实现方式 : - 类实现IDisposable接口,在Dispose()方法中释放非托管资源。
- 用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的对象(如FileStream、SqlConnection等内置类型)。
 
- 注意: