C#自定义异常

1.概要

System.Exception这个类在刚刚接触代码的时候,哪怕一点代码不会写也会认识它。稍不注意就会见到,我当时对Exception理解是两点:

  • Exception这个东西非常讨厌。

  • try catch语句捕捉一下解决报错问题。

try
{
  //Error..
}
catch (Exception ex)
{
  Logger.Info(ex.Message);
}

然后就写出了以上的代码,不知道有没有一样感受的小伙伴。以上代码非常比较入门,那这篇文章就来分享其他角度看待Exception:.

  • 设计

设计一个良好的Exception类,可以帮助开发者更容易获得友好、语义清晰、意图明确的异常提示。尤其是在设计框架、组件、构建产品项目时会用到。一个糟糕的异常会让使用的开发者对其印象大打折扣。因为抛出来的问题,摸棱两可,似是而非或者干脆根本看不懂都是非常浪费生命的事情,连搜索引擎里都不知道填什么。

  • 使用

当一个异常设计好了之后其实事情只做了一半,在使用时同样需要注意在声明的时候尽可能的让异常内容更贴切让开发者能快速的定位到异常的“第一案发现场”。当然这个想法是比较理想的状况。

  • 处理

在遇到异常之后,如果不能妥善的处理也将会是一种灾难。例如下面代码:

private void ReadData(string path)
{
   FileStream fs = null;
   try
  {
       fs = new FileStream(path,FileAccess.Read);
  }
   catch (Exception ex)
  {
       //code..
  }
}

在读取文件的时候发生了异常,在catch块处理的时候如果因为业务逻辑不得不写一些逻辑时又导致异常会出现文件对象不释放导致文件占用。

第二个例子,这段代码主要是描述了Socket断线重连,这样会导致Socket对象重复被创建引起的一系列问题。

try
{
  Socket socket = new Socket();
  socket.Connect("127.0.0.1",5001);
  socket.Send(array);
}
catch (Exception ex)
{
  Socket socket = new Socket();
  socket.Connect("127.0.0.1", 5001);
}

2.详细内容

任何自定义的异常实现,都是需要继承System.Exception的。在看详细内容之前,我们先了解一下System.Exception属性里包含的哪些内容。

https://learn.microsoft.com/zh-cn/dotnet/api/system.exception?f1url=%3FappId%3DDev16IDEF1%26l%3DZH-CN%26k%3Dk(System.Exception)%3Bk(DevLang-csharp)%26rd%3Dtrue&view=net-7.0

C#自定义异常

接下来分享一下简单的相关示例。

  • 异常设计

自定义异常类:

  [Serializable]
   public sealed class GeneralUpdateException<TExceptionArgs> : Exception, ISerializable
       where TExceptionArgs : ExceptionArgs
  {
       private const String c_args = "Args";
       private readonly TExceptionArgs m_args;

       public TExceptionArgs Args { get { return m_args; } }

       public GeneralUpdateException(String message = null, Exception innerException = null) : this(null, message, innerException) { }

       public GeneralUpdateException(TExceptionArgs args, String message = null, Exception innerException = null) : base(message, innerException) => m_args = args;

      [SecurityPermission(SecurityAction.LinkDemand, Flags = SecurityPermissionFlag.SerializationFormatter)]
       private GeneralUpdateException(SerializationInfo info, StreamingContext context) : base(info, context) => m_args = (TExceptionArgs)info.GetValue(c_args, typeof(TExceptionArgs));

      [SecurityPermission(SecurityAction.LinkDemand, Flags = SecurityPermissionFlag.SerializationFormatter)]
       public override void GetObjectData(SerializationInfo info, StreamingContext context)
      {
           info.AddValue(c_args, typeof(TExceptionArgs));
           base.GetObjectData(info, context);
      }

       public override string Message
      {
           get
          {
               String baseMsg = base.Message;
               return (m_args == null) ? baseMsg : $"{baseMsg}({m_args.Message})";
          }
      }

       public override bool Equals(object obj)
      {
           GeneralUpdateException<TExceptionArgs> other = obj as GeneralUpdateException<TExceptionArgs>;
           if (other == null) return false;
           return Object.Equals(m_args, other.m_args) && base.Equals(obj);
      }

       public override int GetHashCode() => base.GetHashCode();
  }

自定义异常参数:

  [Serializable]
   public abstract class ExceptionArgs
  {
       public virtual string Message { get { return String.Empty; } }
  }
  [Serializable]
   public sealed class PatchDirtyExceptionArgs : ExceptionArgs
  {
       private readonly String _patchPath;

       public PatchDirtyExceptionArgs(String patchPath) { _patchPath = patchPath; }

       public String PatchPath { get { return _patchPath; } }

       public override string Message
      {
           get
          {
               return (_patchPath == null) ? base.Message : $"Patch file path {_patchPath}";
          }
      }
  }
  • 使用(声明异常)

       static void Main(string[] args)
      {
           throw new GeneralUpdateException<PatchDirtyExceptionArgs>(new PatchDirtyExceptionArgs("C:\\1.pacth"), "This file is probably an encrypted file .");
      }

C#自定义异常

  • 处理异常

下面代码为什么会写两个catch块是因为,在try块的代码中抛出异常,CLR将搜索捕捉类型与抛出的异常相同的catch块。如果没有任何捕捉类型与抛出的异常匹配,CLR会去调用栈更高的一层搜索与异常匹配的捕捉类型。如果都到了调用栈的顶部,还是没有找到匹配的catch块,就会发生未处理的异常。所以在catch块中填写try块里有可能抛出的异常让CLR“快速”的匹配减少匹配带来的损耗。如何证明刚刚的理论呢?下面的代码简单演示了一下,如果try块声明的异常不是GeneralUpdateException而是Exception则不会触发GeneralUpdateException而是Exception。

       static void Main(string[] args)
      {
           try
          {
               throw new GeneralUpdateException<PatchDirtyExceptionArgs>(new PatchDirtyExceptionArgs("C:\\1.pacth"), "This file is probably an encrypted file .");
          }
           catch (GeneralUpdateException<PatchDirtyExceptionArgs> ex)
          {
               //Code...
               Console.WriteLine(ex.Message);
               Logger.Error(ex.Message);
          }
           catch (Exception ex)
          {
               //Code...
               Console.WriteLine(ex.Message);
               Logger.Error(ex.Message);
          }
           Console.Read();
      }

C#自定义异常

在企业开发当中还会使用到以下两个组件帮助辅助处理异常:

  • Dump文件,C#中代码可以生成Dump文件通过windbug工具进行分析

https://learn.microsoft.com/zh-cn/dotnet/core/diagnostics/dotnet-dump

  • Logger组件,在企业开发中非常常见的组件有Nlog、log4net等等。

异常帮助类

在设计好自定义异常类之后,发现会有很多地方会引用到且部分内容比较相似或者有更高阶的用法。可以专门去封装一个ThrowHelper,也可以使用现成的工具类。在Windows Community Toolkit中有提供。

https://learn.microsoft.com/en-us/windows/communitytoolkit/developer-tools/throwhelper

我们简单的来看看源码提供了哪些内容,算是提供了一个基本的封装思路。那么在有了基本思路时候封装一个自己的ThrowHelper那么就容易一些,但不是那么容易。

// ==++==
//
//   Copyright (c) Microsoft Corporation. All rights reserved.
//
// ==--==

namespace System {
   // This file defines an internal class used to throw exceptions in BCL code.
   // The main purpose is to reduce code size.
   //
   // The old way to throw an exception generates quite a lot IL code and assembly code.
   // Following is an example:
   //     C# source
   //         throw new ArgumentNullException("key", Environment.GetResourceString("ArgumentNull_Key"));
   //     IL code:
   //         IL_0003: ldstr     "key"
   //         IL_0008: ldstr     "ArgumentNull_Key"
   //         IL_000d: call       string System.Environment::GetResourceString(string)
   //         IL_0012: newobj     instance void System.ArgumentNullException::.ctor(string,string)
   //         IL_0017: throw
   //   which is 21bytes in IL.
   //
   // So we want to get rid of the ldstr and call to Environment.GetResource in IL.
   // In order to do that, I created two enums: ExceptionResource, ExceptionArgument to represent the
   // argument name and resource name in a small integer. The source code will be changed to
   //   ThrowHelper.ThrowArgumentNullException(ExceptionArgument.key, ExceptionResource.ArgumentNull_Key);
   //
   // The IL code will be 7 bytes.
   //   IL_0008: ldc.i4.4
   //   IL_0009: ldc.i4.4
   //   IL_000a: call       void System.ThrowHelper::ThrowArgumentNullException(valuetype System.ExceptionArgument)
   //   IL_000f: ldarg.0
   //
   // This will also reduce the Jitted code size a lot.
   //
   // It is very important we do this for generic classes because we can easily generate the same code
   // multiple times for different instantiation.
   //
   // <


