Closed7

Effective C# 6.0/7.0 のメモ(第5章)

だーら(Flamers / Memotia)だーら(Flamers / Memotia)

項目45 契約違反を例外として報告すること

  • 処理が失敗した場合には、例外を返すようにする(エラーコードを返すなどではなく)
    • 例外のクラスお中に、失敗に関する情報を豊富に詰めるようにする。
  • 例外が出ることを前提としてtry...catchブロックを頻繁に書くべきではない。(例外をフロー制御に使用しない)
    • 例外は実行コストが高いから
  • 例外を発生する可能性のあるメソッドを実行する前に、例外を発生させうる条件をチェックするメソッドを使用するべき(nullチェック的な)
だーら(Flamers / Memotia)だーら(Flamers / Memotia)

項目46 usingおよびtry...finallyを使用してリソースの後処理を行う

  • 処理の途中で例外が発生したとしても、Disposeが呼ばれるようにするために、usingまたはtry...finallyを使用する
実験コード
public class Program
{
    public static void Main()
    {
        ShouldDisposeSample sample = null;
        
        // このようにDisposeを呼ぶ場合、SampleMethodが例外を投げた場合にDisposeが呼ばれない
        sample = new ShouldDisposeSample();
        sample.SampleMethod();
        sample.Dispose();
        
        // usingを使えば、SampleMethodが例外を投げた場合でもDisposeが呼ばれる
        using (sample = new ShouldDisposeSample())
        {
            sample.SampleMethod();
        }

        // 以下のように記述してもusingの場合と同じ効果が得られる
        try
        {
            sample = new ShouldDisposeSample();
            sample.SampleMethod();
        }
        finally
        {
            sample.Dispose();
        }
    }
}

public class ShouldDisposeSample : IDisposable
{
    public void SampleMethod()
    {
        Console.WriteLine("SampleMethod is called");
    }

    public void Dispose()
    {
        Console.WriteLine("Disposed is called");
    }
}

出力

SampleMethod is called
Disposed is called
SampleMethod is called
Disposed is called
SampleMethod is called
Disposed is called

SampleMethodの中で例外を発生させた場合

    public void SampleMethod()
    {
        Console.WriteLine("SampleMethod is called");
        throw new Exception("Exception in SampleMethod");
    }
  • 結果: どの書き方をしても、Disposeメソッドのログは出力されなかった。
    • これは本の内容と違うが...??
    • 実行環境の問題なのかな?(未解決)
  • usingの外側でコンストラクトする場合、コンストラクター内で例外が発生した場合はオブジェクトが破棄されないメモリリークが発生する。
    • これは避けるべきで、常にusingステートメントの中かtryブロック中で生成するようにする
        // usingの外でのコンストラクトは例外時にメモリリークにつながる
        ShouldDisposeSample sample = new ShouldDisposeSample();
        using (sample)
だーら(Flamers / Memotia)だーら(Flamers / Memotia)

項目47 アプリケーション固有の例外クラスを作成する

  • 独自の例外クラスを作成するのは、「例外を発生させた問題に対して、異なる対応が必要になることが確実である場合のみ」にするべき
    • catch句の中で独自で作成した個別の例外に対して、その例外を処理・対処できるようなメソッドを呼びたい場合など。

独自の例外クラスを作成するなら

  • Exceptionクラスにある4つのコンストラクタを実装すること
  • サードパーティ製のライブラリでエラーが発生した場合、独自のエラークラスの中で、サードパーティ製ライブラリのエラーをInnerExceptionとして含むようなエラーを作るべき
    • このように、下位レベルの例外から、より詳細の情報を含んだ上位レベルの例外を作成することを 例外翻訳と呼ぶ
        try
        {
            // サードパーティ製のライブラリからエラーの発生する可能性のあるメソッドを呼ぶ
        }
        catch (ThirdPartyException e)
        {
            var msg = "hoge";
            throw new OriginalException(msg, e);
        }
だーら(Flamers / Memotia)だーら(Flamers / Memotia)

項目48 例外を強く保証すること

3つの例外保証

  • 基本的な例外保証...関数内で例外がスローされて処理が別の場所に遷移したとしてもリソースリークを発生させず、また全てのオブジェクトが正しい状態であり続けることを保証する
    • (メモ、usingなどを使ってリソースリークしないようにするとか)
  • 強い例外保証...例外のスロー後もプログラムの状態が変化しないことを保証する
    • 例外がスローされても、「処理が完了する」または「プログラムの状態が変化しない」のどちからで達成
  • 例外をスローしない保証...メソッドから例外が決してスローされないようにすること

