English 简体中文 繁體中文 한국 사람 日本語 Deutsch русский بالعربية TÜRKÇE português คนไทย french
查看: 9|回复: 0

异常的使用

[复制链接]
查看: 9|回复: 0

异常的使用

[复制链接]
查看: 9|回复: 0

355

主题

0

回帖

1067

积分

金牌会员

积分
1067
pda设备

355

主题

0

回帖

1067

积分

金牌会员

积分
1067
2025-2-5 10:43:16 | 显示全部楼层 |阅读模式
异常的使用

不管是网络还是出版读物,关于 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 - 博客园
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

355

主题

0

回帖

1067

积分

金牌会员

积分
1067

QQ|智能设备 | 粤ICP备2024353841号-1

GMT+8, 2025-3-10 15:37 , Processed in 2.745606 second(s), 27 queries .

Powered by 智能设备

©2025

|网站地图