   using System.Runtime.CompilerServices;        
   using System.Runtime.Serialization;
   using System.Diagnostics.Contracts;

  [Pure]
   internal static class ThrowHelper {    
       internal static void ThrowArgumentOutOfRangeException() {        
           ThrowArgumentOutOfRangeException(ExceptionArgument.index, ExceptionResource.ArgumentOutOfRange_Index);            
      }

       internal static void ThrowWrongKeyTypeArgumentException(object key, Type targetType) {
           throw new ArgumentException(Environment.GetResourceString("Arg_WrongType", key, targetType), "key");
      }

       internal static void ThrowWrongValueTypeArgumentException(object value, Type targetType) {
           throw new ArgumentException(Environment.GetResourceString("Arg_WrongType", value, targetType), "value");
      }

       internal static void ThrowKeyNotFoundException() {
           throw new System.Collections.Generic.KeyNotFoundException();
      }
       
       internal static void ThrowArgumentException(ExceptionResource resource) {
           throw new ArgumentException(Environment.GetResourceString(GetResourceName(resource)));
      }

       internal static void ThrowArgumentException(ExceptionResource resource, ExceptionArgument argument) {
           throw new ArgumentException(Environment.GetResourceString(GetResourceName(resource)), GetArgumentName(argument));
      }

       internal static void ThrowArgumentNullException(ExceptionArgument argument) {
           throw new ArgumentNullException(GetArgumentName(argument));
      }

       internal static void ThrowArgumentOutOfRangeException(ExceptionArgument argument) {
           throw new ArgumentOutOfRangeException(GetArgumentName(argument));
      }

       internal static void ThrowArgumentOutOfRangeException(ExceptionArgument argument, ExceptionResource resource) {
               
           if (CompatibilitySwitches.IsAppEarlierThanWindowsPhone8) {
               // Dev11 474369 quirk: Mango had an empty message string:
               throw new ArgumentOutOfRangeException(GetArgumentName(argument), String.Empty);                                                  
          } else {
               throw new ArgumentOutOfRangeException(GetArgumentName(argument),
                                                     Environment.GetResourceString(GetResourceName(resource)));
          }            
      }

       internal static void ThrowInvalidOperationException(ExceptionResource resource) {
           throw new InvalidOperationException(Environment.GetResourceString(GetResourceName(resource)));
      }

       internal static void ThrowSerializationException(ExceptionResource resource) {
           throw new SerializationException(Environment.GetResourceString(GetResourceName(resource)));
      }

       internal static void  ThrowSecurityException(ExceptionResource resource) {
           throw new System.Security.SecurityException(Environment.GetResourceString(GetResourceName(resource)));            
      }

       internal static void ThrowNotSupportedException(ExceptionResource resource) {
           throw new NotSupportedException(Environment.GetResourceString(GetResourceName(resource)));
      }

       internal static void ThrowUnauthorizedAccessException(ExceptionResource resource) {
           throw new UnauthorizedAccessException(Environment.GetResourceString(GetResourceName(resource)));
      }

       internal static void ThrowObjectDisposedException(string objectName, ExceptionResource resource) {
           throw new ObjectDisposedException(objectName, Environment.GetResourceString(GetResourceName(resource)));
      }

