在这篇文章中,我将继续发表一系列文章,其结果将是一本有关.NET CLR和.NET的工作的书。 链接-欢迎来到cat。
异常架构
关于异常主题的最重要的问题之一可能是在应用程序中构建异常体系结构的问题。 这个问题很有趣,原因有很多。 对我而言,最主要的是表面上的简单性,并不总是很明显该怎么做。 此属性是在各处使用的所有基本构造所固有的:它是IEnumerable
, IDisposable
和IObservable
以及其他。 一方面,他们通过其简单性来招呼自己,使自己参与各种情况下的使用。 另一方面,它们到处都是漩涡和浅滩,从那里,他们根本不知道如何有时不出去。 而且,也许看一下未来的交易量,您的问题已经成熟:那么在特殊情况下会怎样?
注意事项
在哈布雷(Habré)上发表的这一章没有更新,可能有点过时了。 因此,请转到原始文本以获取更多最新文本:

但是,为了得出有关特殊情况类的体系结构构建的结论,我们必须在分类方面积累一些经验。 毕竟,只有了解了我们将要处理的内容,程序员应该如何以及在何种情况下选择错误的类型,以及在哪种情况下(关于捕获或跳过异常的选择),您才能理解如何以对用户显而易见的方式构建类型系统。代码。 因此,我们将尝试根据各种标准对异常情况(不是异常本身的类型,而是确切的情况)进行分类。
根据捕获预期异常的理论可能性
从理论上讲,异常可以很容易地分为两种:可以准确拦截的异常和极有可能拦截的异常。 为什么具有很高的概率 ? 因为总有人会尝试拦截,尽管不一定要完全做到这一点。
让我们首先揭示第一类的特征:应该并且将会捕获的异常。
当我们引入这种类型的异常时,一方面,我们通知外部子系统,我们处于我们数据内的进一步操作没有意义的位置。 另一方面,我们的意思是没有破坏全局,如果将其删除,则什么也不会改变,因此可以轻松拦截此异常以改善情况。 此属性非常重要:它确定错误的严重性,并确定如果捕获异常并清除资源,则可以安全地进一步执行代码。
第二组,不管听起来有多奇怪,都对不需要捕获的异常负责。 它们只能用于写入错误日志,而不能以某种方式纠正这种情况。 最简单的示例是ArgumentException
和NullReferenceException
组异常。 确实,在正常情况下,例如,您不应捕获ArgumentNullException
异常,因为问题的根源将是您,而不是其他任何人。 如果捕获到此异常,则假定您犯了一个错误,并给出了您无法将其赋予的方法:
void SomeMethod(object argument) { try { AnotherMethod(argument); } catch (ArgumentNullException exception) { // Log it } }
在此方法中,我们尝试捕获ArgumentNullException
。 但是我认为,它的拦截非常奇怪:将正确的参数扔给该方法完全是我们的关注。 事后做出反应是不正确的:在这种情况下,可以做的最正确的事情是在调用该方法之前预先检查传输的数据,或者甚至更好地以无法接收不正确参数的方式构造代码。
另一组是消除致命错误。 如果某个高速缓存中断了,并且子系统的运行在任何情况下都不正确? 然后这是一个致命错误,并且不能保证最接近堆栈的代码将其拦截:
T GetFromCacheOrCalculate() { try { if(_cache.TryGetValue(Key, out var result)) { return result; } else { T res = Strategy(Key); _cache[Key] = res; return res; } } catch (CacheCorreptedException exception) { RecreateCache(); return GetFromCacheOrCalculate(); } }
并让CacheCorreptedException
为一个异常,表示“硬盘驱动器上的缓存不一致”。 事实证明,如果此类错误的原因对于缓存子系统而言是致命的(例如,对缓存文件没有权限),那么如果进一步的代码无法使用RecreateCache
命令重新创建缓存,则进一步的代码本身就是错误所在。
关于实际拦截的异常
另一个使我们无法在编程算法上发展思维的问题是理解:捕获这些或其他异常是否值得,或者值得理解的人让它们通过它们。 将我们需要解决的问题翻译成术语,就是要区分责任领域。 让我们看下面的代码:
namespace JetFinance.Strategies { public class WildStrategy : StrategyBase { private Random random = new Random(); public void PlayRussianRoulette() { if(DateTime.Now.Second == (random.Next() % 60)) { throw new StrategyException(); } } } public class StrategyException : Exception { /* .. */ } } namespace JetFinance.Investments { public class WildInvestment { WildStrategy _strategy; public WildInvestment(WildStrategy strategy) { _strategy = strategy; } public void DoSomethingWild() { ?try? { _strategy.PlayRussianRoulette(); } catch(StrategyException exception) { } } } } using JetFinance.Strategies; using JetFinance.Investments; void Main() { var foo = new WildStrategy(); var boo = new WildInvestment(foo); ?try? { boo.DoSomethingWild(); } catch(StrategyException exception) { } }
建议的两种策略中哪一种更正确? 责任范围非常重要。 最初看来,由于WildInvestment
的工作及其一致性完全取决于WildStrategy
, WildStrategy
,如果WildInvestment
仅忽略此异常,它将进入更高的层次,因此无需执行其他任何操作。 但是,请注意,这是一个纯粹的体系结构问题: Main
方法通过调用体系结构不同的方法从体系结构一层捕获异常。 使用方面看起来像什么? 是的,通常看起来像这样:
- 对于这种例外情况的关注只是我们所克服的;
- 此类的用户不确定在我们之前有无数种方法抛出此异常
- 我们开始吸引不必要的成瘾,这些成瘾被我们消除了,从而导致了中间层。
但是,从这个结论可以得出另一个结论:我们必须在DoSomethingWild
方法中设置catch
。 这对我们来说有点奇怪: WildInvestment
似乎非常依赖某人。 即 如果PlayRussianRoulette
无法正常工作,则也可以执行DoSomethingWild
:它没有返回码,但必须播放轮盘。 在这种看似绝望的情况下该怎么办? 答案实际上很简单:在另一层, DoSomethingWild
应该抛出自己的异常,该异常引用此层并将原始内容包装为问题的原始来源InnerException
:
namespace JetFinance.Strategies { pubilc class WildStrategy { private Random random = new Random(); public void PlayRussianRoulette() { if(DateTime.Now.Second == (random.Next() % 60)) { throw new StrategyException(); } } } public class StrategyException : Exception { /* .. */ } } namespace JetFinance.Investments { public class WildInvestment { WildStrategy _strategy; public WildInvestment(WildStrategy strategy) { _strategy = strategy; } public void DoSomethingWild() { try { _strategy.PlayRussianRoulette(); } catch(StrategyException exception) { throw new FailedInvestmentException("Oops", exception); } } } public class InvestmentException : Exception { /* .. */ } public class FailedInvestmentException : Exception { /* .. */ } } using JetFinance.Investments; void Main() { var foo = new WildStrategy(); var boo = new WildInvestment(foo); try { boo.DoSomethingWild(); } catch(FailedInvestmentException exception) { } }
将异常转到另一个异常中,我们从本质上将问题从一个应用程序层转移到另一个应用程序层,从此类用户( Main
方法)的角度来看,使其工作更加可预测。
对于重用问题
很多时候,我们面临着艰巨的任务:一方面,我们懒于创建新的异常类型,而当我们做出决定时,并不总是清楚要提出什么:以哪种类型为基础。 但是正是这些决定决定了特殊情况的整个架构。 让我们看一下流行的解决方案并得出一些结论。
在选择异常类型时,您可以尝试采用现有的解决方案:找到名称中具有相似含义的异常并使用它。 例如,如果通过某种不适合我们的参数为我们提供了一个实体,则可以抛出InvalidArgumentException
,指示Message中错误的原因。 这种情况看起来不错,尤其是考虑到InvalidArgumentException
位于不受强制捕获的异常组中。 但是,如果您要处理任何数据,那么选择InvalidDataException
将会很糟糕。 仅仅因为此类型在System.IO
区域中,这几乎不是您要做的。 即 事实证明,由于懒惰地自己做而找到现有类型几乎总是错误的方法。 几乎没有为一般任务创建的例外。 它们几乎都是针对特定情况创建的,它们的重用将严重违反特殊情况的体系结构。 不仅如此,在收到某种类型的异常(例如相同的System.IO.InvalidDataException
)之后,用户会感到困惑:一方面,他会将System.IO
中的问题根源视为异常名称空间,另一方面将其视为完全不同的抛出点名称空间。 另外,考虑引发此异常的规则,它将转到referencesource.microsoft.com并查找引发该异常的所有位置 :
internal class System.IO.Compression.Inflater
他会明白 只是有人弯曲了手 异常类型的选择使他感到困惑,因为引发异常的方法不涉及压缩。
另外,为了简化重用,您可以通过声明带有错误代码的ErrorCode
字段并随后愉快地生活,来简单地采用和创建一个异常。 看起来:一个很好的解决方案。 到处都抛出相同的异常,设置代码,仅捕获一个catch
从而提高了应用程序的稳定性:没有其他事情要做。 但是,请不同意这个立场。 一方面,在整个应用程序中以这种方式进行操作当然可以简化您的生活。 但是另一方面,您放弃了捕获某个异常子组的功能,这些功能由某些常用功能结合在一起。 例如,如何使用ArgumentException
做到这一点,该ArgumentException
在其自身下通过继承合并了一组异常。 第二个严重的减号是太大且无法读取的代码片,它们将按错误代码组织过滤。 但是,如果您采用另一种情况:当错误的确定对于最终用户不重要时,引入泛化类型和错误代码看起来已经是更正确的应用程序了:
public class ParserException : Exception { public ParserError ErrorCode { get; } public ParserException(ParserError errorCode) { ErrorCode = errorCode; } public override string Message { get { return Resources.GetResource($"{nameof(ParserException)}{Enum.GetName(typeof(ParserError), ErrorCode)}"); } } } public enum ParserError { MissingModifier, MissingBracket, // ... } // Usage throw new ParserException(ParserError.MissingModifier);
保护解析器调用的代码几乎总是无关紧要的,因为阻止解析的原因是什么:错误事实本身对此很重要。 但是,如果这仍然很重要,则用户将始终能够从ErrorCode
提取错误代码。 为此,根本不需要通过Message
的子字符串来搜索必要的单词。
如果从忽略重用问题开始,则可以为每种情况创建一个异常类型。 一方面,这看起来合乎逻辑:一种错误是一种异常。 但是,在这里,就像在所有内容中一样,主要的事情并不是要过度使用它:在每个发布点上执行异常操作,会导致拦截问题:调用方法的代码将被catch
块重载。 毕竟,他需要处理您想要给他的所有类型的异常。 另一个缺点是纯粹的建筑性。 如果您不使用继承,那么您会使这些异常的用户感到迷惑:它们之间可能有很多共同点,并且您必须单独拦截它们。
尽管如此,在特定情况下还是有引入特定类型的良好方案。 例如,当发生细分时,不是针对整个实体,而是针对特定方法。 然后,此类型应该在继承层次结构中的某个位置,这样就不会考虑将其与其他对象一起拦截:例如,通过单独的继承分支进行选择。
此外,如果将这两种方法结合使用,则可以得到一个非常强大的工具箱来处理一组错误:您可以引入一个泛化的抽象类型,从中可以继承特定的特定情况。 基类(我们的泛型类型)必须配备一个存储错误代码的抽象属性,并且继承者将覆盖此属性以指定此错误代码:
public abstract class ParserException : Exception { public abstract ParserError ErrorCode { get; } public override string Message { get { return Resources.GetResource($"{nameof(ParserException)}{Enum.GetName(typeof(ParserError), ErrorCode)}"); } } } public enum ParserError { MissingModifier, MissingBracket } public class MissingModifierParserException : ParserException { public override ParserError ErrorCode { get; } => ParserError.MissingModifier; } public class MissingBracketParserException : ParserException { public override ParserError ErrorCode { get; } => ParserError.MissingBracket; } // Usage throw new MissingModifierParserException(ParserError.MissingModifier);
通过这种方法,我们可以获得什么奇妙的特性?
- 一方面,我们保留了按基本类型捕获异常的功能;
- 另一方面,通过基本类型捕获异常,仍然有可能找出特定情况;
- 除此之外,无需使用类的平面结构,就可以针对特定类型(而不是基本类型)进行拦截。
对于我来说,这是一个非常方便的选择。
关于一组行为情况
根据前面描述的推理可以得出什么结论? 让我们尝试制定它们:
首先,让我们决定情况的含义。 在谈论类和对象时,我们主要习惯于具有某些内部状态的操作实体,我们可以在这些内部状态下执行操作。 事实证明,通过这样做,我们发现了第一类行为情况:对某个实体的动作。 此外,如果从外部看对象图,您会注意到它在逻辑上被组合为功能组:第一个处理缓存,第二个处理数据库,第三个处理数学计算。 层可以通过所有这些功能组:各种内部状态的日志记录层,进程日志记录,方法调用的跟踪。 层可以更具有包容性:组合多个功能组。 例如,模型层,控制器层,表示层。 这些组可以处于同一集合中,也可以处于完全不同的集合中,但是每个组都可以创建自己的特殊情况。
事实证明,如果您以此方式进行辩论,则可以基于属于特定组或层的类型来构建某种类型的特殊情况类型的层次结构,从而在这种类型层次结构中创建代码来捕获异常以便于语义导航的可能性。
让我们看一下代码:
namespace JetFinance { namespace FinancialPipe { namespace Services { namespace XmlParserService { } namespace JsonCompilerService { } namespace TransactionalPostman { } } } namespace Accounting { /* ... */ } }
看起来像什么? 对我而言,命名空间是一个根据异常行为将异常类型自然分组的绝好机会:属于某些组的所有事物都应存在,包括异常。 此外,当您收到某个异常时,除了其类型的名称之外,您还将看到其名称空间,该名称空间将清楚地确定其从属关系。 还记得在System.IO
名称空间中实际定义的InvalidDataException
类型的错误重用示例吗? 它属于此名称空间的意思是,从本质上讲,可以从System.IO
名称空间中或更嵌套的类中抛出此类的异常。 但是异常本身是从一个完全不同的地方抛出的,这使研究人员感到困惑。 通过将异常类型集中在与引发这些异常的类型相同的名称空间上,一方面可以保持类型体系结构的一致性,另一方面,可以使最终开发人员更容易理解发生事件的原因。
在代码级别进行分组的第二种方法是什么? 继承:
public abstract class LoggerExceptionBase : Exception { protected LoggerExceptionBase(..); } public class IOLoggerException : LoggerExceptionBase { internal IOLoggerException(..); } public class ConfigLoggerException : LoggerExceptionBase { internal ConfigLoggerException(..); }
此外,如果在普通应用程序实体的情况下,继承是指行为和数据的继承,通过属于单个实体组来组合类型,那么在例外的情况下,继承是指属于一组情况 ,因为例外的本质不是本质,而是有问题的。
结合两种分组方法,我们可以得出一些结论:
- 必须在程序集(
Assembly
)内部提供此程序集引发的异常的基本类型。 这种类型的异常必须在程序集的根名称空间中。 这将是分组的第一层; - 此外,在程序集本身内部,可能存在一个或多个不同的名称空间。 它们中的每一个都将程序集划分为一些功能区域,从而定义了该程序集中出现的情况组。 这些可以是控制器,数据库实体,数据处理算法等区域。 对我们来说,这些名称空间是按功能隶属关系对类型进行分组,从异常的角度来看,按同一程序集的问题区域进行分组。
- 异常的继承只能来自相同名称空间中或更根的类型。 这样可以确保最终用户对情况有明确的了解,并且在根据基本类型进行拦截时,不会拦截剩余异常。一致认为,这将是奇怪的接收
global::Finiki.Logistics.OhMyException
,有catch(global::Legacy.LoggerExeption exception)
,但看起来绝对和谐下面的代码:
namespace JetFinance.FinancialPipe { namespace Services.XmlParserService { public class XmlParserServiceException : FinancialPipeExceptionBase { // .. } public class Parser { public void Parse(string input) { // .. } } } public abstract class FinancialPipeExceptionBase : Exception { } } using JetFinance.FinancialPipe; using JetFinance.FinancialPipe.Services.XmlParserService; var parser = new Parser(); try { parser.Parse(); } catch (XmlParserServiceException exception) { // Something wrong in parser } catch (FinancialPipeExceptionBase exception) { // Something else wrong. Looks critical because we don't know real reason }
, : , , , XmlParserServiceException
. , , , JetFinance.FinancialPipe.FinancialPipeExceptionBase
, : XmlParserService
, . , catch
: .
?
- . . — , : , -, UI. 即 ;
- , : ,
catch
; - – . ;
- , . : , , , . , - : , — , , , ;
- , : ;
- , Mixed Mode c ErrorCode.
. , , :
- unsafe , . : , (, ) ;
- , , , .. . , , . , , . — —
InnerExcepton
. — ; - 我们自己的代码被随机输入到不一致状态。解析文本就是一个很好的例子。没有外部依赖关系,没有撤消
unsafe
,但是存在解析错误。
链接到整本书
