当前位置: 首页 > news >正文

C#/.NET 如何获取一个异常(Exception)的关键特征,用来判断两个异常是否表示同一个异常

在 .NET / C# 程序中出现异常是很常见的事情,程序出现异常后记录日志或者收集到统一的地方可以便于分析程序中各种各样此前未知的问题。但是,有些异常表示的是同一个异常,只是因为参数不同、状态不同、用户的语言环境不同就分开成多个异常的话,分析起来会有些麻烦。

本文将提供一个方法,将异常的关键信息提取出来,这样可以比较多次抛出的不同的异常实例是否表示的是同一个异常。


本文内容

    • `Exception.ToString()`
    • 哪些信息是异常的关键信息
    • 提取特征的 C# 代码
    • 一个完整的 ExceptionDescriptor

Exception.ToString()

以下是捕获到的一个异常实例,调用 ToString() 方法后拿到的结果:

System.NotSupportedException: BitmapMetadata 在 BitmapImage 上可用。
   在 System.Windows.Media.Imaging.BitmapImage.get_Metadata()
   在 System.Windows.Media.Imaging.BitmapFrame.Create(BitmapSource source)
   在 Walterlv.Demo.Exceptions.Foo.Take(string fileName)

在英文的系统上,拿到的结果可能是这样的:

System.NotSupportedException: BitmapMetadata is not available on BitmapImage.
   at System.Windows.Media.Imaging.BitmapImage.get_Metadata()
   at System.Windows.Media.Imaging.BitmapFrame.Create(BitmapSource source)
   at Walterlv.Demo.Exceptions.Foo.Take(string fileName)

这样,我们就不能使用 ToString() 来判断两个异常是否表示同一个异常了。

另外,在 ToString() 方法中,如果包含 PDB,那么异常堆栈中还会包含源代码文件的路径以及行号信息。

关于 ToString() 中输出的信息,可以阅读 StackTrace.ToString() 方法的源码来了解:

  • StackTrace.cs

哪些信息是异常的关键信息

从默认的 ToString() 中我们可以得知,它包含三个部分:

  1. 异常类型的全名 Type.FullName
  2. 异常信息 Exception.Message
  3. 异常堆栈 Exception.StackTrace

考虑到 Message 部分受多语言影响非常严重,很难作为关键异常特征,所以我们在提取关键异常特征的时候,需要将这一部分去掉,只能作为此次异常的附加信息,而不能作为关键特征。

所以我们的关键特征就是:

  1. 异常类型的全名 Type.FullName
  2. 异常堆栈中所有帧的方法签名(这能保证语言无关)

比如本文一开始列举出来的异常堆栈,我们应该提取成:

System.NotSupportedException
  System.Windows.Media.Imaging.BitmapImage.get_Metadata()
  System.Windows.Media.Imaging.BitmapFrame.Create(BitmapSource source)
  Walterlv.Demo.Exceptions.Foo.Take(string fileName)

提取特征的 C# 代码

为了提取出以上的关键特征,我需要写一段 C# 代码来做这样的事情:

public (string typeName, IReadonlyList<string> frameSignature) GetDescriptor(Exception exception)
{
    var type = exception.GetType().FullName;
    var stackFrames = new StackTrace(exception).GetFrames() ?? new StackFrame[0];
    var frames = stackFrames.Select(x => x.GetMethod()).Select(m =>
        $"{m.DeclaringType?.FullName ?? "null"}.{m.Name}({string.Join(", ", m.GetParameters().Select(p => $"{p.ParameterType.Name} {p.Name}"))})");
    return (type, frames.ToList());
}

一个是拿到 Exception 实例的类型名称,通过 exception.GetType().FullName

另一个拿到方法签名。

由于 Exception.StackTrace 属性得到的是一个字符串,而且此字符串还真的有可能根本不是异常信息呢,所以我们这里通过创建一个 StackTrace 的实例来从异常中获取真实的堆栈,当然如果拿不到我们这里使用空数组来表示。

随后,遍历异常堆栈中的所有帧,将方法名和方法的所有参数进行拼接,形成 ClassFullName.MethodName(ParameterType parameterName) 这样的形式,于是就拼接成类似 Exception.ToString() 中的格式了。

由于确定一个类型中是否是同一个方法时与返回值无关,所以我们甚至不需要将返回值加上就能唯一确定一个方法了。

一个完整的 ExceptionDescriptor