強い例外保証をするために

  1. 変更対象のディフェンシブコピーを作成し、そのディフェンシブコピーに対して変更を行う(例外スローの可能性あり)
  2. 例外なく変更できたら、ディフェンシブコピーを元のデータと置き換える
  • これは、不変な値型であるとやりやすい(後述する参照が置き換わらない問題が発生しないから)
  • 以下の例では、参照型であるため、古い参照がキャッシュされて残った結果、意図通り処理されないことになる。
参照型のコピーが引き起こす問題について
public class Program
{
    public static void Main()
    {
        var sample = new SampleClass();
        // 問題!ここで一度保持してしまったsample.Listへの参照は、あとでsample.Listが置き換えられたとしても、反映されない。
        var list = sample.List;
        
        Console.WriteLine($"list(ローカル変数): {list.GetHashCode()}");
        Console.WriteLine($"sample.List: {sample.List.GetHashCode()}");
        foreach (var item in list)
        {
            Console.WriteLine(item);
        }
        
        Console.WriteLine("=== UpdateList()メソッドを呼ぶ ===");
        sample.UpdateList();
        
        Console.WriteLine($"list(ローカル変数): {list.GetHashCode()}");
        Console.WriteLine($"sample.List: {sample.List.GetHashCode()}");
        
        Console.WriteLine("listでforeach");
        // ローカル変数にキャッシュされたものを用いると、古い参照がそのまま利用されてしまう。
        foreach (var item in list)
        {
            Console.WriteLine(item);
        }
        
        Console.WriteLine("sample.Listでforeach");
        // 改めてプロパティにアクセスすれば新しい参照を利用することになる。
        foreach (var item in sample.List)
        {
            Console.WriteLine(item);
        }
    }
}

public class SampleClass
{
    private List<int> _list = new () { 1, 2, 3 };
    public IList<int> List => _list;

    public void UpdateList()
    {
        var tmp = new List<int> { 4, 5, 6 };
        _list = tmp;
    }
}

出力

list(ローカル変数): 58225482
sample.List: 58225482
1
2
3
=== UpdateList()メソッドを呼ぶ ===
list(ローカル変数): 58225482
sample.List: 18643596
listでforeach
1
2
3
sample.Listでforeach
4
5
6
  • 対処法として、Envelop/Letterパターンを用いることができる。
    • Envelopがコレクションをラップする。中に実際のdata(Letter)を持つ。置き換えるときは内部のdataを置き換えればよい。
    • Envelopのインスタンスを、あたかもその中身(data)のLetterにアクセスしているように扱わせるために、Envelopクラスにdataの型のメソッドを実装する必要がある。(具体例は本を参照)
だーら(Flamers / Memotia)だーら(Flamers / Memotia)

項目49 catchからの再スローよりも例外フィルタを使用すること

  • 例外フィルタ(catchの後のwhen)を利用することで、スタックの巻き戻しが起きる前のプログラムの状態がスタックトレースに保存される。そのため、例外の根本的な原因の調査に役立つ情報にアクセスしやすい
  • もし例外フィルタではなく、catch句の中でif文等で条件分岐をさせる場合、ランタイムは例外を処理できるcatch句を見つけると即座にスタックの巻き戻しが行われるため、情報が失われてしまう。
だーら(Flamers / Memotia)だーら(Flamers / Memotia)

項目50 例外フィルタの副作用を活用する

  • ランタイムが適切なcatch句を見つけるまで、ランタイムはスタックを確認していく。このステップの中で例外フィルタが実行される。これはスタックの巻き戻しが起こる前である。
  • 例外フィルタとして常にfalseを返すことで、例外は必ず次へ伝播されていく。
実験コード
public class Program
{
    public static void Main()
    {
        try
        {
            ThrowError();
        }
        catch (Exception e) when (ConsoleLogException(e)) { }
        catch (Exception e)
        {
            Console.WriteLine($"最終catch句: {e}");
        }
    }
    
    private static bool ConsoleLogException(Exception e)
    {
        Console.WriteLine($"エラーをログ出力: {e}");
        return false;
    }
    
    private static void ThrowError()
    {
        throw new Exception("ThrowErrorメソッド");
    }
}

出力

エラーをログ出力: System.Exception: ThrowErrorメソッド
   at Program.ThrowError() in /hoge/Program.cs:line 24
   at Program.Main() in /hoge/Program.cs:line 6
最終catch句: System.Exception: ThrowErrorメソッド
   at Program.ThrowError() in /hoge/Program.cs:line 24
   at Program.Main() in /hoge/Program.cs:line 6
このスクラップは2023/12/02にクローズされました