.NET例外設計原則

黄舟
黄舟オリジナル
2017-02-06 14:48:291371ブラウズ

.NET を使用する場合、例外は避けられない問題ですが、API 設計の観点からこの問題を考慮していない開発者が多すぎます。ほとんどのジョブでは、どの例外をキャッチする必要があるか、どの例外をグローバル ログに書き込む必要があるかを最初から最後まで知っています。例外を正しく使用できる API を設計すると、欠陥の修正にかかる時間を大幅に短縮できます。

誰のせい?

例外設計の背後にある基本理論は、「誰のせいですか?」という質問から始まります。この記事では、この質問に対する答えは常に次の 3 つのうちの 1 つになります:

ライブラリ

アプリケーション

環境

「ライブラリ」に問題があると言うときは、現在実行されているメソッドの 1 つに内部欠陥があることを意味します。この場合、「アプリケーション」はライブラリ メソッドを呼び出すコードです (ライブラリ コードとアプリケーション コードが同じアセンブリ内にある可能性があるため、これは少し混乱します)。最後に、「環境」はアプリケーションの外部にあるすべてのものを指します。制御することはできません。

ライブラリの欠陥

最も典型的なライブラリの欠陥は NullReferenceException です。アプリケーションが検出できる null 参照例外をライブラリがスローする理由はありません。 null が検出された場合、ライブラリ コードは常に、null の内容と問題の修正方法を説明する、より具体的な例外をスローする必要があります。パラメーターの場合、これは明らかに ArgumentNullException です。また、プロパティまたはフィールドが空の場合は、通常、InvalidOperationException の方が適切です。

定義上、ライブラリの欠陥を示す例外は、修正する必要があるライブラリのバグです。これは、アプリケーション コードにバグがないということではなく、ライブラリのバグを最初に修正する必要があるということです。そうして初めて、アプリケーション開発者は自分も間違いを犯したことを知ることができます。

その理由は、同じライブラリを多くの人が利用している可能性があるためです。 1 人が誤って null を渡してはいけない場所に渡してしまうと、他の人も必ず同じ間違いを犯すことになります。 NullReferenceException を何が問題だったかを明確に示す例外に置き換えることにより、アプリケーション開発者は何が問題だったかをすぐに知ることができます。

「成功の穴」

.NET デザイン パターンに関する初期の文献を読むと、「成功の穴」というフレーズによく遭遇します。基本的な考え方は次のとおりです。コードを正しく使用しやすく、誤って使用しにくいものにし、例外によって何が問題になったのかを確実に把握できるようにすることです。この API 設計哲学に従うことで、開発者は最初から正しいコードを書くことがほぼ保証されます。

これが、コメントされていない NullReferenceException が非常に悪い理由です。スタック トレース (ライブラリ コードの非常に深い部分にある場合があります) 以外には、開発者が何が間違っているのかを判断するのに役立つ情報はありません。一方、ArgumentNullException と InvalidOperationException は、ライブラリ作成者がアプリケーション開発者に問題の解決方法を説明する方法を提供します。

その他のライブラリの欠陥

次のライブラリの欠陥は、DivideByZeroException、FiniteNumberException、OverflowException を含む ArithmeticException ファミリです。繰り返しますが、これは、たとえその欠陥が単なるパラメーターの有効性チェックの欠落であったとしても、常にライブラリ メソッドの内部欠陥を意味します。

ライブラリの欠陥のもう 1 つの例は、IndexOutOfRangeException です。意味的には ArgumentOutOfRangeException と変わりません (IList.Item を参照) が、配列インデクサーでのみ機能します。アプリケーション コードは通常、裸の配列を使用しないため、カスタム コレクション クラスにはバグがあることを意味します。

.NET 2.0 でジェネリック リストが導入されて以来、ArrayTypeMismatchException はまれになりました。この例外を引き起こす状況はかなり奇妙です。ドキュメントによると:

ArrayTypeMismatchException は、システムが配列要素を宣言された配列型に変換できない場合にスローされます。たとえば、String 型の要素は、Int32 の配列に格納できません。これは、2 つの型の間で変換が行われないためです。通常、アプリケーションはそのような例外をスローする必要はありません。

