【问题标题】:c# exception handling, practical example. How would you do it?c#异常处理,实例。你会怎么做?
【发布时间】:2025-12-22 17:30:17
【问题描述】:

我正在努力提高处理异常的能力,但是当我尽力捕捉它们时,我觉得我的代码变得非常丑陋、不可读和混乱。我很想看看其他人如何通过给出一个实际的例子和比较解决方案来解决这个问题。

我的示例方法从 URL 下载数据并尝试将其序列化为给定类型,然后返回填充数据的实例。

首先,完全没有任何异常处理:

    private static T LoadAndSerialize<T>(string url)
    {            
        var uri = new Uri(url);
        var request = WebRequest.Create(uri);
        var response = request.GetResponse();
        var stream = response.GetResponseStream();

        var result = Activator.CreateInstance<T>();
        var serializer = new DataContractJsonSerializer(result.GetType());
        return (T)serializer.ReadObject(stream);            
    }

我觉得这样的方法可读性很强。我知道该方法中有一些不必要的步骤(例如 WebRequest.Create() 可以接受一个字符串,并且我可以在不给它们变量的情况下链接方法)但是我会这样保留它以便更好地与有异常的版本进行比较-处理。

这是处理所有可能出错的第一次尝试:

    private static T LoadAndSerialize<T>(string url)
    {
        Uri uri;
        WebRequest request;
        WebResponse response;
        Stream stream;
        T instance;
        DataContractJsonSerializer serializer;

        try
        {
            uri = new Uri(url);
        }
        catch (Exception e)
        {
            throw new Exception("LoadAndSerialize : Parameter 'url' is malformed or missing.", e);
        }

        try
        {
            request = WebRequest.Create(uri);
        }
        catch (Exception e)
        {
            throw new Exception("LoadAndSerialize : Unable to create WebRequest.", e);
        }

        try
        {
            response = request.GetResponse();
        }
        catch (Exception e)
        {
            throw new Exception(string.Format("LoadAndSerialize : Error while getting response from host '{0}'.", uri.Host), e);
        }

        if (response == null) throw new Exception(string.Format("LoadAndSerialize : No response from host '{0}'.", uri.Host));

        try
        {
            stream = response.GetResponseStream();
        }
        catch (Exception e)
        {
            throw new Exception("LoadAndSerialize : Unable to get stream from response.", e);
        }

        if (stream == null) throw new Exception("LoadAndSerialize : Unable to get a stream from response.");

        try
        {
            instance = Activator.CreateInstance<T>();
        }
        catch (Exception e)
        {
            throw new Exception(string.Format("LoadAndSerialize : Unable to create and instance of '{0}' (no parameterless constructor?).", typeof(T).Name), e);
        }

        try
        {
            serializer = new DataContractJsonSerializer(instance.GetType());
        }
        catch (Exception e)
        {

            throw new Exception(string.Format("LoadAndSerialize : Unable to create serializer for '{0}' (databinding issues?).", typeof(T).Name), e);
        }


        try
        {
            instance = (T)serializer.ReadObject(stream);
        }
        catch (Exception e)
        {
            throw new Exception(string.Format("LoadAndSerialize : Unable to serialize stream into '{0}'.", typeof(T).Name), e);                   
        }

        return instance;
    }

这里的问题是,虽然所有可能出错的东西都会被捕获并给出一个有意义的例外,但这是一个相当大比例的混乱。

那么,如果我改为链式捕获会怎样。我的下一个尝试是这样的:

    private static T LoadAndSerialize<T>(string url)
    {
        try
        {
            var uri = new Uri(url);
            var request = WebRequest.Create(uri);
            var response = request.GetResponse();
            var stream = response.GetResponseStream();
            var serializer = new DataContractJsonSerializer(typeof(T));
            return (T)serializer.ReadObject(stream);
        }
        catch (ArgumentNullException e)
        {
            throw new Exception("LoadAndSerialize : Parameter 'url' cannot be null.", e);
        }             
        catch (UriFormatException e)
        {
            throw new Exception("LoadAndSerialize : Parameter 'url' is malformed.", e);
        }
        catch (NotSupportedException e)
        {
            throw new Exception("LoadAndSerialize : Unable to create WebRequest or get response stream, operation not supported.", e);
        }
        catch (System.Security.SecurityException e)
        {
            throw new Exception("LoadAndSerialize : Unable to create WebRequest, operation was prohibited.", e);
        }
        catch (NotImplementedException e)
        {
            throw new Exception("LoadAndSerialize : Unable to get response from WebRequest, method not implemented?!.", e);
        }
        catch(NullReferenceException e)
        {
            throw new Exception("LoadAndSerialize : Response or stream was empty.", e);
        }
    }

