クロージャとキャプチャされた変数は、変数のスコープの基本原則の例外である。クロージャは境界づけられた変数を格納したオブジェクトを作り出す。これらの変数の生存期間は驚くほど長いことがある。
この挙動は、変数がメモリを消費しているだけである場合にはさほど気にする必要はないが、IDisposable
を実装するような高価なリソース(ファイル、DB接続等)である場合には注意が必要である。
クロージャとキャプチャされた変数は、変数のスコープの基本原則の例外である。クロージャは境界づけられた変数を格納したオブジェクトを作り出す。これらの変数の生存期間は驚くほど長いことがある。
この挙動は、変数がメモリを消費しているだけである場合にはさほど気にする必要はないが、IDisposable
を実装するような高価なリソース(ファイル、DB接続等)である場合には注意が必要である。
宣言的コードは何をすべき(what)かを定義する。命令的コードはやり方(how)を説明する。どちらも必要だが、両者をごちゃ混ぜにすると予期せぬ振る舞いが発生する。
命令的コードは即時実行される。以下ではMethod1〜3が順番に即時実行される。
var answer = DoStuff(Method1(), Method2(), Method3());
これに対して、宣言的コードは、遅延実行されることがある。
var answer = DoStuff(() => Method1(),
() => Method2(),
() => Method3());
上記コードでMethod1〜3のそれぞれが実行されるか、またそれらの実行順序がどうなるかは、DoStuff()の実装次第である。
データをパラメーターとして使うか、関数をパラメーターとして使うかによって、振る舞いは大きく異なる。入力されるデータが小さい場合はデータを渡すほうが良い場合が多い。一方、入出力のサイズが非常に大きく、かつ、全体をメモリに置く必要がない場合は、関数を使ったほうがリソースの節約になる可能性がある。
一連の値に対して処理を行うコードを作成する際に、例外が投げられると、状態の復元に問題が発生する。
以下のような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;
この方法は、前者よりも実装に手間がかかり、パフォーマンスも劣る。可能であれば前者を使用し、必要な場合にのみ後者を使用すべきである。
以下のように、似通った部分のあるコードを見かけると、共通化したくなるのはプログラマーの性だ。
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);
}
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();
}
注意が必要なのは、クエリの演算によっては、一連の要素の全体が必要になることもある、ということだ。OrderBy
、Max
、Min
はいずれも遅延評価できない演算である。
一方、特定の時点の一連の要素のスナップショットが欲しい場合もある。その際は、ToList()
ないしToArray()
を実行して、結果を取得すればよい。
一連の要素を処理する際は、以下のような事項に気をつける。
ToList()
やToArray()
を使わない)Where
によるフィルタリングを行う場合、全体を必要とする演算より前で実行する