《前后端面试题》专栏集合了前后端各个知识模块的面试题,包括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等内置类型)。
- 注意: