Effective C# 第3章

www.amazon.co.jp

個人的な読書ノートであり、自分の解釈が誤っていたり要点がずれてる可能性があります。内容が気になる人は自分で読んでください。

だんだん内容が濃くなってきて、サクサクは読み進めれなくなった。でもその分役立つポイント多くて面白い。

3章 ジェネリックによる処理

18. 最低限必須となる制約をつねに定義すること

  • 型引数への制約を行わない場合、型引数はSystem.Objectに定義されたインターフェースしか扱えない。必要な制約は実行時にチェックする必要あり
  • 型引数への制約を追加すると、System.Objectを超えたインターフェースを型引数に期待できる。必要な制約はコンパイル時にチェックされる
  • あらゆる型引数に対応するクラスを作るのが大変。制約をつけることでクラス作成者の負担が減るが、かわりに利用者は利用しにくくなる。
  • 型引数に制約を追加することで得られる安全性と、制約に従うために必要となる作業量のバランスを取る。
  • 前提条件を最小限に抑えつつ、本当に必要となる条件だけをすべて制約として指定する。

最低限必要な制約が何か、自信を持つためには言語仕様やあらゆる標準インターフェースの継承関係など理解してないといけなくて超ハードじゃないですか?

19. 実行時の型チェックを使用してジェネリックアルゴリズムを特化する

public sealed class ReverseEnumerable<T> : IEnumerable<T> { ... }

と定義して実装することで、色々な列に対応できる。 ランダムアクセス性があるIListに特化した実装を用意したいときは以下のようにする。

// 定義
IEnumerable<T> sourceSequence;
IList<T> originalSequence;

// コンストラクタ内
collection = srcCollection;
originalSequence = collection as IList<T>;

// IListにキャストできたかどうかで実装をかえる。
if (originalSequence == null)
{
    ...
}
  • 特定の型に特化した実装をジェネリッククラスの内部に隠蔽することが目標。
  • 再利用性を最大限に高めつつ、特定型に特化した実装を提供することができる。

20. IComparableとIComparerにより順序関係を実装する

  • IComparable:自分自身と他のオブジェクトを比較
  • IComparer:引数で与えられた2要素の比較
  • ジェネリック版のIComparableもあわせて実装すべき。
    • 明示的に int IComparable.CompareTo(object obj) と宣言する。
  • 演算子オーバーロードも実装するとよい。
  • static Comparsion<T> デリゲートを公開して他のジェネリックAPIでも使えるようにする。
  • 他の要素で比較するクラスを内部クラスとして定義してやるとよい。
  • 同値性==は必ずしも実装しなくてよい。

21. 破棄可能な型引数をサポートするようにジェネリック型を作成すること

  • IDisposableを実装した型引数を考慮する。
  • ローカル変数の場合:IDisposable実装していなくてもusing使って問題ない。
  • メンバ変数にする場合:IDisposableを実装する
  • sealed classにすることでIDisposableの実装を簡略化できる。

22. ジェネリックの共変性と反変性をサポートする

  • C#4.0以降の機能
  • 共変 = covariant = out
  • 反変 = contravariant = in
  • 不変 = invariant
  • 上記まとめて分散性というらしい
  • インターフェースやデリゲートを作成する場合、できる限りin, out キーワードを指定する。
  • 難しく考えず入力専用の型ならin, 出力専用の型ならout を指定すれば良い。

最後の段落のまとめに誤植(共変反変が逆になってる)があって悩んだ.. これは使ったことない機能で勉強になった。

23. 型パラメータにおけるメソッドの制約をデリゲートとして定義する

  • たいていの場合にはクラスやインターフェース制約を指定するのが適切。
  • このクラス使うにはこのインターフェースを継承して使ってね、という感じでクラスを作るとき、インターフェースはデリゲートで代替したほうがいい。
  • ほとんどの場合、ジェネリッククラスで呼び出す必要のあるメソッドは特定のデリゲートとして置き換え可能。

これは役にたった。

24. 親クラスやインターフェース用に特化したジェネリックメソッドを作成しないこと

  • ジェネリックメソッドは型引数に該当するあらゆる型と一致する。
  • ジェネリックメソッドになっているメソッドがあるときに、オーバーロードで特定のインターフェース用に特化したメソッドを作っても、ジェネリックメソッド側が使われてしまう。
  • 明示的にインターフェースにキャスト呼び出せば使える。
  • ジェネリックメソッドの中で実行時にインターフェースチェックをしても良いが、実行時オーバーヘッドがかかるので大きなクラスではやらないほうが良い。

なるほど。知らないとやってしまいそうだった。

25. 型引数がインスタンスのフィールドではない場合にはジェネリックメソッドとして定義すること

  • ジェネリッククラスの型引数に制約を与えると、クラス全体に制約がかかる。
  • ジェネリッククラスの中に、ジェネリックメソッドを定義することができる。
  • さらに非ジェネリックメソッドを定義することで、その型に特化した実装を提供できる。
  • 呼び出す側は型引数なしで呼べるので、ジェネリック版よりも最適化された実装があれば、自動的にそちらが使われるようになる。
  • ジェネリッククラスにするかジェネリックメソッドにするか
    • 型パラメータの値をクラス内部状態として保持するかどうか
    • ジェネリックインターフェースを実装する型かどうか

26. ジェネリックインターフェースとともに古いインターフェースを実装すること

  • クラス及びインターフェース, publicプロパティ, シリアル化の対象とした要素 が該当する。
  • IComparable<T> を実装するなら IComparable も実装せよという話。
  • 古いAPIとの互換性や、異なる型の間で順序や同値関係を構築したい場合には非ジェネリック版を扱う必要がある。

正直同意できない。 書いたところでろくに動作確認してないコードになってしまいそう。 本当に必要になってから実装すればええんじゃ。。

27. 最小限に制限されたインターフェースを拡張メソッドにより機能拡張する

  • 必要最低限のメソッドだけをインターフェースに定義し、拡張メソッドで補助的なメソッドを増やす。
  • インターフェースのデフォルト機能をメソッドとして提供できるようになる。

驚き。拡張メソッドはメソッドを外から追加しているようで気持ち悪いので使いたくない派だった。 確かに最近のライブラリはこの実装方法をよく見かける。勉強になった。

マーカーの例は誤植か、最後は new MyType2(); では?

28. 構築された型に対する拡張メソッドを検討すること

  • 具体的な型引数を指定したジェネリッククラスに対して、拡張メソッドを提供する。
  • アプリケーションの中に出てくるストレージモデル(構築された型で作られる)やインターフェースに対して拡張メソッドして機能を実装するのが最善策。
  • ジェネリック型のインスタンスを生成するだけで必要な機能がすべて揃った状態にできる。ストレージとモデルも分離できる。

なるほど。拡張メソッド使っていこう。