これを行うには、前述の Int32 配列を Object[] 型の変数に格納する必要があります。生の配列を使用した場合、ライブラリはこれをチェックする必要があります。この理由と他の多くの考慮事項により、生の配列を使用せず、それらを適切なコレクション クラスにカプセル化することをお勧めします。

通常、他の変換の問題は InvalidCastException 例外を通じて反映されます。トピックに戻ると、型チェックは、InvalidCastException がスローされず、ArgumentException または InvalidOperationException が呼び出し元にスローされることを意味する必要があります。

MemberAccessException は、さまざまなリフレクションベースのエラーをカバーする基本クラスです。リフレクションの直接使用に加えて、COM 相互運用や動的キーワードの誤った使用によって、この例外が発生する可能性があります。

アプリの欠陥

典型的なアプリケーションの欠陥は、ArgumentException とそのサブクラス ArgumentNullException および ArgumentOutOfRangeException です。あなたが知らないかもしれない他のサブクラスは次のとおりです: ization.CultureNotFoundException

  • システム.IO.Log.RegistrationNotFoundException

  • System.Text.DecoderFallbackException

  • System.Text.EncoderFallbackException

  • これらはすべて、アプリケーションに何か問題があり、ライブラリを呼び出す行に問題があることを明確に示しています。方法 。このステートメントの両方の部分が重要です。次のコードを考えてみましょう:

    foo.Customer = null;
    foo.Save();
  • 上記のコードが ArgumentNullException をスローした場合、アプリケーション開発者は混乱するでしょう。現在の行の前で何か問題が発生したことを示す InvalidOperationException をスローする必要があります。
  • ドキュメントとしての例外

  • 一般的なプログラマーは、少なくとも最初からドキュメントを読みません。代わりに、ユーザーはパブリック API を読み取り、コードを作成して実行します。コードが正しく実行されない場合は、Stack Overflow で例外情報を検索してください。プログラマが運が良ければ、正しいドキュメントへのリンクとともに答えを簡単に見つけることができます。しかし、それでも、プログラマーは実際にそれを読むことはおそらくないでしょう。
  • それでは、ライブラリ作成者として、この問題をどのように解決すればよいでしょうか?最初のステップは、部分ドキュメントを例外に直接コピーすることです。

  • その他のオブジェクト ステータス例外
  • InvalidOperationException には、よく知られているサブクラス ObjectDissolvedException があります。その目的は明白ですが、この例外をスローすることを忘れている破壊可能なクラスはほとんどありません。忘れた場合、一般的な結果は NullReferenceException になります。この例外は、Dispose メソッドが破壊可能な子オブジェクトを null に設定することによって発生します。

  • InvalidOperationException と密接に関連しているのは、NotSupportedException 例外です。 2 つの例外は簡単に区別できます。InvalidOperationException は「現在その操作は実行できません」を意味し、NotSupportedException は「このクラスではその操作を実行できません」を意味します。理論的には、NotSupportedException は抽象インターフェイスを使用する場合にのみ発生するはずです。

たとえば、不変コレクションは、IList.Add メソッドに遭遇したときに NotSupportedException をスローする必要があります。対照的に、凍結可能なコレクションは、凍結状態でこのメソッドに遭遇すると InvalidOperationException をスローします。

NotSupportedException のますます重要なサブクラスは、PlatformNotSupportedException です。この例外は、一部の動作環境では操作を実行できるが、他の動作環境では実行できないことを示します。たとえば、.NET Framework のすべての機能が提供されているわけではないため、.NET から UWP または .NET Core にコードを移植する場合は、この例外を使用する必要がある場合があります。

とらえどころのない FormatException

Microsoft は、.NET の最初のバージョンを設計したときにいくつかの間違いを犯しました。たとえば、論理的には FormatException はパラメータ例外タイプであり、ドキュメントにも「この例外はパラメータの形式が無効な場合にスローされる」と記載されています。ただし、何らかの理由で、実際には ArgumentException を継承しません。また、パラメータ名を保存する場所もありません。