       // Allow nulls for reference types and Nullable<U>, but not for value types.
       internal static void IfNullAndNullsAreIllegalThenThrow<T>(object value, ExceptionArgument argName) {
           // Note that default(T) is not equal to null for value types except when T is Nullable<U>.
           if (value == null && !(default(T) == null))
               ThrowHelper.ThrowArgumentNullException(argName);
      }

       //
       // This function will convert an ExceptionArgument enum value to the argument name string.
       //
       internal static string GetArgumentName(ExceptionArgument argument) {
           string argumentName = null;

           switch (argument) {
               case ExceptionArgument.array:
                   argumentName = "array";
                   break;

               case ExceptionArgument.arrayIndex:
                   argumentName = "arrayIndex";
                   break;

               case ExceptionArgument.capacity:
                   argumentName = "capacity";
                   break;

               case ExceptionArgument.collection:
                   argumentName = "collection";
                   break;

               case ExceptionArgument.list:
                   argumentName = "list";
                   break;

               case ExceptionArgument.converter:
                   argumentName = "converter";
                   break;

               case ExceptionArgument.count:
                   argumentName = "count";
                   break;

               case ExceptionArgument.dictionary:
                   argumentName = "dictionary";
                   break;

               case ExceptionArgument.dictionaryCreationThreshold:
                   argumentName = "dictionaryCreationThreshold";
                   break;

               case ExceptionArgument.index:
                   argumentName = "index";
                   break;

               case ExceptionArgument.info:
                   argumentName = "info";
                   break;

               case ExceptionArgument.key:
                   argumentName = "key";
                   break;

               case ExceptionArgument.match:
                   argumentName = "match";
                   break;

               case ExceptionArgument.obj:
                   argumentName = "obj";
                   break;

               case ExceptionArgument.queue:
                   argumentName = "queue";
                   break;

               case ExceptionArgument.stack:
                   argumentName = "stack";
                   break;

               case ExceptionArgument.startIndex:
                   argumentName = "startIndex";
                   break;

               case ExceptionArgument.value:
                   argumentName = "value";
                   break;

               case ExceptionArgument.name:
                   argumentName = "name";
                   break;

               case ExceptionArgument.mode:
                   argumentName = "mode";
                   break;

               case ExceptionArgument.item:
                   argumentName = "item";
                   break;

               case ExceptionArgument.options:
                   argumentName = "options";
                   break;

               case ExceptionArgument.view:
                   argumentName = "view";
                   break;

              case ExceptionArgument.sourceBytesToCopy:
                   argumentName = "sourceBytesToCopy";
                   break;

               default:
                   Contract.Assert(false, "The enum value is not defined, please checked ExceptionArgumentName Enum.");
                   return string.Empty;
          }

           return argumentName;
      }

