Effective C# 3rd 読書メモ 41 高価なリソースのキャプチャを避ける

クロージャとキャプチャされた変数は、変数のスコープの基本原則の例外である。クロージャは境界づけられた変数を格納したオブジェクトを作り出す。これらの変数の生存期間は驚くほど長いことがある。

この挙動は、変数がメモリを消費しているだけである場合にはさほど気にする必要はないが、IDisposableを実装するような高価なリソース(ファイル、DB接続等)である場合には注意が必要である。

Effective C# 3rd 読書メモ 40 即時実行と遅延実行を区別する

宣言的コードは何をすべき(what)かを定義する。命令的コードはやり方(how)を説明する。どちらも必要だが、両者をごちゃ混ぜにすると予期せぬ振る舞いが発生する。

命令的コードは即時実行される。以下ではMethod1〜3が順番に即時実行される。

var answer = DoStuff(Method1(), Method2(), Method3());

これに対して、宣言的コードは、遅延実行されることがある。

var answer = DoStuff(() => Method1(),
    () => Method2(),
    () => Method3());

上記コードでMethod1〜3のそれぞれが実行されるか、またそれらの実行順序がどうなるかは、DoStuff()の実装次第である。

データをパラメーターとして使うか、関数をパラメーターとして使うかによって、振る舞いは大きく異なる。入力されるデータが小さい場合はデータを渡すほうが良い場合が多い。一方、入出力のサイズが非常に大きく、かつ、全体をメモリに置く必要がない場合は、関数を使ったほうがリソースの節約になる可能性がある。

Effective C# 3rd 読書メモ 39 LINQ: FunctionとActionで例外が飛ばないようにする

一連の値に対して処理を行うコードを作成する際に、例外が投げられると、状態の復元に問題が発生する。

以下のようなEmployeeクラスがあるとする。ここでは、MonthlySalaryプロパティのsetterに事前条件のチェックが入っており、例外を投げている。

class Employee
{
    public string Classification { get; set; }

    private int _monthlySalary;

    public int MonthlySalary
    {
        get => _monthlySalary;
        set
        {
            if (Classification != EmployeeType.Active)
                throw new Exception();
            _monthlySalary = value;
        }
    }
}

このクラスに対して、以下のような処理でMonthlySalaryの変更を行おうとすると、例外が発生する可能性がある。また、例外が発生して、途中まで処理が進んでいる場合、状態を復元することは困難である。

var allEmployees = FindAllEmployees();
allEmployees.ForEach(e => e.MonthlySalary *= 1.05M);

この問題に対する最も簡単な解決策は、例外が飛ばないようにすることだ。

allEmployees.FindAll(e => e.Classification == EmployeeType.Active)
            .ForEach(e => e.MonthlySalary *= 1.05M);

しかし、例外が絶対に飛ばないようにできない場合もある。この場合は、処理は一連の要素のコピーに対して行い、全ての処理が成功したらオリジナルと置き換える、という方法がある。

var updates = (from e in allEmployees
               select new Employee
               {
                   Classification = e.Classification,
                   MonthlySalary = e.MonthlySalary * 1.05M,
               }).ToList();
allEmployees = updates;

この方法は、前者よりも実装に手間がかかり、パフォーマンスも劣る。可能であれば前者を使用し、必要な場合にのみ後者を使用すべきである。

Effective C# 3rd 読書メモ 38 LINQではメソッドではなくラムダ式を使う

以下のように、似通った部分のあるコードを見かけると、共通化したくなるのはプログラマーの性だ。

public static void Main()
{
    var allEmployees = FindAllEmployees();

    var earlyFolks = from e in allEmployees
        where e.Classification == EmployeeType.Salaly
        where e.YearsOfService > 20
        where e.MonthlySalary < 4000
        select e;

    var newest = from e in allEmployees
        where e.Classification == EmployeeType.Salaly
        where e.YearsOfService < 2
        where e.MonthlySalary < 4000
        select e;
}

しかし、以下のような判別ロジックの共通化はうまくいかない。

private static bool LowPaidSalaried(Employee e) =>
    e.MonthlySalary < 4000 && e.Classification == EmployeeType.Salaly;

public static void Main()
{
    var allEmployees = FindAllEmployees();

    var earlyFolks = from e in allEmployees
        where LowPaidSalaried(e) && e.YearsOfService > 20
        select e;

    var newest = from e in allEmployees
        where LowPaidSalaried(e) && e.YearsOfService < 2
        select e;
}

LINQ to Objectsは、ラムダ式をデリゲートに変換し、コードを実行する。
一方、LINQ to SQLは、ラムダ式から式ツリーを作成し、式を構文解析して、それを別の環境(SQL Server等)で実行する。LINQの式の中にメソッド呼び出しが含まれると例外が発生する。

LINQの式を再利用可能にしたい場合、閉じた型のジェネリックに対する拡張メソッドとして実装するのが最も能率的である。

private static IQueryable<Employee> LowPaidSalariedFilter
    (this IQueryable<Employee> sequence) =>
    from s in sequence
    where s.Classification == EmployeeType.Salaly &&
          s.MonthlySalary < 4000
    select s;

public static void Main()
{
    var allEmployees = FindAllEmployees();
    var salaried = allEmployees.LowPaidSalariedFilter();
    var earlyFolks = salaried.Where(e => e.YearsOfService > 20);
    var newest = salaried.Where(e => e.YearsOfService < 2);
}

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

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

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

public static void Main()
{
    WriteLine($"Start time for Test One: {DateTime.Now:T}");
    var sequence = Generate(10, () => DateTime.Now);

    WriteLine("Waiting...");
    Thread.Sleep(millisecondsTimeout: 1000);

    WriteLine("Iterating...");
    foreach (var value in sequence)
        WriteLine($"{value:T}");

    WriteLine("Waiting...");
    Thread.Sleep(millisecondsTimeout: 1000);

    WriteLine("Iterating...");
    foreach (var value in sequence)
        WriteLine($"{value:T}");
}

private static IEnumerable<TResult>
    Generate<TResult>(int number, Func<TResult> generator)
{
    for (var i = 0; i < number; i++)
        yield return generator();
}

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

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

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

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