虽然看起来确实更容易,但我在这里非常倾向于智能感知,以提供所有可能从方法或类中抛出的异常。我不相信这个文档是 100% 准确的,如果某些方法来自 .net 框架之外的程序集,我会更加怀疑。例如,DataContractJsonSerializer 在智能感知上没有显示异常。这是否意味着构造函数永远不会失败?我可以确定吗?

与此相关的其他问题是某些方法会抛出相同的异常,这使得错误更难描述(这个或这个或这个出错),因此对用户/调试器的用处不大。

第三种选择是忽略所有异常,除了那些允许我采取诸如重试连接之类的操作的异常。如果 url 为 null,则 url 为 null,捕获它的唯一好处是更详细的错误消息。

我很想看看你的想法和/或实现!

【问题讨论】:

  • 出于这个原因,我经常渴望一种类似于 case 语句的直通功能来捕获异常。
  • 通过用自己的异常包装异常来增加什么价值?简单地不捕捉这些异常并让它们冒泡解开有什么问题?您真的需要进一步解释 UriFormatException 是与 Uri 相关的异常吗?这不是已经很明显了吗?
  • @Siege - 你的意思是“失败”。
  • 捕获您的代码可以 100% 处理的异常 - 只有一个容易理解的原因。否则,您所做的只是 a) 添加噪音,或 b) 隐藏异常。
  • 我认为您的方法中有太多代码。你可以拆分它。

标签: c# exception-handling


【解决方案1】:

异常处理规则之一——不要捕获你不知道如何处理的异常。

仅仅为了提供漂亮的错误消息而捕获异常是值得怀疑的。异常类型和消息已经为开发人员包含了足够的信息 - 您提供的消息没有增加任何价值。

DataContractJsonSerializer 在智能感知上没有显示异常。这是否意味着构造函数永远不会失败?我可以确定吗?

不,你不能确定。 C# 和 .NET 通常不像 Java,您必须声明可能抛出的异常。

第三种选择是忽略所有异常,除了那些允许我采取诸如重试连接之类的操作的异常。

这确实是最好的选择。

您还可以在应用程序顶部添加一个通用异常处理程序,该处理程序将捕获所有未处理的异常并记录它们。

【讨论】:

  • 这听起来很合理,当我写这篇文章时,我开始怀疑。现在我所需要的只是一种摆脱烦人的 reshaper squigglies 的方法,它们告诉我某些东西可能为 null,除了在那里抛出异常之外我无能为力,无论如何都会发生这种情况。
  • @Toodleey:这是一个有效的警告。对于您自己的参数,如果参数为空,您应该抛出ArgumentNullException(这使调用者很明显这是错误的调用代码)。对于法律允许返回 null 的函数,您不应忽略这种情况;抛出 NullReferenceException 无法告诉调用者出了什么问题(即,被调用代码中的错误还是调用代码中的错误)。对于不允许返回 null 的函数,Assert 是理想的。
【解决方案2】:

首先,阅读我关于异常处理的文章:

http://ericlippert.com/2008/09/10/vexing-exceptions/

我的建议是:您必须处理代码可能引发的“令人烦恼的异常”和“外生异常”。令人烦恼的异常是“非异常”异常,因此您必须处理它们。由于您无法控制的考虑因素,可能会发生外生异常,因此您必须处理它们。

不得处理致命和愚蠢的异常。您不需要处理愚蠢的异常,因为您永远不会做任何导致它们被抛出的事情。如果它们被抛出,那么你有一个错误,解决方案是修复错误。不处理异常;那是隐藏错误。而且您无法有意义地处理致命异常,因为它们是致命的。该过程即将下降。您可能会考虑记录致命异常,但请记住,记录子系统可能首先触发了致命异常。

简而言之:只处理那些可能发生知道如何处理的异常。如果您不知道如何处理,请将其留给您的来电者;来电者可能比你更清楚。

在您的特定情况下:不要在此方法中处理任何异常。让调用者处理异常。如果调用者向您传递了一个无法解析的 url,则使它们崩溃。如果错误的 url 是一个 bug,那么调用者就有一个 bug 需要修复,你通过提请他们注意是在帮他们一个忙。如果错误的 url 不是错误——比如说,因为用户的互联网连接被搞砸了——那么调用者需要通过询问 real 来找出获取失败的原因 > 例外。调用者可能知道如何恢复,所以请帮助他们。