       //
       // This function will convert an ExceptionResource enum value to the resource string.
       //
       internal static string GetResourceName(ExceptionResource resource) {
           string resourceName = null;

           switch (resource) {
               case ExceptionResource.Argument_ImplementIComparable:
                   resourceName = "Argument_ImplementIComparable";
                   break;

               case ExceptionResource.Argument_AddingDuplicate:
                   resourceName = "Argument_AddingDuplicate";
                   break;

               case ExceptionResource.ArgumentOutOfRange_BiggerThanCollection:
                   resourceName = "ArgumentOutOfRange_BiggerThanCollection";
                   break;

               case ExceptionResource.ArgumentOutOfRange_Count:
                   resourceName = "ArgumentOutOfRange_Count";
                   break;

               case ExceptionResource.ArgumentOutOfRange_Index:
                   resourceName = "ArgumentOutOfRange_Index";
                   break;

               case ExceptionResource.ArgumentOutOfRange_InvalidThreshold:
                   resourceName = "ArgumentOutOfRange_InvalidThreshold";
                   break;

               case ExceptionResource.ArgumentOutOfRange_ListInsert:
                   resourceName = "ArgumentOutOfRange_ListInsert";
                   break;

               case ExceptionResource.ArgumentOutOfRange_NeedNonNegNum:
                   resourceName = "ArgumentOutOfRange_NeedNonNegNum";
                   break;

               case ExceptionResource.ArgumentOutOfRange_SmallCapacity:
                   resourceName = "ArgumentOutOfRange_SmallCapacity";
                   break;

               case ExceptionResource.Arg_ArrayPlusOffTooSmall:
                   resourceName = "Arg_ArrayPlusOffTooSmall";
                   break;

               case ExceptionResource.Arg_RankMultiDimNotSupported:
                   resourceName = "Arg_RankMultiDimNotSupported";
                   break;

               case ExceptionResource.Arg_NonZeroLowerBound:
                   resourceName = "Arg_NonZeroLowerBound";
                   break;

               case ExceptionResource.Argument_InvalidArrayType:
                   resourceName = "Argument_InvalidArrayType";
                   break;

               case ExceptionResource.Argument_InvalidOffLen:
                   resourceName = "Argument_InvalidOffLen";
                   break;

               case ExceptionResource.Argument_ItemNotExist:
                   resourceName = "Argument_ItemNotExist";
                   break;                    

               case ExceptionResource.InvalidOperation_CannotRemoveFromStackOrQueue:
                   resourceName = "InvalidOperation_CannotRemoveFromStackOrQueue";
                   break;

               case ExceptionResource.InvalidOperation_EmptyQueue:
                   resourceName = "InvalidOperation_EmptyQueue";
                   break;

               case ExceptionResource.InvalidOperation_EnumOpCantHappen:
                   resourceName = "InvalidOperation_EnumOpCantHappen";
                   break;

               case ExceptionResource.InvalidOperation_EnumFailedVersion:
                   resourceName = "InvalidOperation_EnumFailedVersion";
                   break;

               case ExceptionResource.InvalidOperation_EmptyStack:
                   resourceName = "InvalidOperation_EmptyStack";
                   break;

               case ExceptionResource.InvalidOperation_EnumNotStarted:
                   resourceName = "InvalidOperation_EnumNotStarted";
                   break;

               case ExceptionResource.InvalidOperation_EnumEnded:
                   resourceName = "InvalidOperation_EnumEnded";
                   break;

               case ExceptionResource.NotSupported_KeyCollectionSet:
                   resourceName = "NotSupported_KeyCollectionSet";
                   break;

               case ExceptionResource.NotSupported_ReadOnlyCollection:
                   resourceName = "NotSupported_ReadOnlyCollection";
                   break;

               case ExceptionResource.NotSupported_ValueCollectionSet:
                   resourceName = "NotSupported_ValueCollectionSet";
                   break;


               case ExceptionResource.NotSupported_SortedListNestedWrite:
                   resourceName = "NotSupported_SortedListNestedWrite";
                   break;


               case ExceptionResource.Serialization_InvalidOnDeser:
                   resourceName = "Serialization_InvalidOnDeser";
                   break;

               case ExceptionResource.Serialization_MissingKeys:
                   resourceName = "Serialization_MissingKeys";
                   break;

               case ExceptionResource.Serialization_NullKey:
                   resourceName = "Serialization_NullKey";
                   break;

               case ExceptionResource.Argument_InvalidType:
                   resourceName = "Argument_InvalidType";
                   break;

               case ExceptionResource.Argument_InvalidArgumentForComparison:
                   resourceName = "Argument_InvalidArgumentForComparison";                    
                   break;

               case ExceptionResource.InvalidOperation_NoValue:
                   resourceName = "InvalidOperation_NoValue";                    
                   break;

               case ExceptionResource.InvalidOperation_RegRemoveSubKey:
                   resourceName = "InvalidOperation_RegRemoveSubKey";                    
                   break;

               case ExceptionResource.Arg_RegSubKeyAbsent:
                   resourceName = "Arg_RegSubKeyAbsent";                    
                   break;

               case ExceptionResource.Arg_RegSubKeyValueAbsent:
                   resourceName = "Arg_RegSubKeyValueAbsent";                    
                   break;
                   
               case ExceptionResource.Arg_RegKeyDelHive:
                   resourceName = "Arg_RegKeyDelHive";                    
                   break;

               case ExceptionResource.Security_RegistryPermission:
                   resourceName = "Security_RegistryPermission";                    
                   break;

               case ExceptionResource.Arg_RegSetStrArrNull:
                   resourceName = "Arg_RegSetStrArrNull";                    
                   break;

               case ExceptionResource.Arg_RegSetMismatchedKind:
                   resourceName = "Arg_RegSetMismatchedKind";                    
                   break;

               case ExceptionResource.UnauthorizedAccess_RegistryNoWrite:
                   resourceName = "UnauthorizedAccess_RegistryNoWrite";
                   break;

               case ExceptionResource.ObjectDisposed_RegKeyClosed:
                   resourceName = "ObjectDisposed_RegKeyClosed";
                   break;

               case ExceptionResource.Arg_RegKeyStrLenBug:
                   resourceName = "Arg_RegKeyStrLenBug";
                   break;

               case ExceptionResource.Argument_InvalidRegistryKeyPermissionCheck:
                   resourceName = "Argument_InvalidRegistryKeyPermissionCheck";
                   break;

               case ExceptionResource.NotSupported_InComparableType:
                   resourceName = "NotSupported_InComparableType";
                   break;

               case ExceptionResource.Argument_InvalidRegistryOptionsCheck:
                   resourceName = "Argument_InvalidRegistryOptionsCheck";
                   break;

               case ExceptionResource.Argument_InvalidRegistryViewCheck:
                   resourceName = "Argument_InvalidRegistryViewCheck";
                   break;

               default:
                   Contract.Assert( false, "The enum value is not defined, please checked ExceptionArgumentName Enum.");
                   return string.Empty;
          }
           return resourceName;
      }
  }

