Effective C# 3rd 読書メモ 37 クエリの遅延評価を使う

LINQによって定義したクエリは、通常は遅延評価(lazy evaluation)され、データは実際に必要になるまで取得されない。しかし、すぐに取得する(eager evaluation)こともできる。

実際に遅延評価の動作を確認するには、以下のようなコードが使用できる。

注意が必要なのは、クエリの演算によっては、一連の要素の全体が必要になることもある、ということだ。OrderByMaxMinはいずれも遅延評価できない演算である。

一方、特定の時点の一連の要素のスナップショットが欲しい場合もある。その際は、ToList()ないしToArray()を実行して、結果を取得すればよい。

一連の要素を処理する際は、以下のような事項に気をつける。

  • データの取得タイミングを遅らせ、コピーを減らす(無駄にToList()ToArray()を使わない)
  • Whereによるフィルタリングを行う場合、全体を必要とする演算より前で実行する
  • 膨大な数の要素を一部のみを取得する際は、全体に対する演算が実行されないようにする

Effective C# 3rd 読書メモ 36 クエリ式がどのようにメソッド呼び出しにマップされるか理解する

LINQは2つの概念に基づいている: クエリ言語と、クエリ言語から一式のメソッドへの翻訳である。C#コンパイラはクエリ言語で書かれたクエリ式をメソッド呼び出しに変換する。

全てのクエリ式は、1つ以上のメソッド呼び出しにマップされる。このマッピングを理解することは、組み込みのクラスを利用する場合にも、組み込みのクラスを拡張する場合にも重要である。

クエリ式のパターンは11種類ある(詳細は割愛)。

.NETの基本ライブラリは以下の実装を提供している。

  • System.Linq.Enumerableによる、IEnumerable<T>の拡張メソッド(クエリ式の実装)
  • System.Linq.Queryableによる、IQueryable<T>の拡張メソッド(クエリを別形式に変換する)

クエリ式からメソッド呼び出しへの変換はコンパイラによって行われる。

from n in numbersはnumbersの値を1つずつ取り出してnという変数に紐付ける。where節はWhere()メソッドに変換されるフィルターを定義する。

select節はSelect()メソッドに変換されるが、一定条件のもと最適化によって消去されることがある。サンプルのselectは、範囲のある値(range)を、単に別のrangeに変換しているだけである。このようなselectをdegenerate select(退化select)と呼ぶ。degenerate selectは特別な処理を行っていないので、コンパイラはこのselectを消去する。

この結果、上記クエリ式は以下のメソッド呼び出しに変換される。

以下のように、selectが元の値とは異なる値を返す場合には、省略されない。

次に、orderby節を検討する。

ここで、クエリ式は以下のように変換される。

ThenBy()メソッドはOrderBy()ないしThenBy()の結果を受け、追加の並び替えを行う。

残りは、グルーピングと、複数のfrom句の使用による継続(continuations)の実現である。継続を含むクエリ式は、ネストされたクエリに変換され、ネストされたクエリがメソッド呼び出しに変換される。

上記コードは、継続によって以下のように変換される。

ネストされたクエリができたら、メソッド呼び出しに変換される。

最後はSelectMany()Join()、そしてGroupJoin()である。

上記コードでは、16個のペアが作成される。このクエリ式のように、複数のfrom句を含むクエリ式は、SelectMany()メソッドに変換される。

なお、SelectMany()の内部では以下のような処理が行われている。

joinJoin()に変換される。

into節を含むjoin式はGroupJoin()に変換される。

個人的な感想

複数のselectjoinが出てくる場合はクエリ式のほうが簡潔に書けるけど、それ以外の場合はメソッド呼び出しのほうが簡潔に書ける。クエリ式を使う機会はそんなに多くなさそうな気がする。まあ、自分が書かなくても、他の人が書いたものを読む機会はあるので、クエリ式=>メソッド呼び出しの変換規則は、知っておいて損はない。

Effective C# 3rd 読書メモ 35 拡張メソッドをオーバーロードしてはいけない

拡張メソッドは以下の場合にのみ利用すべきである。

  • インターフェイスにデフォルト実装を追加する
  • 閉じたジェネリック型に振る舞いを追加する
  • 合成可能なインターフェイスを作成する

拡張メソッドは、型に機能を付け足すが、型の振る舞いを変更するわけではない。

拡張メソッドは以下のような用途には使ってはならない。

  • クラスにデフォルト実装を追加する
  • 複数のクラスに拡張メソッドを作成し、名前空間によって切り換え可能にする

拡張メソッドの使いすぎや誤用は、メソッドの衝突を引き起こし、コードのメンテナンスコストを増大させる。

たとえば、以下のようなPersonクラスがあるとする。

このクラスはsealedなので、Personに機能を付け足すことはできない。Personの情報を、一定のフォーマットに従って出力したい場合、どのように実装すればいいだろうか?
以下のような拡張メソッドの作成は望ましくない方法である。

もし、レポート方法をXMLに切り換えたくなったら、XmlExtensions.XmlReport.Format()を作成し、using文によって使用する拡張メソッドを切り換えるという方法がある。しかし、この方法では、誤った名前空間のインポートによるバグの発生や、両方の拡張メソッドを同時に使用できない等の問題が発生する。

このような機能は、以下のように、独立したクラスの静的メソッドとして実装すべきである。

拡張メソッドは、特定の型に対して、単一かつグローバルなものとみなすべきである。名前空間によるオーバーロードは決して行ってはならない。同一のシグネチャで複数の拡張メソッドを作る必要が出てきた場合は、設計を見直し、静的メソッドに変更すべきである。

Effective C# 3rd 読書メモ 34 関数パラメータを使って結合度を下げる

関数パラメータを使うことで、コンポーネント間の結合度を下げることができる。この方法には、実装上のトレードオフもあるが、クラスの継承やインターフェイスの実装よりも自由度の高い拡張ポイントを設けたい場合には有用である。

関数パラメータを使って実際の処理を委譲するコードは以下のようになる。

利用側のコードは以下のようになる。

あなたのコードの挙動の一部を利用者が拡張できるようにしたい場合、第一の候補となるのはインターフェイスである。
抽象クラスの定義は、デフォルト実装の提供等の利点があるが、クラス階層を強制するという欠点もある。
関数パラメータは、最も柔軟だが、エラーチェック等の手間がかかる。

Effective C# 3rd 読書メモ 33 要求に応じて一連の要素を生成する

イテレーターメソッドは一連の要素の生成にも使用できる。その際、実際に必要になるまで要素の生成を遅延させることができる。

この方法を使うと、任意の箇所でコレクションの生成を止めることができる、という利点もある。