ホームページ  >  記事  >  バックエンド開発  >  .NET での例外処理のベスト プラクティス (翻訳)

.NET での例外処理のベスト プラクティス (翻訳)

黄舟
黄舟オリジナル
2017-02-17 11:23:181236ブラウズ

元のアドレス: クリックしてリンクを開きます



この記事は、CodeProject の元のアドレスの記事を翻訳したものです。

目次

  • はじめに

  • 最悪の事態に備えてください

    • 事前に確認してください

    • 外部データを信用しないでください

    • 信頼できるデバイス: カメラ、マウス、キーボード

    • 「書き込み操作」も失敗する可能性があります

  • 安全なプログラミング

    • 「新しい例外( )"

    • 重要な例外情報を Message プロパティに保存しないでください

    • 各スレッドには try/catch ブロックを含める必要があります

    • 例外をキャッチしたら、それを記録します

    • Exception.Message の値を記録するだけでなく、Exception.ToString() も記録する必要があります

    • 特定の例外をキャプチャするには

    • 例外のスローを中止しないでください

    • クリーンアップコードは、ブロック内のfinallyに配置する必要があります

    • using

    • を使用することを忘れないでください

    • メソッドで発生する例外を示すために特別な戻り値を使用しないでください

    • リソースが存在しないことを示すために「例外のスロー」を使用しないでください

    • 関数の実行結果として「例外のスロー」を使用しないでください

    • 無視できないエラーを強調表示するには、「例外のスロー」を使用してください

    • スタックトレース情報をクリアしないでください

    • 例外クラスはシリアル化可能としてマークする必要があります

    • 「例外のスロー」を使用してくださいDebug.Assert の代わりに

  • 各例外クラスには、少なくとも 3 つのコンストラクター メソッドが含まれています

  • 車輪の再発明はしないでください

    • VB.NET

    • C# で using ステートメントをシミュレートします

  • 非構造化例外処理 (On Error goto) を使用しないでください

概要

はじめに

「私のソフトウェア プログラムは決してうまくいかない"。あなたはそれを信じていますか?きっとみんなが私を嘘つきだと叫ぶだろう。 「ソフトウェア プログラムにバグがないことはほぼ不可能です

」 実際、信頼できる健全なソフトウェア プログラムを開発することは不可能ではありません。私が言及しているのは原子力発電所の制御に使用されるソフトウェアではなく、サーバーまたは PC 上で実行される可能性のあるいくつかの一般的なソフトウェアです。これらのソフトウェアは、重大な問題が発生します。ご想像のとおり、私が言いたかったのは、ソフトウェアのエラー率が比較的低く、エラーの原因をすぐに見つけて修正でき、エラーによって重大なデータ損傷が引き起こされることはないということです。

つまり、私が言いたいのは、ソフトウェアが比較的安定しているということです。

ソフトウェアにバグがあるのは当然です。しかし、それが再発するバグであり、十分な情報がないためにすぐに修正できない場合、それは許しがたいことです。

上で述べたことをよりよく理解するために、例を挙げてみましょう: ハードディスクが不足している場合に、数え切れないほどの商用ソフトウェアが次のようなエラー プロンプトを表示するのをよく見かけます:

「顧客情報の更新に失敗しました。システムにお問い合わせください」管理者に再試行してください。」

🎜🎜🎜これら以外の情報は記録されません。このエラーの原因を突き止めるには非常に時間がかかり、プログラマーは実際に問題の原因を特定する前にさまざまな推測を行う必要がある場合があります。 🎜🎜

この記事では、主に .NET プログラミングで例外をより適切に処理する方法について説明しますが、適切な「エラー プロンプト メッセージ」を表示する方法については説明するつもりはありません。この仕事は UI インターフェイス開発者の仕事であると考えているためです。それは UI の種類と、最終的にソフトウェアを使用する人によって異なります。たとえば、一般ユーザー向けのテキスト エディターの「エラー メッセージ」は、Socket 通信フレームワークの「エラー メッセージ」とはまったく異なるものでなければなりません。後者の直接のユーザーはプログラマーであるためです。

最悪の事態に備える

いくつかの基本的な設計原則に従うことで、プログラムをより堅牢にし、エラー発生時のユーザー エクスペリエンスを向上させることができます。ここで私が話している「ユーザー エクスペリエンスの向上」とは、エラー プロンプト フォームがユーザーを満足させるという意味ではなく、発生したエラーによって元のデータが損傷したり、コンピューター全体がクラッシュしたりしないことを意味します。プログラムでハードディスク不足エラーが発生しても、そのプログラムが他の悪影響を引き起こさない場合 (翻訳者注: エラー メッセージが表示されるだけで、他の問題は引き起こさない)、ユーザー エクスペリエンスは向上します。

  • 事前に確認してください

強力な型チェックと検証は、バグを回避する強力な方法です。問題を早く検出すればするほど、より早く修正できるようになります。 「なぜ InvoiceItems テーブルの ProductID 列に CustomerID データがあるのか​​?」を数か月後に理解するのは簡単ではなく、非常に面倒です。基本型 (int、string など) の代わりにクラスを使用して顧客データを保存すると、コンパイラーは、今起こったこと (CustomerID と ProductID の混同を指す、翻訳者の注記) の発生を許可しません。

  • 外部データを信頼しないでください

外部データは信頼できないため、当社のソフトウェアプログラムは使用前に厳密にチェックする必要があります。これらの外部データがレジストリ、データベース、ハードディスク、ソケット、またはキーボードを使用して書き込まれたファイルからのものであるかどうかに関係なく、これらすべての外部データは使用前に厳密にチェックする必要があります。構成ファイルを完全に信頼しているプログラムを何度も目にします。これは、これらのプログラムを開発するプログラマが、構成ファイルを編集して破損する人はいないと常に想定しているためです。

  • 信頼できるデバイス: カメラ、マウス、キーボード

外部データを使用する必要がある場合、次の状況が発生する可能性があります:

1) セキュリティ権限が不十分です

2) データが存在しません

3) データが不完全です

4) データは完全ですが、形式が間違っています

データソースがキーであってもファイルであっても関係ありませんレジストリ、ソケット、データベース、Web サービス、シリアル ポートなど、上記のすべての状況が発生する可能性があります。すべての外部データは失敗する可能性があります。

  • 「書き込み操作」も失敗する可能性があります

信頼できないデータ ソースは、信頼できないデータ ウェアハウスでもあります。データを保存するときに、同様の状況が依然として発生する可能性があります:

1) セキュリティ権限が不十分

2) デバイスが存在しない

3) 十分なスペースがありません

4) ストレージ物理エラーデバイス上で が発生しました

これが、一部の圧縮ソフトウェアが、ソース ファイルを直接変更するのではなく、作業中に一時ファイルを作成し、作業が完了したときにその名前を変更する理由です。理由は、ハードディスクが破損した場合(またはソフトウェアに異常があった場合)、元のデータが失われる可能性があるためです。 (翻訳者はこの状況に遭遇しました。データのバックアップ中に停電があり、元の古いバックアップが破損しました。翻訳者注)

安全なプログラミング

私の友人はこう言いました。私 : 優れたプログラマは、自分のプログラムに間違ったコードを決して書きません。これは良いプログラマになるための必要条件ではあるが、十分条件ではないと思います。以下に、例外処理を行うときに作成する可能性のある「悪いコード」をいくつかまとめました。

  • 「new Exception()」をスローしないでください

こんなことはしないでください。例外は非常に抽象的な例外クラスです。このタイプの例外をキャッチすると、通常、多くの悪影響が生じます。通常、独自の例外クラスを定義し、システム (フレームワーク) によってスローされる例外と自分自身によってスローされる例外を区別する必要があります。

  • 重要な例外情報を Message プロパティに保存しないでください。

例外はクラスにカプセル化されます。例外情報を返す必要がある場合は、その情報を (Message プロパティではなく) いくつかの別のプロパティに保存してください。そうしないと、必要な情報を Message プロパティから解析することが困難になります。たとえば、スペル ミスを修正する必要があるだけの場合、メッセージ プロパティにエラー メッセージやその他のプロンプト コンテンツを文字列形式で書き込んだ場合、他のユーザーはどのようにして必要なエラー メッセージを簡単に取得できるでしょうか。彼らがどれほどの努力をしなければならないかは想像もつきません。

  • 各スレッドには try/catch ブロックが含まれている必要があります

一般的な例外処理は、プログラム内の比較的集中された場所に配置されます。各スレッドには try/catch ブロックが必要です。そうしないと、いくつかの例外が見逃され、理解しにくい問題が発生します。プログラムがバックグラウンド タスクを処理するために複数のスレッドを開始する場合、通常は各スレッドの実行結果を保存する型を作成します。このとき、各スレッドで発生する可能性のある例外を格納するフィールドを型に追加することを忘れないでください。そうしないと、メインスレッドは他のスレッドの例外を認識できなくなります。一部の「ファイア アンド フォーゲット」状況 (つまり、メイン スレッドは、スレッドの開始後にスレッドの実行ステータスを気にしなくなる、翻訳者注) では、メイン スレッドの例外処理ロジックを子スレッドにコピーする必要がある場合があります。 。 行く。

  • 例外をキャッチしたら、それを記録してください

プログラムがログをどのように記録するか(log4net、EIF、イベントログ、TraceListeners、テキストファイルなど)は関係ありません。 。重要なことは、例外が発生した場合は、それをどこかに記録する必要があるということです。ただし、ログは 1 回だけにしてください。そうしないと、大量の繰り返し情報を含む非常に大きなログ ファイルが作成されてしまいます。

  • Exception.Message の値をログに記録するだけでなく、Exception.ToString() の値もログに記録する必要があります

ロギングについて話すときは、ログを記録する必要があることを忘れないでください。 Exception.Message の代わりに Exception.ToString() 値。 Exception.ToString() には「スタック トレース」情報、内部例外情報、メッセージが含まれているためです。通常、この情報は非常に重要であり、Exception.Message のみをログに記録する場合は、「オブジェクト参照がヒープ内のインスタンスを指していません」のようなプロンプトが表示されるだけです。

  • 特定の例外をキャッチするには

例外をキャッチしたい場合は、できるだけ特定の例外(例外ではなく)をキャッチするようにしてください。

初心者が、優れたコードとは例外をスローできないコードであると言っているのをよく見かけます。実際、この記述は間違っています。優れたコードは必要に応じて対応する例外をスローする必要があり、優れたコードは処理方法を知っている例外のみをキャッチできます (この文に注意してください、翻訳者注)。

次のコードは、このルールの説明として機能します。以下のコードを書いた人が見たら私を殺すでしょうが、これは実際のプログラミング作業から抜粋したコードです。

最初のクラス MyClass は 1 つのアセンブリにあり、2 番目のクラス GenericLibrary は別のアセンブリにあります。開発マシンでは正常に動作しますが、テストマシンでは、毎回入力されたデータが正当であるにもかかわらず、常に「データが無効です!」という例外がスローされます。

その理由を教えてもらえますか?


public class MyClass
{
    public static string ValidateNumber(string userInput)
    {
        try
        {
            int val = GenericLibrary.ConvertToInt(userInput);
            return "Valid number";
        }
        catch (Exception)
        {
            return "Invalid number";
        }
    }
}

public class GenericLibrary
{
    public static int ConvertToInt(string userInput)
    {
        return Convert.ToInt32(userInput);
    }
}


この問題の理由は、例外処理があまり具体的ではないことです。 MSDN の紹介によると、Convert.ToInt32 メソッドは ArgumentException、FormatException、OverflowException の 3 つの例外のみをスローします。したがって、これら 3 つの例外のみを処理する必要があります。

问题发生在我们程序安装的步骤上,我们没有将第二个程序集(GenericLibrary.dll)打包进去。所以程序运行后,ConvertToInt方法会抛出FileNotFoundException异常,但是我们捕获的异常是Exception,所以会提示“数据不合法”。

  • 不要中止异常上抛

最坏的情况是,你编写catch(Exception)这样的代码,并且在catch块中啥也不干。请不要这样做。

  • 清理代码要放在finally块中

大多数时候,我们只处理某一些特定的异常,其它异常不负责处理。那么我们的代码中就应该多一些finally块(就算发生了不处理的异常,也可以在finally块中做一些事情,译者注),比如清理资源的代码、关闭流或者回复状态等。请把这当作习惯。

有一件大家容易忽略的事情是:怎样让我们的try/catch块同时具备易读性和健壮性。举个例子,假设你需要从一个临时文件中读取数据并且返回一个字符串。无论什么情况发生,我们都得删除这个临时文件,因为它是临时性的。

让我们先看看最简单的不使用try/catch块的代码:


string ReadTempFile(string FileName)
{
    string fileContents;
    using (StreamReader sr = new StreamReader(FileName))
    {
        fileContents = sr.ReadToEnd();
    }
    File.Delete(FileName);
    return fileContents;
}


这段代码有一个问题,ReadToEnd方法有可能抛出异常,那么临时文件就无法删除了。所以有些人修改代码为:


string ReadTempFile(string FileName)
{
    try
    {
        string fileContents;
        using (StreamReader sr = new StreamReader(FileName))
        {
            fileContents = sr.ReadToEnd();
        }
        File.Delete(FileName);
        return fileContents;
    }
    catch (Exception)
    {
        File.Delete(FileName);
        throw;
    }
}


这段代码变得复杂一些,并且它包含了重复性的代码。

那么现在让我们看看更简介更健壮的使用try/finally的方式:


string ReadTempFile(string FileName)
{
    try
    {
        string fileContents;
        using (StreamReader sr = new StreamReader(FileName))
        {
            fileContents = sr.ReadToEnd();
        }
        File.Delete(FileName);
        return fileContents;
    }
    catch (Exception)
    {
        File.Delete(FileName);
        throw;
    }
}


变量fileContents去哪里了?它不再需要了,因为返回点在清理代码前面。这是让代码在方法返回后才执行的好处:你可以清理那些返回语句需要用到的资源(方法返回时需要用到的资源,所以资源只能在方法返回后才能释放,译者注)。

  • 不要忘记使用using

仅仅调用对象的Dispose()方法是不够的。即使异常发生时,using关键字也能够防止资源泄漏。(关于对象的Dispose()方法的用法,可以关注我的书,有一章专门介绍。译者注

  • 不要使用特殊返回值去表示方法中发生的异常

因为这样做有很多问题:

  1)直接抛出异常更快,因为使用特殊的返回值表示异常时,我们每次调用完方法时,都需要去检查返回结果,并且这至少要多占用一个寄存器。降低代码运行速度。

  2)特殊返回值能,并且很可能被忽略

  3)特殊返回值不能包含堆栈跟踪(stack trace)信息,不能返回异常的详细信息

  4)很多时候,不存在一个特殊值去表示方法中发生的异常,比如,除数为零的情况:


public int pide(int x, int y)
{
    return x / y;
}
  • 不要使用“抛出异常”的方式去表示资源不存在

微软建议在某些特定场合,方法可以通过返回一些特定值来表示方法在执行过程中发生了预计之外的事情。我知道我上面提到的规则恰恰跟这条建议相反,我也不喜欢这样搞。但是一些API确实使用了某些特殊返回值来表示方法中的异常,并且工作得很好,所以我还是觉得你们可以谨慎地遵循这条建议。

我看到了.NET Framework中很多获取资源的API方法使用了特殊返回值,比如Assembly.GetManifestStream方法,当找不到资源时(异常),它会返回null(不会抛出异常)。

  • 不要将“抛出异常”作为函数执行结果的一种

这是一个非常糟糕的设计。代码中包含太多的try/catch块会使代码难以理解,恰当的设计完全可以满足一个方法返回各种不同的执行结果(绝不可能到了必须使用抛出异常的方式才能说明方法执行结果的地步,译者注),如果你确实需要通过抛出异常来表示方法的执行结果,那只能说明你这个方法做了太多事情,必须进行拆分。(这里原文的意思是,除非确实有异常发生,否则一个方法不应该仅仅是为了说明执行结果而抛出异常,也就是说,不能无病呻呤,译者注

  • 可以使用“抛出异常”的方式去着重说明不能被忽略的错误

我可以举个现实中的例子。我为我的Grivo(我的一个产品)开发了一个用来登录的API(Login),如果用户登录失败,或者用户并没有调用Login方法,那么他们调用其他方法时都会失败。我在设计Login方法的时候这样做的:如果用户登录失败,它会抛出一个异常,而并不是简单的返回false。正因为这样,调用者(用户)才不会忽略(他还没登录)这个事实。

  • 不要清空了堆栈跟踪(stack trace)信息

堆栈跟踪信息是异常发生时最重要的信息,我们经常需要在catch块中处理一些异常,有时候还需要重新上抛异常(re-throw)。下面来看看两种方法(一种错误的一种正确的):

错误的做法:


try
{
    // Some code that throws an exception
}
catch (Exception ex)
{
    // some code that handles the exception
    throw ex;
}



为什么错了?因为当我们检查堆栈跟踪信息时,异常错误源变成了“thorw ex;”,这隐藏了真正异常抛出的位置。试一下下面这种做法:


try
{
    // Some code that throws an exception
}
catch (Exception ex)
{
    // some code that handles the exception
    throw;
}

有什么变化没?我们使用“throw;”代替了“throw ex;”,后者会清空原来的堆栈跟踪信息。如果我们在抛出异常时没有指定具体的异常(简单的throw),那么它会默认地将原来捕获的异常继续上抛。这样的话,上层代码捕获的异常还是最开始我们通过catch捕获的同一个异常。

拓展阅读:

C# 异常处理(Catch Throw)IL分析

  • 异常类应标记为Serializable

很多时候,我们的异常需要能被序列化。当我们派生一个新的异常类型时,请不要忘了给它加上Serializable属性。谁会知道我们的异常类会不会用在Remoting Call或者Web Services中呢?

  • 使用”抛出异常”代替Debug.Assert

当我们发布程序后,不要忘了Debug.Assert将会被忽略。我们在代码中做一些检查或者验证工作时,最好使用抛出异常的方式代替输出Debug信息。

  将输出Debug信息这种方式用到单元测试或者那些只需要测试当软件真正发布后确保不会出错的场合。

  • 每个异常类至少包含三个构造方法

做这件事相当简单(直接从其他的类型粘贴拷贝相同的代码即可),如果你不这样做,那么别人在使用你编写的异常类型时,很难遵守上面给出的一些规则的。

  我指的哪些构造方法呢?这三个构造方法可以参见这里。

 

不要重复造轮子

已经有很多在异常处理方面做得比较好的框架或库,微软提供的有两个:

Exception Management Application Block

Microsoft Enterprise Instrumentation Framework

上記のルールの一部に従わない場合、これらのライブラリはあまり役に立たない可能性があることに注意してください。

VB.NET

記事全体を読むと、すべてのサンプル コードが C# で記述されていることがわかります。それは、C# が私が好む .NET 言語であり、VB.NET には独自の特別なルールがあるためです。

  • C# で using ステートメントをシミュレートする

残念ながら、VB.NET には using ステートメントはありません。オブジェクトのアンマネージ リソースを解放するたびに、これを行う必要があります:

上記の方法で DISpose メソッドを呼び出さない場合、エラーが発生する可能性があります (Dispose For メソッドについて)電話の場合は、翻訳者ノートにご注意ください)。

  • 非構造化例外処理 (On Error Goto) を使用しないでください

非構造化例外処理は「On Error Goto」とも呼ばれ、Djikstra (Edzhar Dexter) La) は 1974 年にこう言いました。 goto ステートメントは役立つというより有害です。」これはもう 30 年前のことです。コードからすべての goto ステートメントを削除してください。保証しますが、役に立ちません。 (エドガー・ダイクストラは、「goto有害理論」、セマフォ、PVプリミティブを提案し、興味深い哲学者問題を解決しました。著書「ソフトウェア・ストーリー」でFortran言語について語る際に言及されました。訳者注)

概要

この記事が、コーディングの品質を向上させるのに役立つことを願っています。また、この記事が、例外処理の開始と作成したプログラムをより堅牢にする方法についても説明することを願っています。

翻訳者の言葉:

私には欠点があります、私のようなネチズンがいるかどうかはわかりません。私は遅咲きで、テクノロジーについても同様で、多くのものが人気のピークを過ぎてから初めて何かを感じ始めました。一番の理由は、私が新しいことがあまり好きではないということですが、二番目の理由は、いつも何かを習得する前に変えてしまう、つまり途中で諦めてしまうということです。実際、これが非常にまずいことであることもわかっています。結局のところ、IT 業界は急速に発展しており、追いつくことができなくなるとすぐに後退してしまいます。

このような矛盾した状況に遭遇したときこそ、私は知識を学ぶときに技術間の共通性を学ぶことに重点を置きます。いわゆる共通性とは、10年、20年、さらには30年ではないことを意味します。あなたが現在働いている会社が実際の開発プロセスで特定のフレームワークを使用しており、「このフレームワークをどのように使用するか」に固執する場合も同様です。 「良いシステムを作ろう」と考えていると、数年後には時代に乗り遅れてしまうかもしれません。そして、プロトコルやシステム間の相互作用原理などのプログラミングの共通点を研究すると、一見時代遅れに見える PC プログラムでも、Web プログラムでも、現在人気のモバイル APP でも、これらはあらゆるネットワーク通信システムで使用されていることがわかります。が使用され、基本原理は同じです。見れば見るほど新しいものが出てきて、薬は変えずにスープを変えているようです(ちょっと大袈裟:-))

なので、私と同じような方や、新しいものにはあまり従わない、または長年開発を続けている人へのアドバイスは、「テクノロジー間の共通点を見つけて、そこに留まらないこと」です。新しいものに十分な興味があり、十分なエネルギーがない限り、テクノロジーの表面には触れられません。

上記の言葉は弊社の座談会でも共有されました。



著者: Zhou Jianzhi出典: http://www.php.cn/
この記事の著作権は著者とブログパークに属します。転載は歓迎されますが、この声明は必須です。著者の同意なしに保持し、記事ページのわかりやすい位置に原文へのリンクを提供します。そうでない場合、当社は法的責任を追及する権利を留保します。


追加:

CLR の「2 ラウンド トラバーサル」例外処理戦略について。
アプリケーションに多層のネストされた例外キャッチ構造がある場合、最下層で例外が発生した場合 (実際には中間層でも同じことが当てはまります)、CLR はまず例外が発生した層で catch ステートメント ブロックを検索します。 「互換性のある」
このタイプの例外を処理するコードがない場合は、上位層に「ジャンプ」して検索します。上位層がまだない場合は、「上位層」を探し続けます。上層の「layer」をアプリケーションのTopmostまで移動します。
これは、ネストされた例外キャッチ構造の CLR アプリケーションの「最初のラウンド」の走査であり、適切な例外ハンドラーを見つけます。
例外ハンドラーが特定のレベルで見つかった場合、CLR はそれをすぐには実行せず、「事故シーン」に戻り、「第 2 ラウンド」トラバーサルを再度実行し、すべてのfinally ステートメント ブロックを実行することに注意してください。次に、
を実行して例外ハンドラーを見つけ、最後にこの層から最上位に移動して、すべてのfinallyステートメントブロックを実行します。


上記は、.NET での例外処理のベスト プラクティス (翻訳) です。その他の関連コンテンツについては、PHP 中国語 Web サイト (www.php.cn) に注目してください。


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