为了方便,我写了一个完整的 ExceptionDescriptor 类型来完成异常特征提取的事情。这个类同时重写了相等方法,这样可以直接使用相等方法来判断两个异常的关键信息是否表示的是同一个异常。

源码可以在这里找到:https://gist.github.com/walterlv/0ce95369aa78c5f0f38a527bef5779c2

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;

namespace Walterlv
{
    /// <summary>
    /// 包含一个 <see cref="Exception"/> 对象的关键特征,可使用此对象的实例判断两个不同的异常实例是否极有可能表示同一个异常。
    /// </summary>
    [DebuggerDisplay("{TypeName,nq}: {FrameSignature[0],nq}")]
    public class ExceptionDescriptor : IEquatable<ExceptionDescriptor>
    {
        /// <summary>
        /// 获取此异常的类型名称。
        /// </summary>
        public string TypeName { get; }

        /// <summary>
        /// 获取此异常堆栈中的所有帧的方法签名,指的是在一个类型中不会冲突的最小部分,所以不含返回值和可访问性。
        /// 比如 private void Foo(Bar b); 方法,在这里会写成 Foo(Bar b)。
        /// </summary>
        public IReadOnlyList<string> FrameSignature { get; }

        /// <summary>
        /// 从一个异常中提取出关键的异常特征,并创建 <see cref="ExceptionDescriptor"/> 的新实例。
        /// </summary>
        /// <param name="exception">要提取特征的异常。</param>
        public ExceptionDescriptor(Exception exception)
        {
            var type = exception.GetType().FullName;
            var stackFrames = new StackTrace(exception).GetFrames() ?? new StackFrame[0];
            var frames = stackFrames.Select(x => x.GetMethod()).Select(m =>
                $"{m.DeclaringType?.FullName ?? "null"}.{m.Name}({string.Join(", ", m.GetParameters().Select(p => $"{p.ParameterType.Name} {p.Name}"))})");
            TypeName = type;
            FrameSignature = frames.ToList();
        }

        /// <summary>
        /// 根据异常的信息本身创建异常的关键特征。
        /// </summary>
        /// <param name="typeName">异常类型的完整名称。</param>
        /// <param name="frameSignature">
        /// 异常堆栈中的所有帧的方法签名,指的是在一个类型中不会冲突的最小部分,所以不含返回值和可访问性。
        /// 比如 private void Foo(Bar b); 方法,在这里会写成 Foo(Bar b)。
        /// </param>
        public ExceptionDescriptor(string typeName, IReadOnlyList<string> frameSignature)
        {
            TypeName = typeName;
            FrameSignature = frameSignature;
        }

        /// <summary>
        /// 判断此异常特征对象是否与另一个对象实例相等。
        /// 如果参数指定的对象是 <see cref="ExceptionDescriptor"/>,则判断特征是否相等。
        /// </summary>
        public override bool Equals(object obj)
        {
            if (ReferenceEquals(null, obj))
            {
                return false;
            }

            if (ReferenceEquals(this, obj))
            {
                return true;
            }

            if (obj.GetType() != this.GetType())
            {
                return false;
            }

            return Equals((ExceptionDescriptor) obj);
        }

        /// <summary>
        /// 判断此异常特征与另一个异常特征是否是表示同一个异常。
        /// </summary>
        public bool Equals(ExceptionDescriptor other)
        {
            if (ReferenceEquals(null, other))
            {
                return false;
            }

            if (ReferenceEquals(this, other))
            {
                return true;
            }

            return string.Equals(TypeName, other.TypeName) && FrameSignature.SequenceEqual(other.FrameSignature);
        }

        /// <inheritdoc />
        public override int GetHashCode()
        {
            unchecked
            {
                return ((TypeName != null ? StringComparer.InvariantCulture.GetHashCode(TypeName) : 0) * 397) ^
                       (FrameSignature != null ? FrameSignature.GetHashCode() : 0);
            }
        }

        /// <summary>
        /// 判断两个异常特征是否是表示同一个异常。
        /// </summary>
        public static bool operator ==(ExceptionDescriptor left, ExceptionDescriptor right)
        {
            return Equals(left, right);
        }

        /// <summary>
        /// 判断两个异常特征是否表示的不是同一个异常。
        /// </summary>
        public static bool operator !=(ExceptionDescriptor left, ExceptionDescriptor right)
        {
            return !Equals(left, right);
        }
    }
}

参考资料

  • StackTrace.cs

我的博客会首发于 https://blog.walterlv.com/,而 CSDN 会从其中精选发布,但是一旦发布了就很少更新。