【讨论】:

    【解决方案3】:

    首先,出于所有实际目的,您应该永远不要抛出类型Exception。总是抛出一些更具体的东西。即使ApplicationException 会稍微好一点。其次,当且仅当调用者有理由关心哪个操作失败时,对不同的操作使用单独的 catch 语句。如果在您的程序中的某个时间点发生的 InvalidOperationException 将暗示您的对象的状态与在其他时间发生的状态不同,并且如果您的调用者将关心区别,那么您应该包装第一部分您的程序位于“try/catch”块中,该块会将InvalidOperationException 包装在其他一些(可能是自定义的)异常类中。

    “只捕获你知道如何处理的异常”的概念在理论上很好,但不幸的是,大多数异常类型对底层对象的状态非常模糊,以至于几乎不可能知道是否可以“处理”异常或不是。例如,一个人可能有一个TryLoadDocument 例程,它必须在内部使用在无法加载文档的某些部分时可能引发异常的方法。在发生此类异常的 99% 的情况下,“处理”此类异常的正确方法是简单地放弃部分加载的文档并返回而不将其暴露给调用者。不幸的是,很难确定这 1% 的情况是不够的。在您的例程没有做任何事情而失败的情况下,与可能有其他不可预测的副作用的情况下,您应该努力抛出不同的异常;不幸的是,您可能会被困在对您调用的例程的大多数异常的解释上。

    【讨论】:

    • ApplicationException 已被放弃。来自 MSDN 文章 Best Practices for Handling Exceptions:“对于大多数应用程序,从 Exception 类派生自定义异常。最初认为自定义异常应该从 ApplicationException 类派生;但在实践中尚未发现这会增加显着价值。”
    • @AllonGuralnek:我的评论并不是要鼓励投掷ApplicationException,而是要劝阻投掷Exception。投掷ApplicationException 非常糟糕,但投掷Exception 更糟糕。实际上,标准异常“层次结构”的设计几乎毫无用处,因为处理异常时的 50,000 美元问题通常是“将处理操作未按预期完成的事实足以处理异常”,而层次结构的layout nothing 可以回答这个问题。例如,当尝试添加 IDictionary 项目时...
    • ...许多不同的事情可能会出错,具体取决于IDictionary 的实现。在某些情况下,IDictionary 将有效,Add 尝试无效。在其他情况下,IDictionary 或传入项中的某些内容可能已损坏,从而导致由Add 方法调用的方法抛出ArgumentException。调用者需要知道的不是尝试失败的原因,而是IDictionary 的状态以及它使用的任何静态对象。不幸的是,异常层次结构无法传达该信息。
    • 我认为抛出 Exception 没有任何问题。在某些情况下,您会抛出不打算在代码中处理的异常。异常的消息可能会记录到文件中或由通用异常处理程序显示给用户,但代码不应将其与任何其他不可预见的异常区别对待。对于这种情况,创建自定义异常类不会增加任何价值,因此我认为这是个坏建议。无论如何,我不会建议某人使用ApplicationException,而不是ArrayList,因为从所有意图和目的来看,它已经过时了。
    【解决方案4】:

    异常 e.message 应该有足够多的错误消息数据供您正确调试。当我进行异常处理时,我通常只记录一些关于它发生的位置和实际异常的简短信息。

    我不会那样拆分它,那只会弄得一团糟。例外情况主要针对您。理想情况下,如果您的用户导致异常,您会更早发现它们。

    我不建议抛出不同的命名异常,除非它们不是真正的异常(例如,有时在某些 API 调用中,响应变为 null。我通常会检查它并为我抛出一个有用的异常)。

    【讨论】:

      【解决方案5】:

      看看Unity Interception。在该框架内,您可以使用称为ICallHandler 的东西,它允许您拦截呼叫并对拦截的呼叫执行您需要/想要执行的任何操作。

      例如:

      public IMethodReturn Invoke(IMethodInvocation input, 
          GetNextHandlerDelegate getNext)
      {
          var methodReturn = getNext().Invoke(input, getNext);
          if (methodReturn.Exception != null)
          {
              // exception was encountered... 
              var interceptedException = methodReturn.Exception
      
              // ... do whatever you need to do, for instance:
              if (interceptedException is ArgumentNullException)
              {
                  // ... and so on...
              }             
          }
      }
      

      当然还有其他拦截框架。

      【讨论】:

        【解决方案6】:

        考虑将方法拆分成更小的方法,以便对相关错误进行错误处理。

        您在同一个方法中发生了多个半不相关的事情,因为结果错误处理必须或多或少地在每行代码中进行。

        即对于您的情况,您可以将方法拆分为:CreateRequest(在此处处理无效参数错误)、GetResponse(处理网络错误)、ParseRespone(处理内容错误)。

        【讨论】:

          【解决方案7】:

          我不同意@oded 的说法:

          “异常处理规则之一——不要捕获你不知道如何处理的异常。”

          出于学术目的可能没问题,但在现实生活中,您的客户不希望他们的脸上突然出现非信息性错误。

          我认为您可以并且应该捕获异常,它们会为用户生成一些信息丰富的异常。当一个很好的错误信息显示给用户时,它可以获得更多关于他/她应该做什么来解决问题的信息。

          此外,当您决定记录错误或者更好地自动将它们发送给您时,捕获所有异常会很有用。

          我所有的项目都有一个错误类,我总是使用它来捕获每个异常。尽管我在这门课上做的不多,但它就在那里,它可以用来做很多事情。

          【讨论】: