きなこもち.net

.NET Framework × UiPath,Orchestrator × Azure × AWS × Angularなどの忘備録

Effective C# 6.0/7.0 × 4章:LINQを扱う処理 × 自分なりの要点まとめ

目的

流し読みにならないように、1項目毎要約をする。
今回は、4章:LINQを扱う処理の項目に対してのまとめとなる。

項目29:コレクションを返すメソッドではなくイテレータを返すメソッドとすること

イテレータメソッド とは、 yield return を 使用 し て 必要 に 応じ て シーケンス の 要素 を 生成 する よう な メソッド の こと です。BillWagner. Effective C# 6.0/7.0 (p.121). 株式会社翔泳社. Kindle 版.

  • ある配列(List<string>など)を作成して処理を行う場面があった場合、作成した配列を返すことがある。
public static IEnumerable<char> GenerateAlphabetCache()
{
    var collection = new List<char>();
    var letter = 'a';
    while (letter <= 'z')
    {
        collection.Add(letter);
        letter++;
    }
    return collection;
}
  • イテレータを利用することで、配列を返すのではなく、配列の要素が呼び出されたときに1つ1つの要素を呼び出し元に返すようにすることができる。
public static IEnumerable<char> GenerateAlphabet()
{
    var letter = 'a';
    while (letter <= 'z')
    {
        yield return letter;
        letter++;
    }
}
  • これにより、呼び出し元で、生成した配列を丸ごとキャッシュして処理を進めるか、必要になった要素を取り出し、それ毎に処理を進めるかという選択をさせることができる。また、後者の場合、呼び出し元で配列を管理する手間を省くことができる。
  • 処理を進めるうえで、必要な要素だけを使って処理を進めることができる場合は、必要な分だけ要素を生成するということができる場面では、イテレータを利用したほうが良い。

項目30:ループよりもクエリ構文を使用すること

  • メモ:誤植?クエリよりもパフォーマンスの出るループを手書きすることもできるけど、常にそうなるとは限らない。といいたいのでは?

ループ よりも クエリ の 方 が パフォーマンス が 低い という 意見 や データ も あり ます。 クエリ よりも パフォーマンス の 出 ない ループ を 手書き する こと も 確か に でき ます が、 常に そう だ という わけ では あり ませ ん。BillWagner. Effective C# 6.0/7.0 (p.130). 株式会社翔泳社. Kindle 版.

  • 上記の通り、パフォーマンスに関する課題は完全に解決されていないのかもしれない。しかし、可読性の観点からすると、ForやWhileなどの命令形式のループを使った処理より、はるかに可読性の高いコードが実装できる。
  • クエリ構文にするか、メソッドチェーンにするか迷う場面も出てくる。メソッドチェーンだけでサポートされている拡張メソッドもあるため、メソッドチェーンに行きがちだが、クエリ構文のほうが可読性が高い場面もある。可読性の観点から、どちらを使うべきか判断するようにするのがよさそう。
  • 明瞭な違いがないシンプルなサンプル。ここに特定の条件だけ値を返す処理や、ソート順序を変えたりするなどの追加要件が発生したとたん、可読性の観点で明瞭な違いが出てくる。
public static IEnumerable<Tuple<int,int>> ProduceIndices()
{
    return from x in Enumerable.Range(0, 10)
           from y in Enumerable.Range(0, 10)
           select Tuple.Create(x, y);
}
public static IEnumerable<Tuple<int, int>> ProduceIndicesOld()
{
    foreach (var x in Enumerable.Range(0, 10))
    {
        foreach (var y in Enumerable.Range(0, 10))
        {
            yield return Tuple.Create(x,y);
        }
    }
}

項目31:シーケンス用の組み合わせ可能なAPIを作成する

  • ポイントは、イテレータメソッドの利点を理解すること。

単一 の 項目 を 処理 する よりも、 シーケンス を 処理 する プログラム の 方 が 多い はず で、 foreach や for、 while といった キーワード を 使用 し て 処理 する こと に なるでしょう。 つまり コレクション を 入力 として 受け取り、 項目 を 確認 または 変更 し、 別 の コレクション を 返す よう な メソッド を 作成 する こと に なる でしょ う。 BillWagner. Effective C# 6.0/7.0 (p.130). 株式会社翔泳社. Kindle 版. - なぜばれた( ^ω^)・・・

  • 一つの配列を受け取り、新しい配列を呼び出し元に返す。イテレータを使えば、一つ一つの要素毎に処理を行うことができる。そのため、従来の配列全部を処理するループで実装するより、効率的に処理を実行することができる。また、他のイテレータメソッドと組み合わせることで、可読性を損なうことなく複雑な処理を、効率的に実行できるようになる。

  • イテレータを効果的に利用するためにも、データの種類ごとにメソッドを定義する代わりに、任意の配列に対して処理を行えるようなメソッドを定義するという考え方を持つ。

項目37:クエリを即時評価ではなく遅延評価すること

  • クエリを実行したとき、つど新しい要素を取得する挙動を遅延評価(lazy evakuation)といい、結果を一括で取得する挙動を即時評価(eager evaluetion)という。
  • 遅延評価をすることで、一度定義したクエリに対して、追加のクエリを追加することができる。同じことを即時評価すると、一度定義したクエリからすべての結果を受け取ったのち、追加のクエリを実行することになるため、配列の保存に利用されるメモリやループ処理を実行する処理コストの面からパフォーマンスの低下を招く。
    • クエリ式で定義すれば、期待通りの遅延評価になるかというと、そうではない。その例は↓。以下のようにすべてのintの値を出力するメソッドがあった場合、CaseAでは、10回だけアイテムを取得するように動くのに対し、CaseBでは、配列をすべて生成し、すべての項目に対してwhere句で指定した条件を検査する。そのため、int.MaxValue-11回のループ処理が実行されることになる。
public static IEnumerable<int> AllNumbers()
{
    var number = 0;
    while (number <= int.MaxValue)
    {
        yield return number;
        number++;
    }
}

//呼び出し元。CaseA
 var test = Sample.AllNumbers();
 foreach (var item in test.Take(10))
{
    Console.WriteLine(item);
}

//呼び出し元。CaseB
var test2 = from item in Sample.AllNumbers()
            where item <= 10
            select item;
foreach (var item in test2)
{
    Console.WriteLine(item);
}

一方、 一部 の クエリ 式 では 答え を 返す 前 に シーケンス 全体 を 必要 と する 場合 が あり ます。 この ボトルネック が 発生 する タイミング を 把握 する こと によって、 パフォーマンス の 劣化 を 起こさ ない よう な クエリ を 作成 できる よう になり ます。BillWagner. Effective C# 6.0/7.0 (p.163). 株式会社翔泳社. Kindle 版.

  • 同じように、配列の要素すべてが必要になるケースとして、OrderBy、Max、Minがある。
  • まずは、遅延実行で処理が定義できないか考え、どうしても難しい場合は、即時評価(ToList, ToArray)を利用するという思考でいるのが推奨されている。

項目43:クエリに期待する意味をSingle()やFirst()を使用して表現すること

  • 特定の1つの要素を配列から取得しようとする場合、Signleメソッドを利用して取得するようにすると、そのクエリの実装に明確な意図を表現することができる。
  • 複数の要素を取得し、その中のどれでもいいから1つの要素を取得したいという考えで、Firstメソッドを利用すると、そのコードの明確な意図が表現しきれない。

所感

項目44、41あたりのクロージャが絡んでくるところが十分に理解できていない。これまで使ってこなかった機能なので、これを起点に深掘りしていきたい。