私たちの一時的な提案は、FormatException をスローするのではなく、ArgumentException のサブクラスを自分で作成することです。これには、「ArgumentFormatException」または同様の効果を持つ他の名前を付けることができます。これにより、パラメータ名や使用される実際の値などの必要な情報が得られ、デバッグ時間を短縮できます。

これは私たちを「異常なデザイン」という元のテーマに戻します。はい、独自のパーサーが問題を検出したときに FormatException をスローすることもできますが、それはライブラリを使用したいアプリケーション開発者の助けにはなりません。

このフレームワーク設計上の欠陥のもう 1 つの例は、IndexOutOfRangeException です。意味的には ArgumentOutOfRangeException と変わりませんが、これは配列インデクサーだけの特殊なケースなのでしょうか?いいえ、そのように考えるのは間違いです。 IList.Item のインスタンス セットを見ると、このメソッドは ArgumentOutOfRangeException のみをスローします。

環境欠陥

環境欠陥は、データの停止、Web サーバーの応答不能、ファイルの損失など、世界が完璧ではないという事実から生じます。環境上の欠陥がバグ レポートに表示される場合、次の 2 つの側面を考慮する必要があります:

アプリケーションはその欠陥を正しく処理しましたか?

この環境で不具合が発生する原因は何ですか?

通常、これには分業が必要です。まず、アプリケーション開発者が最初に質問に対する答えを探す必要があります。これは、エラーを処理して回復するだけでなく、有用なログを生成することも意味します。

你可能想知道,为什么要从应用程序开发人员开始。应用程序开发人员要对运维团队负责。如果一次Web服务器调用失败,则应用程序开发人员不能只是甩手大叫“不是我的问题”。他或她首先需要确保异常提供了足够的细节信息,让运维人员可以开展他们的工作。如果异常仅仅提供了“服务器连接超时”的信息,那么他们怎么能知道涉及了哪台服务器?

专用异常

NotImplementedException

NotImplementedException表示且仅表示一件事:这项特性还在开发过程中。因此,NotImplementedException提供的信息应该总是包含一个任务跟踪软件的引用。例如:

throw new NotImplementedException("参见工单#42.");

你可以提供更详细的信息,但实际上,你记录的任何信息几乎立刻就会过期。因此,最好是只将读者导向工单,他们可以在那里看到诸如该特性按计划将会在何时实现这样的信息。

AggregateException

AggregateException是必要之恶,但很难使用。它本身不包含任何有价值的信息,所有的细节信息都隐藏在它的InnerExceptions集合中。

由于AggregateException通常只包含一个项,所以在库中将它解封装并返回真正的异常似乎是合乎逻辑的。一般来说,你不能在没有销毁原始堆栈跟踪的情况下再次抛出一个内部异常,但从.NET 4.5开始,该框架提供了使用ExceptionDispatchInfo的方法。

解封装AggregateException

catch (AggregateException ex)
{   
   if (ex.InnerExceptions.Count == 1) //解封装        
       ExceptionDispatchInfo.Capture(ex.InnerExceptions[0]).Throw();  
  else     
     throw; //我们真的需要AggregateException
}

无法回答的情况

有一些异常无法简单地纳入这个主题。例如,AccessViolationException表示读取非托管内存时有问题。对,那可能是由原生库代码所导致的,也可能是由应用程序错误地使用了同样的代码库所导致的。只有通过研究才能揭示这个Bug的本质。

如果可能,你就应该在设计时避免无法回答的异常。在某些情况下,Visual Studio的静态代码分析器甚至可以分析该规则所涵盖的标识冲突。

例如,ApplicationException实际上已经废弃。Framework设计指南明确指出,“不要抛出或继承ApplicationException。”为此,应用程序不必抛出ApplicationException异常。虽说初衷如此,但看下下面这些子类:

  • Microsoft.JScript.BreakOutOfFinally

  • Microsoft.JScript.ContinueOutOfFinally

  • Microsoft.JScript.JScriptException

  • Microsoft.JScript.NoContextException

  • Microsoft.JScript.ReturnOutOfFinally

  • System.Reflection.InvalidFilterCriteriaException

  • System.Reflection.TargetException

  • System.Reflection.TargetInvocationException

  • System.Reflection.TargetParameterCountException

  • System.Threading.WaitHandleCannotBeOpenedException

