はじめに
遅延実行と参照のコピーの辺りでしっかりと把握せずに使用していた部分があったので一度言語化してみようかなと思います。
値型と参照型のような難しさはあったが、一度理解すると新しいインスタンスが代入された時にどうなるかなど、深い理解を得られました。
遅延実行
コード
var list = new List<int> { 1, 2, 3 };
var query = list.Where(x => x > 1);
list.Add(4);
Console.WriteLine(query.Count());
問
出力結果はどうなるか?
回答
3が出力される
LINQが実行されるのは値を取得するとき
上記のコードを実行した結果は、query変数に代入しているタイミングだと考えていましたが、実際はConsole.WriteLineの部分で、Countメソッドを実行しているタイミングでした。
参照関係
コード
var list2 = new List<int> { 1, 2, 3 };
var query2 = list2.Where(x => x > 1);
list2 = new List<int> { 10, 20, 30, 40 };
Console.WriteLine(query2.Count());
問
出力結果はどうなるか?
回答
2が出力される
queryに保存される参照を具体的に理解出来ていなかった
このコードの出力予想も間違え、当初は4が出力されると考えていました。
理由としては、list2に格納されるインスタンスへの参照がnewされたものに切り替わるだけなので、queryを実行したときには、新しいリストの中身で計算されると考えたためです。
しかし、Whereメソッドの中身を追っていくとこれが間違いであることが分かります。
Whereメソッド->ListWhereIteratorクラスのコンストラクタまでは以下のように繋がっています。
(var queryの部分では、IEnumerableが返ってきているため、このListWhereIteratorが返っています。)
public static IEnumerable<TSource> Where<TSource>(
this IEnumerable<TSource> source, Func<TSource, bool> predicate)
{
// nullチェック(省略)
if (source is List<TSource> list)
{
return new ListWhereIterator<TSource>(list, predicate);
}
return new IEnumerableWhereIterator<TSource>(source, predicate);
}
private sealed partial class ListWhereIterator<TSource> : Iterator<TSource>
{
private readonly List<TSource> _source;
private readonly Func<TSource, bool> _predicate;
public ListWhereIterator(List<TSource> source, Func<TSource, bool> predicate)
{
_source = source; // 参照をフィールドに保存するだけ
_predicate = predicate;
}
}
コードからわかるように、最終的に_sourceに元のlist変数がさしているインスタンスへの参照が格納されます。
この時点で、インスタンスへの参照が別途ListWhereIteratorのプライベート変数に格納されるため、保存されるのは参照の経路ではなく、実データへの参照だと再認識しました。
そのため、元のlist変数の保持しているインスタンスの参照が変わろうとquery変数に格納されているIEnumerableが保持しているListの参照は変わりません。
メモリの状態について、以下図解で補足です。
Whereを呼んだ直後
list ──→ A [1,2,3]
query ──→ Q
Q._source ──→ A
list.Add(4) の後
list ──→ A [1,2,3,4]
Q._source ──→ A (同じAなので見える)
list = new ... の後
list ──→ B [10,20,30,40]
Q._source ──→ A [1,2,3] (矢印はAのまま)