   //
   // The convention for this enum is using the argument name as the enum name
   //
   internal enum ExceptionArgument {
       obj,
       dictionary,
       dictionaryCreationThreshold,
       array,
       info,
       key,
       collection,
       list,
       match,
       converter,
       queue,
       stack,
       capacity,
       index,
       startIndex,
       value,
       count,
       arrayIndex,
       name,
       mode,
       item,
       options,
       view,
       sourceBytesToCopy,
  }
   //
   // The convention for this enum is using the resource name as the enum name
   //
   internal enum ExceptionResource {
       Argument_ImplementIComparable,
       Argument_InvalidType,    
       Argument_InvalidArgumentForComparison,
       Argument_InvalidRegistryKeyPermissionCheck,        
       ArgumentOutOfRange_NeedNonNegNum,
       
       Arg_ArrayPlusOffTooSmall,
       Arg_NonZeroLowerBound,        
       Arg_RankMultiDimNotSupported,        
       Arg_RegKeyDelHive,
       Arg_RegKeyStrLenBug,  
       Arg_RegSetStrArrNull,
       Arg_RegSetMismatchedKind,
       Arg_RegSubKeyAbsent,        
       Arg_RegSubKeyValueAbsent,
       
       Argument_AddingDuplicate,
       Serialization_InvalidOnDeser,
       Serialization_MissingKeys,
       Serialization_NullKey,
       Argument_InvalidArrayType,
       NotSupported_KeyCollectionSet,
       NotSupported_ValueCollectionSet,
       ArgumentOutOfRange_SmallCapacity,
       ArgumentOutOfRange_Index,
       Argument_InvalidOffLen,
       Argument_ItemNotExist,
       ArgumentOutOfRange_Count,
       ArgumentOutOfRange_InvalidThreshold,
       ArgumentOutOfRange_ListInsert,
       NotSupported_ReadOnlyCollection,
       InvalidOperation_CannotRemoveFromStackOrQueue,
       InvalidOperation_EmptyQueue,
       InvalidOperation_EnumOpCantHappen,
       InvalidOperation_EnumFailedVersion,
       InvalidOperation_EmptyStack,
       ArgumentOutOfRange_BiggerThanCollection,
       InvalidOperation_EnumNotStarted,
       InvalidOperation_EnumEnded,
       NotSupported_SortedListNestedWrite,
       InvalidOperation_NoValue,
       InvalidOperation_RegRemoveSubKey,
       Security_RegistryPermission,
       UnauthorizedAccess_RegistryNoWrite,
       ObjectDisposed_RegKeyClosed,
       NotSupported_InComparableType,
       Argument_InvalidRegistryOptionsCheck,
       Argument_InvalidRegistryViewCheck
  }
}