Effective C# 3rd 読書メモ 21 ジェネリッククラスは必ずDisposableな型パラメータをサポートするようにする

ジェネリッククラスでIDisposableを実装している型を受け取る際は、適切にリソースを破棄する必要がある。
リソースの破棄には以下のようなイディオムが利用できる。

somethingIDisposableを実装していない場合、キャストの結果はnullになるので、usingのブロックは実行されない。

ジェネリッククラス自身がIDisposableを実装しなければならない場合、実装はやや面倒になる。

ここでは、クラスをsealedにすることで、Disposeが必ず実行されるようにしている。

リソースの生成・破棄をクラスの責務から外すことで、実装を簡素化することもできる。

Effective C# 3rd 読書メモ 20 順序関係はIComparableとIComparerで実装する

独自に定義する型をコレクションがソートしたり検索したりするときのために、順序関係を必要とする場合がある。.NET Frameworkは、順序関係を実装するために2つのインターフェイスを提供している。IComparable<T>IComparer<T>である。

IComparableインターフェイスには、メソッドが1つだけ含まれる。CompareTo()である。比較結果は以下のような戻り値によって表現される。

  • 負の数: 現在のオブジェクト < 比較対象
  • 0: 現在のオブジェクト = 比較対象
  • 1以上: 現在のオブジェクト > 比較対象

新しめの.NET FrameworkのAPIはIComparable<T>を使用するが、古いAPIの中にはIComparableを使うものもある。そのため、IComparable<T>を実装する際は、IComparableも実装すべきである。
以下は、IComparable<T>およびIComparableの実装例。

さらに、比較の方法自体をカスタマイズしたい場合は、IComparer<T>を使うことができる。

このようにすれば、Customer.CompareTo()でnameによる比較を提供しつつ、Customer.RevenueCompare.Compare()でrevenueに基づいた比較も提供することができる。

Effective C# 3rd 読書メモ 19 実行時型チェックを使ってジェネリックのアルゴリズムを特化させる

以下のコードは、通常のEnumerable<T>とは逆順に走査を行う、ReverseEnumerable<T>の実装である、
ここでは、以下の工夫によってコピーの回数を減らし、パフォーマンスを向上させている。

  • IList<T>を実装している型はそのまま利用する
  • ICollection<T>を実装している型からサイズを取得する

さらに特化させる方法として、文字列を逆順に操作するためのReverseStringEnumerable<T>を定義し、ReverseEnumerable<string>として呼び出された場合に使用する、といった特化の仕方も考えられる。
このように、実行時型チェックによって、渡された型に応じた最適な処理を実装できる。

Effective C# 3rd 読書メモ 18 最小限かつ十分な制約を定義する

ジェネリクスの型パラメータに宣言する制約は、クラスが役割を果たすのに必要な振る舞いを特定する。
制約を行うのとは別のアプローチとして、渡された型が条件を満たすか実行時にチェックする、という方式もある。
これに対して、型パラメータで制約を行えば、コンパイル時にエラーになるため、実行時エラーよりも早く間違いに気づくことができる。
しかし、制約が厳しすぎると、クラスの利用者にとって使いづらくなってしまう。
ジェネリクスの型パラメータには、最小限かつ十分な制約を定義することが重要である。

Effective C# 3rd 読書メモ 17 標準的なDisposeパターンを実装する

ファイルやDB接続等、.NET Frameworkの管理下にないリソース(unmanaged resource)を利用する際は、適切な後処理を行う必要がある。.NET Frameworkにおいて標準的に利用されるパターンは、IDisposableインタフェースの実装である。

リソースの後処理は、原則Disposeメソッドで行う。クラスの利用者がDisposeを呼び忘れた場合に備えて、ファイナライザでもunmanaged resourceの後処理を行うのが安全である。また、Disposeとファイナライザは、いずれも実際のリソース解放処理を仮想メソッドとして定義して、派生クラスが必要に応じて振る舞いを変更できるようにするとよい。

派生クラスでは、必要に応じてリソース処理の仮想メソッドをオーバーライドする。さらに、自分自身のメンバーフィールドがunmanaged resourceである場合に限って、ファイナライザを実装する。いずれの場合でも、ベースクラスのリソース解放メソッドを呼ぶことを忘れないように。

IDisposable.Dispose()メソッドの実装は、4つの仕事をする責務を負う。

  1. 全てのunmanaged resourceを解放する
  2. 全てのmanaged resourceを解放する(フックしないイベントも含む)
  3. オブジェクトがDispose済みであることを示すフラグをセットする。なお、publicメンバーは、Dispose後に呼ばれた場合には、このフラグを確認して、ObjectDisposed例外を投げる必要がある
  4. ファイナライザの実行を抑制する。これにはGC.SuppressFinalize(this)が利用できる。

IDisposableの典型的な実装は以下のようになる。

なお、ファイナライザは、unmanaged resourceを解放する必要がある場合にのみ定義すべきである。GCは、ファイナライザの定義されたオブジェクトを、ファイナライザ実行待ちのキューに入れ、その後ファイナライザを実行してからオブジェクトを破棄する。そのため、ファイナライザを定義すると、ファイナライザがない場合よりもオブジェクトの生存期間が長くなり、パフォーマンス上の悪影響がある。

また、Disposeでは、リソースの解放以外の処理を実行してはいけない。

参考:IDisposable インターフェイス