|
异常的使用
不管是网络还是出版读物,关于 C# 异常系统性的资料都比较少,我所在的工控领域也很少有开发者使用异常。异常实际上是一种非常好的机制,很值得推广。为此我根据过往的学习积累,结合一些项目经验,撰写本文。
1. 为什么应该使用异常
在开始本文之前,我们先看一下常用的几种“报告错误”的方式:
缺点:
- 使用者不得不对返回值进行判断,导致“圈复杂度”增加;
- 如果方法需要返回值,返回内容不得不通过“out”参数传出。
下面是一段伪代码,我们可以看到使用错误码有诸多不便:
Client client = new Client();if (client.Connect() == 1){ if (client.Receive(out string content) == 1) { // 执行其他动作 }}class Client{ public int Connect() { // 执行其他动作 if (successeded) { // 执行成功,返回 1 return 1; } else { // 执行失败,返回错误码 return errorCode; } } public int Receive(out string result) { if (successeded) { result = result; return 1; } else { result = null; return errorCode; } }}
缺点:
- 需要对全局属性进行判断,使用者很容易遗漏;
- 同样会造成“圈复杂度”增加。
Client value = new Client();value.Connect();if (value.ErrorCode == 1){ string content = value.Receive(); if (value.ErrorCode == 1) { // 执行其他动作 }}class Client{ public int ErrorCode { get; private set; } public void Connect() { // 执行其他动作 if (successeded) { // 执行成功,全局属性置为 1 ErrorCode = 1; return; } else { // 执行失败,设置错误码 ErrorCode = errorCode; return; } } public string Receive() { if (successeded) { ErrorCode = 1; return result; } else { ErrorCode = errorCode; return null; } }}上述两种机制不光使用繁琐,返回错误码在部分没有返回值的场景下还无法使用:
而异常刚好可以弥补这些缺陷。
Info
《框架设计指南》这本书对异常的好处进行了详细的阐释,我在这里进行引述(有删改):
- 异常与面向对象语言结合精密。就构造函数、运算符重载和属性而言,开发者无法选择返回值。出于这个原因,对于面向对象的框架来说,基于返回值的错误报告是不可能标准化的。
- 异常促进了 API 的一致性,因为它们只被设计用于错误报告。相比之下,返回值有很多用途,错误报告只是其中一个子集。出于这个原因,尽管异常可以被限制在特定的模式中,然而通过返回值报告错误的 API 很可能会利用大量的模式。Win32 API 就是这种不一致的一个典型例子:它使用了 BOOL、HRESULTS 和 GetLastError 等。
- 在基于返回值的错误报告中,错误处理代码总是被放置在靠近故障点的地方。然而,对于异常处理,应用程序的开发者可以有自己的选择,他们既可以在故障点附近捕获异常,也可以将错误处理代码集中在调用栈的更上方。
- 错误处理代码更容易被本地化。如果通过返回值报告错误的代码非常健壮,则往往意味着几乎每一行功能代码中都有一个 if 语句。这些 if 语句用于处理失败的情况。有了基于异常的错误报告,通常可以这样书写健壮的代码:顺序执行多个方法或操作,然后在 try 语法块后面按组去处理错误,甚至可以在调用栈更高层级位置处理错误。
- 错误码很容易被忽略掉,并且在大多数情况下都是如此。
- 异常携带的丰富信息可描述导致错误的原因。
- 异常允许面向未经处理异常的处理器。
这里的“未经处理异常的处理器”指诸如 Application.DispatcherUnhandledException、Application.ThreadException 支持订阅全局的异常处理事件。
- 异常促进工具的发展。异常是一种定义明确的方法失败模型。正因为如此,调试器、分析器、性能计数器等工具才有可能密切关注异常。
2. 异常的使用示例
上一节的伪代码改为使用异常后如下:
try{ Client value = new Client(); value.Connect(); string content = value.Receive();}catch (Exception ex){ Console.WriteLine(ex.Message);}class Client{ public void Connect() { // 执行其他动作 if (successeded) { return; } else { // 执行失败,抛出异常 throw new InvalidOperationException("因...,连接失败。"); } } public string Receive() { if (successeded) { return result; } else { throw new InvalidOperationException("因...,接受数据失败。"); } }}前面我们还提到返回错误码无法在没有返回值的场景下使用,下面是在构造函数和属性 setter 中使用异常的一个简单示例:
class Client{ public bool IsConnected { get; private set; } private string _hostname; private int _port; // 通过异常限制 Hostname 和 Port 只能在未连接状态下修改。 public string Hostname { get => _hostname; set { if (IsConnected) { throw new InvalidOperationException("客户端处于连接状态,无法修改主机名称。"); } _hostname = value; } } public int Port { get => _port; set { if (IsConnected) { throw new InvalidOperationException("客户端处于连接状态,无法修改端口。"); } _port = value; } } public Client(string hostname, int port) { if (string.IsNullOrWhiteSpace(hostname)) { throw new ArgumentException("参数无效。", nameof(hostname)); } Hostname = hostname; Port = port; } public void Connect() { IsConnected = true; }}可以看到,通过异常反馈执行错误代码更为整洁,并且能通过异常的 Message 属性报告错误信息。
3. 应该抛出哪个异常?
对于异常,简单使用并不复杂。但是异常的种类繁多,开发者常常困惑应该抛出哪个异常。异常和错误相关,错误一般分为两类:
使用错误:错误调用导致的错误,例如传入了 null 参数。此类错误不应该由框架处理,而应该修改调用方代码。
此类错误对应的常用异常有 3 个:
- ArgumentException:ArgumentNullException 和 ArgumentOutOfRangeException 的基类,用于表示传入的参数错误
- ArgumentNullException:参数为空异常,当传入方法的参数为 null,应该抛出该异常
- ArgumentOutOfRangeException:参数值超出范围异常,当传入方法的参数超过限定范围,应该抛出该异常
下面是一个简单的示例:
class Client{ public void Connect(string hostname, int port) { if (string.IsNullOrWhiteSpace(hostname)) { throw new ArgumentException("参数无效。", nameof(hostname)); } // 执行其他操作 }}执行错误-程序错误:可以在程序中处理的错误。如 File.Open 未找到相应文件抛出 FileNotFoundException 异常,我们可以创建一个新文件并继续运行。
此类错误对应的异常有很多,最常用的异常是:
- InvalidOperationException:对象处于不正确的状态时,抛出该异常
下面是一个简单的示例:
class Client{ public bool IsConnected { get; private set; } public void Connect() { if (IsConnected) { throw new InvalidOperationException("客户端已连接。"); } // 其他操作 IsConnected = true; }}执行错误-系统失败:无法在程序中进行处理的执行错误。如即时编译器(Just-In-Time compiler)用尽了内存而引发的 OutOfMemoryException。
此类错误对应的异常也有很多,但是这些异常都不应该由开发者抛出,而是由 CLR 负责。例如:
- OutOfMemoryException:内层分配失败时抛出该异常,只有 CLR 才能抛出该异常
Info
关于更多的异常分类,见第7章 异常 - hihaojie - 博客园 7.3 标准异常类型的使用
4. 异常的捕捉
有抛出,自然有捕捉。异常的捕捉并不复杂,不过仍然有诸多细节需要注意。这里我们通过几个问题厘清异常的捕捉。我们假设有如下 Connect() 方法:
void Connect(string hostname){ if (hostname is null) { throw new ArgumentNullException(nameof(hostname)); } // 执行其他操作}这里先提一点:ArgumentException 是 ArgumentNullException 和 ArgumentOutOfRangeException 的父类。
// 代码1try{ Connect(null!);}catch (ArgumentException ex){ Console.WriteLine("捕捉到 ArgumentException 异常");}catch (ArgumentNullException ex){ Console.WriteLine("捕捉到 ArgumentNullException 异常");}// 代码2try{ Connect(null!);}catch (ArgumentNullException ex){ Console.WriteLine("捕捉到 Exception 异常");}catch (ArgumentException ex){ Console.WriteLine("捕捉到 ArgumentException 异常");}// 代码3try{ Connect(null!);}catch (ArgumentNullException ex){ Console.WriteLine("捕捉到 Exception 异常");}catch (ArgumentException ex){ Console.WriteLine("捕捉到 ArgumentException 异常");}catch (InvalidOperationException ex){ Console.WriteLine("捕捉到 InvalidOperationException 异常");}
- 问题二:如下代码会输出“捕捉到 ArgumentException 异常”吗?
try{ Connect(null!);}catch (ArgumentException ex){ Console.WriteLine("捕捉到 ArgumentException 异常");}
try{ SentMessage("异常测试。");}catch (ArgumentException ex){ Console.WriteLine("捕捉到 ArgumentException 异常");}void SentMessage(string message){ try { Connect(null!); } catch (ArgumentOutOfRangeException ex) { Console.WriteLine("捕捉到 ArgumentOutOfRangeException 异常"); }}
通过上述问题我们可以得出如下结论:
- 要捕捉的异常存在父子关系时,需要子类异常在前,父类异常在后,否则无法编译;
- 子类异常可以通过捕捉父类异常完成捕捉;
Exception 作为所有异常的基类,捕捉它可以捕获全部异常。
- 未捕获的异常会进一步向上抛出;
对应的,捕捉异常有这些惯例(准则):
如“使用错误”异常(ArgumentException 三兄弟),它们的处理方式相同(应由调用者修改代码),可以直接捕获 ArgumentException 异常进行处理。
类似的还有 OperationCanceledException 和 TaskCanceledException
对于未知的异常,应该进一步向上抛出,由上一级进行处理。
如下代码演示了处理已知异常 TimeoutException,忽略未知异常 ArgumentNullException
try{ Connect(hostname);}catch (TimeoutException ex){ Console.WriteLine("连接超时,尝试二次连接"); Connect(hostname);}void Connect(string hostname){ if (hostname is null) { throw new ArgumentNullException(nameof(hostname)); } // 执行其他操作 if (usedTime > TimeSpan.FromSeconds(100)) { throw new TimeoutException("连接用时超过 100s。"); }}5. 异常的捕捉、再抛出
有时我们捕捉异常后并不想处理,而是想进行记录并二次抛出,或者将转为其他异常再抛出。我们先比较如下三段代码,看看它们的二次抛出有何不同:
Exception holder = null;try{ try { throw new Exception("原始异常"); } catch (Exception ex) { Console.WriteLine(ex.StackTrace); holder = ex; throw; }}catch (Exception ex){ Console.WriteLine(ex.Message); Console.WriteLine(ex.StackTrace); Console.WriteLine(holder == ex);}Exception holder = null;try{ try { throw new Exception("原始异常"); } catch (Exception ex) { Console.WriteLine(ex.StackTrace); holder = ex; throw ex; }}catch (Exception ex){ Console.WriteLine(ex.Message); Console.WriteLine(ex.StackTrace); Console.WriteLine(ex == holder);}Exception holder = null;try{ try { throw new Exception("原始异常"); } catch (Exception ex) { Console.WriteLine(ex.StackTrace); holder = ex; throw new Exception("二次抛出异常", ex); }}catch (Exception ex){ Console.WriteLine(ex.Message); Console.WriteLine(ex.StackTrace); Console.WriteLine(holder == ex);}执行上述代码我们可以发现:
- 直接使用 throw 二次抛出:Exception.StackTrace 属性会保留原始栈信息;
- 使用 throw ex 二次抛出:二次抛出的异常与原实例相同,但 Exception.StackTrace 存储的栈信息更新为二次抛出的位置;
- 使用 throw new Exception() 二次抛出:将异常进行了二次包装,Exception.Message、Exception.StrackTrace 等诸多成员值都发生了变化。
我们可以根据需要采用相应的二次抛出方式,不过一般会遵循如下准则:
- 若不需要二次包装异常,应直接使用 throw 二次抛出;
- 若需要二次包装异常,应使用 throw new SomeException 二次抛出异常,并将原异常传入新异常。
你可能对“并将原异常传入新异常”这句话感到困惑。这里我们看一下基类异常 Exception 的四个构造函数:
public Exception();public Exception(string message)public Exception(string message, Exception innerException)protected Exception(SerializationInfo info, StreamingContext context)其中第三个构造函数 Exception(string message, Exception innerException) 需要一个 Exception 实例,是的,该参数是专门用于二次包装异常。当我们需要将原异常包装为其他异常时,需要将原异常实例通过该参数传入。二次包装时不传入该参数也是可以的,不过传入是标准做法,有利于开发者追溯原异常。下面是一个简单示例:
try{ throw new TimeoutException("执行超时。");}catch (Exception ex){ throw new InvalidOperationException("执行失败,请检查硬件状态。", ex);}6. 常见异常
下面我们介绍一下常见的异常,以及它们的使用场景。
6.1 基类异常
如下异常因表达的异常分类不明确,开发者不应抛出下列异常:
- Exception:基类异常,它是所有异常的基类。我们自定义异常时需要派生自该类或其子类。
- ApplicationException 和 SystemException:设计之初,SystemException 的派生类用于表示 CLR(或系统)自身抛出的异常,ApplicationException 的派生类用于表示非 CLR 异常(应用程序异常)。但是很多异常类没有遵循这一模式,如 TargetInvocationException 派生自 ApplicationException,却由 CLR 抛出。因此 ApplicationException 已失去原有意义。
我们在自定义异常时,不再推荐以 ApplicationException、SystemException 为基类。
6.2 常用异常
- InvalidOperationException:如果对象处于不正确的状态,抛出该异常。
例如:往只读的 FileStream 写入数据。
- ArgumentException、ArgumentNullException、ArgumentOutOfRangeException:用户传入错误参数时,要抛出 ArgumentException 或其派生类,并设置 ParamName 属性。如果可以,尽量选择位于继承层次末尾的异常类型。
- OperationCanceledException、TaskCanceledException:表示操作被取消。TaskCanceledException 用于异步编程,OperationCanceledException 可用于任意场景。
需要注意的是,在异步方法中手动抛出 TaskCanceledException 时,需要通过其构造函数传入 CancellationToken,否则相应的 Task 的 Status 属性不会标记为 Canceled
- FormatException:表明文本解析方法中的输入字符串不符合要求或指定格式。
- FileNotFoundException:表示文件未找到。
- InvalidCastException:表示无效的类型转换,常见于强制转换、拆箱。
如下代码便会抛出该异常:
object content = string.Empty;int value = (int)content;
- NotSupportedException:表示当前成员功能不支持。
以 ReadOnlyCollection<T> 为例,它不支持 Add() 方法,但又实现了 IList<T> 接口,因此它的 Add() 方法便抛出了该异常。
- NotImplementedException:表示当前成员功能尚未实现。
- TimeoutException:表示执行超时。因历史遗留原因,Web 通信超时并未抛出该异常。以 WebRequest 为例,它通信超时会抛出 WebException 异常,并令异常实例的 Status 属性值为 WebExceptionStatus.Timeout 枚举值。
6.3 CLR 异常
这类异常通常由 CLR 抛出,开发者不应该使用这些异常。
- NullReferenceException、IndexOutOfRangeException、AccessViolationException:表示代码存在缺陷,需要开发者调整代码。
- StackOverflowException:栈溢出时会抛出该异常,常见于无限递归。栈溢出时,几乎不可能让托管代码保持状态一致。发生该异常 CLR2.0 默认会让程序立即终止。开发者也不应该捕获该异常(是的,此时应该纵容程序崩溃)。
- OutOfMemoryException:内存分配失败时会抛出该异常。
7. 如何自定义异常
上一节我们讲了诸多预定义异常,当预定义异常不能满足我们的需要时,就需要自定义异常了。
自定义异常通常遵循如下准则:
- 自定义异常应该派生自 System.Exception 或其他常用的基类异常;
- 继承层次不应该过深;
- 命名使用“Exception”后缀;
- 如果多种错误可以通过一种方式来处理,则它们应该属于同一类型的异常;
- 自定义异常应该至少有如下 4 个构造函数:
public class SomeException : Exception, ISerializable{ public SomeException(); public SomeExcepiton(string message); public SomeExcepiton(string message, Exception inner); // 序列化所需构造函数 protected SomeException(SerializationInfo info, StreamingContext context);}
Tips
关于自定义异常必须实现二进制序列化(即实现 SomeException(SerializationInfo info, StreamingContext context) 构造函数)在新版 .NET 中已不再要求,且 Exception(SerializationInfo info, StreamingContext context) 也被标记为了弃用([Obsolete])。因此下面的例子忽略了二进制序列化的实现。
现在,我们假设有这么一个硬件设备:
- 它是一个测距仪,计算机通过 TCP/IP 的方式与它通信(它是服务端);
- 当它正在测量中,重复发送测量指令它不会进行响应(即会发生通信超时);
- 如果测量的距离超出返回,它会返回“OutOfRange”字符串;
- 测量成功,则会返回数值,表示测得的距离。
这里我们要自定义一个 DeviceErrorException 表示操作该硬件时的一切异常。请思考,该测距仪的类应该怎样定义?该异常又怎样定义?
下面是我编写的一个测距仪类和对应的 DeviceErrorException 异常,大家可以作为参考:
class Measurer{ private TcpClient _client; public bool IsConnected => _client != null && _client.Connected; private const string Command = "Measure"; private const string OutOfRangeResponse = "OutOfRange"; public void Connect(string hostname, int port) { try { _client = new TcpClient(hostname, port); } catch (TimeoutException ex) { throw new DeviceErrorException("连接超时。", ex, DeviceState.Timeout); } } public double Measure() { try { if (!IsConnected) { throw new DeviceErrorException("测距仪尚未连接。请连接后再进行操作。", DeviceState.NotConnected); } byte[] writeBuffer = Encoding.ASCII.GetBytes(Command); Stream stream = _client.GetStream(); _client.GetStream().Write(writeBuffer, 0, writeBuffer.Length); byte[] readBuffer = new byte[100]; int count = stream.Read(readBuffer, 0, readBuffer.Length); string content = Encoding.ASCII.GetString(readBuffer, 0, count); if (content == OutOfRangeResponse) { throw new DeviceErrorException("数据读取失败,超出测量范围。", DeviceState.OutOfRange); } if (double.TryParse(content, out double result)) { return result; } throw new DeviceErrorException($"转换数据失败。获取的内容为:[{content}]", DeviceState.DataParseError); } catch (TimeoutException ex) { throw new DeviceErrorException("通信超时。", ex, DeviceState.Timeout); } catch (DeviceErrorException) { throw; } catch (Exception ex) { throw new DeviceErrorException("未知异常。", ex); } }}class DeviceErrorException : InvalidOperationException{ public DeviceState State { get; } public DeviceErrorException() : this(DeviceState.Unknown) { } public DeviceErrorException(DeviceState state) { State = state; } public DeviceErrorException(string message) : this(message, DeviceState.Unknown) { } public DeviceErrorException(string message, DeviceState state) : base(message) { State = state; } public DeviceErrorException(string message, Exception innerException) : this(message, innerException, DeviceState.Unknown) { } public DeviceErrorException(string message, Exception innerException, DeviceState state) : base(message, innerException) { State = state; }}enum DeviceState{ Unknown, DataParseError, NotConnected, Timeout, OutOfRange,}8. 异常和性能
一些开发者不愿意使用异常的一个重要原因便是:影响性能(个人决定有点无稽之谈,至少在工控领域我觉得这点性能损失完全不必要担心)。
《框架设计指南》中提到:当抛出异常的频率高于每秒 100 个时,极有可能会带来显著的性能影响。此时我们可以使用“测试者-执行者模式”,或“Try 模式”,这两种模式在 .NET 中十分常见。
8.1 测试者-执行者模式:
我们以集合为例。以下代码我们并不知道 numbers 是否是只读集合,因此它有可能抛出 NotSupportException:
ICollection<int> numbers = ...numbers.Add(1);ICollection<T> 刚好定义了 IsReadOnly 属性,我们可以先通过它判断集合是否是只读的,再进行 Add 操作:
ICollection<int> numbers = ......if (!numbers.IsReadOnly){ numbers.Add(1);}其中,IsReadOnly 是“测试者”,Add() 方法是“执行者”。
在“7. 如何自定义异常”中,我定义的 Measurer 类也添加了 IsConnected 属性,用于告知硬件是否已连接,这也是一种“测试者-执行者模式”。
Notice
在多线程中使用该模式可能出现“竞态条件”,使用时要多加注意。
8.2 Try 模式
Try 模式的使用更加常见,如 int.TryParse()、Dictionary<TKey, TValue>.TryGetValu() 等方法。
以 DateTime 的 Try 模式为例,大致形式如下:
public struct DateTime{ public static DateTime Parse(string dateTime) { ... } public static DateTime TryParse(string dateTime, out DateTime Result) { ... }}Try 模式有诸多细节需要注意:
- 如果成员在常用代码中都可能抛出异常,应使用 Try-Parse 模式避免因异常引起的性能问题。
- Try-Parse 模式要使用“Try”前缀,用 bool 作为返回类型。
- Try 方法返回 false 的原因只有一种,其余类的失败则要抛出异常。
- 要为 Try 方法提供等价抛出异常的方法。
如 DateTime.TryParse() 的等价方法 DateTime.Parse()
- 要通过 out 参数,返回 Try 方法的值。
- 要在 Try 方法返回 false 时,将 default(T) 赋值给 out 参数。
- 避免在抛出异常时向 Try 方法的 out 参数写入数据。
9. 怎么处理“不知道如何处理”的异常
当你调用的接口抛出了未知异常,应该怎么处理?答案可能出乎大多数人的意料:不要捕捉它,让程序崩溃是最好的解决方案。《框架设计指南》中有这么一段话:
你的应用程序应该只处理它理解的那些异常。一般来说,在“某物”出问题之后,几乎不可能把应用程序从可能已被破坏的状态恢复到正常状态,此时只需处理那些你的应用程序可以合理响应的异常。对于其他所有的异常,无需处理,操作系统可中止你的应用程序。
如果你认为当前应用程序已处于带病状态、它不应该继续运行,此时建议通过调用 System.Enviroment.FailFast() 方法来中止进程,而不是抛出异常。该方法接受一个 string 参数,我们可以通过该参数传递错误信息。这个错误信息最终会被操作系统记录在“计算机管理→系统工具→事件查看器→Windows 日志→应用程序”中。因未捕获异常导致的程序崩溃,其崩溃信息也会记录在此处。
最后,根据 Windows 日志信息,进一步排查程序崩溃原因,进行针对性修复。
Enviroment.FailFast() 的用法如下:
Enviroment.FailFast("发生了一个无法挽回的异常", ex)10. 转移异常
有时我们捕捉到异常并不想直接处理,又不想抛出,而是想转移至其他线程。使用一个字段/属性记录该异常,再在主线程抛出?不是很合适,这会破坏原有的调用栈信息。
.NET 早有预备,它提供了 ExceptionDispatchInfo 类,专门用于转移异常:
当从其他线程转移异常,或者 catch 后未使用空的 throw 语句再次抛出异常,要使用 ExceptionDispatchInfo 类,它会在重新抛出的过程中持续保存调用栈。下面是一个简单的用例:
private ExceptionDispatchInfo _savedExceptionInfo;private void BackgroundWorker() { try{ ... } catch (Exception e){ _savedExceptionInfo = ExceptionDispatchInfo.Capture(e); }}public object GetResult() { if(_done) { if(_savedExceptionInfo != null){ _savedExceptionInfo.Throw(); // 编译器无法理解该方法是抛出了一个异常,因此需要额外的return语句。 return null; } }}11. 过滤异常
异常过滤器(exception filter)于 C#6 引入。通过异常过滤器我们可以重复捕获同类型异常:
catch (WebException ex) when (ex.Status == WebExceptionStatus.Timeout){ ... }catch (WebException ex) when (ex.Status == WebExceptionStatus.NameResolutionFailure){ ... }when 子句中的布尔表达式可以包含副作用,例如调用一个方法记录诊断所需的异常的日志。
它的使用也有陷阱。试分析如下代码会发生什么:
try{ throw new ArgumentException();}catch (ArgumentException ex) when (ex.ParamName == string.Empty){ Console.WriteLine("命中第一个 when 子句");}catch (ArgumentException ex) when (ex.ParamName is null){ Console.WriteLine("命中第二个 when 子句");}答案是:会输出“命中第二个 when 子句”。
你可能会疑惑:它不应该在第一个 when 字句那里抛出异常吗?毕竟 ParamName 属性未赋值,进行相等判断时应该抛出 NullReferenceException 才对。这是因为在过滤器中引发异常时,该异常由 CLR 捕获,并且该过滤器返回 false。该行为无法和过滤器执行并返回 false 区分开,因此很难进行调试。使用时要多加注意
12. 其他事项
12.1 切勿随意修改异常名称及其父类
我们前面提到:
处理方式相同的异常,可以捕获它们共同的父类
这意味着我们不应该随意修改自定义异常的基类,否则原有的异常处理代码无法正常工作。以如下代码为例,因基类异常由 InvalidOperationException 改为 Exception,原有的异常处理代码不再起作用:
// 原异常// class MyException : InvalidOperationException { }// 修改基类后的异常class MyException : Exception { }// 原本可以正常运作的异常处理代码失效try{ throw new MyException();}catch (InvalidOperationException ex){ Console.WriteLine("捕捉到 InvalidOperationException 或其子类异常");}12.2 捕捉后仍会向上抛出的异常:ThreadAbortException
提到多线程显然离不开 Thread。Thread 的 Abort() 方法用于终止相应线程,被终止的线程会抛出 ThreadAbortException 异常。不过该异常较为特别,捕捉后会继续向上抛出。试分析如下代码,会发生什么:
Thread thread = new Thread(DoSomething);thread.Start();Thread.Sleep(100);thread.Abort();void DoSomething(){ try { try { while (true) { Thread.Sleep(20); } } catch (ThreadAbortException ex) { Console.WriteLine("第一次捕获到 ThreadAbortException 异常"); } } catch (ThreadAbortException ex) { Console.WriteLine("第二次捕获到 ThreadAbortException 异常"); }}答案是:它会依次输出“第一次捕获到 ThreadAbortException 异常”、“第二次捕获到 ThreadAbortException 异常”。若想终止该异常进一步向外抛出,需调用 Thread.ResetAbort() 方法,它会将线程状态(ThreadState)从 AbortRequested 恢复至 Running。
<hr>参考文献:
- 《框架设计指南:构建可复用.NET库的约定、惯例与模式》第三版
- 《C#7.0 核心技术指南》
Info
上述两本书的部分内容,可参阅我的阅读笔记阅读笔记目录汇总 - hihaojie - 博客园
|
|