显然,这些子类中有一些应该是参数异常,而其他的则表示环境问题。它们全都不是“应用程序异常”,因为他们只会被.NET Framework的库抛出。

同样的道理,开发人员不应该直接使用SystemException。同ApplicationException一样,SystemException的子类也是各不相同,包括ArgumentException、NullReferenceException和AccessViolationException。微软甚至建议忘掉SystemException的存在,而只使用其子类。

无法回答的情况有一个子类别,就是基础设施异常。我们已经看过AccessViolationException,以下是其他的基础设施异常:

  • CannotUnloadAppDomainException

  • BadImageFormatException

  • DataMisalignedException

  • TypeLoadException

  • TypeUnloadedException

这些异常通常很难诊断,可能会揭示出库或调用它的代码中存在的难以理解的Bug。因此,和ApplicationException不同,把它们归为无法回答的情况是合理的。

实践:重新设计SqlException

请记住这些原则,让我们看下SqlException。除了网络错误(你根本无法到达服务器)外,在SQL Server的master.dbo.sysmessages表中有超过11000个不同的错误代码。因此,虽然该异常包含了你需要的所有底层信息,但是,除了简单地捕获&记录外,你实际上难以做任何事。

如果我们要重新设计SqlException,那么我们会希望,根据我们期望用户或开发人员做什么,将其分解成多个不同的类别。

  • SqlClient.NetworkException会表示所有说明数据库服务器本身之外的环境存在问题的错误代码。

  • SqlClient.InternalException には、サーバーの重大な障害 (データベースの破損やハード ディスクへのアクセス不能など) を示すエラー コードが含まれます。

  • SqlClient.SyntaxException は ArgumentException と同等です。これは、(直接または ORM バグにより) 不正な SQL をサーバーに渡していることを意味します。

  • SqlClient.MissingObjectException は、構文は正しいが、データベース オブジェクト (テーブル、ビュー、ストアド プロシージャなど) が存在しない場合に発生します。

  • SqlClient.DeadlockException は、同じ情報を変更しようとしたときに 2 つ以上のプロセスが競合した場合に発生します。

これらの異常はそれぞれ、一連の行動を暗示しています。

  • SqlClient.NetworkException: 操作を再試行してください。頻繁に発生する場合は、運用保守員にご連絡ください。

  • SqlClient.InternalException: すぐに DBA に連絡してください。

  • SqlClient.SyntaxException: アプリケーションまたはデータベースの開発者に通知します。

  • SqlClient.MissingObjectException: 運用および保守担当者に、最後のデータベース展開で何かが失われたかどうかを確認するよう依頼してください。

  • SqlClient.DeadlockException: 操作を再試行してください。頻繁に発生する場合は、設計上のエラーを探してください。

これを実際のジョブで行う場合は、11,000 以上の SQL Server エラー コードをすべてこれらのカテゴリの 1 つにマッピングする必要がありますが、これは特に気の遠くなるような作業であり、これが説明されています。なぜ SqlException はこのようなものなのでしょうか?


概要

API を設計するときは、問題の修正を容易にするために、実行する必要があるアクションの種類に応じて例外を整理する必要があります。これにより、自己修正コードの作成が容易になり、より正確なログを保持し、問題を適切な担当者またはチームに迅速に伝えることができます。


著者について

Jonathan Allen は、1990 年代後半から医療機関向けの MIS プロジェクトに参加し始め、Access と Excel からエンタープライズ レベルのソリューションに徐々にアップグレードしてきました。彼は、ハイエンド ユーザー インターフェイスの開発に移行することを決定する前に、金融業界向けの自動取引システムのコーディングに 5 年間を費やしました。余暇には、15 世紀から 17 世紀の西洋の戦闘技術について研究し、執筆することが好きです。

上記は .NET 例外設計原則の内容です。さらに関連する内容については、PHP 中国語 Web サイト (www.php.cn) に注目してください。


声明:
この記事の内容はネチズンが自主的に寄稿したものであり、著作権は原著者に帰属します。このサイトは、それに相当する法的責任を負いません。盗作または侵害の疑いのあるコンテンツを見つけた場合は、admin@php.cn までご連絡ください。