如果在博客看到有任何不懂的内容,欢迎交流。我搭建了 dotnet 职业技术学院 欢迎大家加入。

知识共享许可协议

本作品采用知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可。欢迎转载、使用、重新发布,但务必保留文章署名吕毅(包含链接:https://walterlv.blog.csdn.net/),不得用于商业目的,基于本文修改后的作品务必以相同的许可发布。如有任何疑问,请与我联系。

相关文章:

  • C#/.NET 如何在第一次机会异常 FirstChanceException 中获取比较完整的异常堆栈
  • C#/.NET 使用 git 命令行来操作 git 仓库
  • C#/.NET 中启动进程时所使用的 UseShellExecute 设置为 true 和 false 分别代表什么意思?
  • WPF 的命令的自动刷新时机——当你 CanExecute 会返回 true 但命令依旧不可用时可能是这些原因
  • 将 C++/WinRT 中的线程切换体验带到 C# 中来(WPF 版本)
  • 如何在 MSBuild 的项目文件 csproj 中获取绝对路径
  • C# 跨设备前后端开发探索
  • MSBuild 如何编写带条件的属性、集合和任务 Condition?
  • WPF 像素着色器入门:使用 Shazzam Shader Editor 编写 HLSL 像素着色器代码
  • 如何快速自定义 Visual Studio 中部分功能的快捷键
  • C# 8.0 如何在项目中开启可空引用类型的支持
  • C# 8.0 可空引用类型中的各项警告/错误的含义和示例代码
  • C# 可空引用类型 NullableReferenceTypes 更强制的约束:将警告改为错误 WarningsAsErrors
  • ClearType 的原理:Windows 上文本的亚像素控制
  • 使用 7-Zip 的命令行版本来压缩和解压文件
  • 【140天】尚学堂高淇Java300集视频精华笔记(86-87)
  • 2019.2.20 c++ 知识梳理
  • Android框架之Volley
  • IIS 10 PHP CGI 设置 PHP_INI_SCAN_DIR
  • javascript数组去重/查找/插入/删除
  • Linux学习笔记6-使用fdisk进行磁盘管理
  • Python进阶细节
  • Python爬虫--- 1.3 BS4库的解析器
  • Python实现BT种子转化为磁力链接【实战】
  • Synchronized 关键字使用、底层原理、JDK1.6 之后的底层优化以及 和ReenTrantLock 的对比...
  • vue从入门到进阶:计算属性computed与侦听器watch(三)
  • yii2中session跨域名的问题
  • 阿里研究院入选中国企业智库系统影响力榜
  • 阿里云应用高可用服务公测发布
  • 成为一名优秀的Developer的书单
  • 读懂package.json -- 依赖管理
  • 机器人定位导航技术 激光SLAM与视觉SLAM谁更胜一筹?
  • 机器学习 vs. 深度学习
  • 马上搞懂 GeoJSON
  • 如何解决微信端直接跳WAP端
  • 如何正确配置 Ubuntu 14.04 服务器?
  • 使用iElevator.js模拟segmentfault的文章标题导航
  • 体验javascript之美-第五课 匿名函数自执行和闭包是一回事儿吗?
  • 小而合理的前端理论:rscss和rsjs
  • 一份游戏开发学习路线
  • ​MPV,汽车产品里一个特殊品类的进化过程
  • ​ssh-keyscan命令--Linux命令应用大词典729个命令解读
  • # 日期待t_最值得等的SUV奥迪Q9:空间比MPV还大,或搭4.0T,香
  • (C++)栈的链式存储结构(出栈、入栈、判空、遍历、销毁)(数据结构与算法)
  • (附源码)springboot高校宿舍交电费系统 毕业设计031552
  • (附源码)基于SSM多源异构数据关联技术构建智能校园-计算机毕设 64366
  • (免费领源码)Java#Springboot#mysql农产品销售管理系统47627-计算机毕业设计项目选题推荐
  • (三)终结任务
  • (五)c52学习之旅-静态数码管
  • (译)2019年前端性能优化清单 — 下篇
  • (转) Android中ViewStub组件使用
  • (转)3D模板阴影原理
  • (转)一些感悟
  • *++p:p先自+,然后*p,最终为3 ++*p:先*p,即arr[0]=1,然后再++,最终为2 *p++:值为arr[0],即1,该语句执行完毕后,p指向arr[1]
  • .net FrameWork简介,数组,枚举