🗿

[C#] 参照型を値渡ししたときの不思議な動作を調べる

2021/09/04に公開

もくじ
https://qiita.com/tera1707/items/4fda73d86eded283ec4f

やりたいこと

下記のようなコードがあったときに、コード中のEditList()メソッドに渡したlistが、

  • EditList()の中でAddした値は、呼び出し元のlistにも反映されている
  • が、EditList()の中でOrderByした結果は、呼び出し元のlistに反映されていない

ということがあった。
なんでそんなことになるのか?が全然わからなかったので調べた。その時のメモ。

その時のコード要点.cs
private void Button_Click(object sender, RoutedEventArgs e)
{
    var list = new List<int>() { 2, 5, 1, -2 };

    EditList(list, 3);

    list.ForEach(x => Debug.WriteLine(x));
}
private void EditList(List<int> list, int val)
{
    list.Add(val);

    list = list.OrderBy(x => x).ToList();
}

調べた結果

下記のページに、答えがすべてあった。
https://ufcpp.net/study/csharp/sp_ref.html

補助情報として、
https://ufcpp.net/study/csharp/oo_reference.html

こちらのページを自分なりに整理すると、

  • 型には、2つのタイプがある。
    • 値型
    • 参照型
  • 値の受け渡しには2つある。
    • 値渡し
    • 参照渡し
  • それらの組み合わせで、いろんな型の値をメソッドに受け渡す時には、下記の4パターンある
    1. 値型の値渡し
    2. 値型の参照渡し
    3. 参照型の値渡し
    4. 参照型の参照渡し

で、今回上のコードではまったのは、3.の 「参照型の値渡し」 によって、不思議な動きをしていたっぽい。

下のサンプルコードで実験してみた。

サンプルコード

参照型を値渡し

//********************************************************
//* 参照型 を 値渡し するパターン
//* (List<T>が参照型)
//********************************************************
private void Button_Click(object sender, RoutedEventArgs e)
{
    var list = new List<int>() { 2, 5, 1, -2 };

    // ①この時点で list は {2, 5, 1, -2 }

    EditList(list, 3);

    // ④この時点で list は {2, 5, 1, -2, 3 }
    // ※③で値渡しされた参照を書き換えても外のlistには反映されない
}

private void EditList(List<int> list, int val)
{
    list.Add(val);
    // ②この時点で list は {2, 5, 1, -2, 3 }
    // ここのlistと外のlistはこの時点では同じヒープ上のメモリを指している

    list = list.OrderBy(x => x).ToList();
    
    // ↑ここのlistと外のlistが別のメモリを指すようになった
    // ③この時点で list は {-2, 1, 2, 3, 5} でソートされてるが、
    // 値渡しされたlistの参照先は、呼び出し元のlist(参照型)には反映されない
}

参照型を参照渡し

//********************************************************
//* 参照型 を 参照渡し するパターン
//* (List<T>が参照型)
//********************************************************
private void Button_Click_1(object sender, RoutedEventArgs e)
{
    var list = new List<int>() { 2, 5, 1, -2 };

    // ①この時点で list は {2, 5, 1, -2 }

    EditListRef(ref list, 3);

    // ④この時点で list は {-2, 1, 2, 3, 5}
    // ※③で代入された参照が、外にも反映される
}

private void EditListRef(ref List<int> list, int val)
{
    list.Add(val);
    // ②この時点で list は {2, 5, 1, -2, 3 }
    // ここのlistと外のlistはこの時点では同じヒープ上のメモリを指している

    list = list.OrderBy(x => x).ToList();

    // ↑ここのlistと外のlistが別のメモリを指すようになったが、
    //  参照渡しされたlistに代入されたその参照は、外のlistにも反映される
    // ③この時点で list は {-2, 1, 2, 3, 5}
}

結果

(参考にさせて頂いたページと似た図になり申し訳ないですが、)
上のコードで試した結果、下記の理解をした。

参照型を値渡ししたときのイメージ

元のlistを指しているうちにAddされた値は反映される。
その後OrderByで新しい領域が確保され、そこへの参照がlistに入れられる。
が、listは値渡しされたものであるので、引数の渡し元へは反映されない。

参照型を参照渡ししたときのイメージ

値渡しのときと同様、OrderByで新しい領域が確保され、そこへの参照がlistに入れられる。
listは参照渡しされたものであるので、引数の渡し元へもそれが反映される。

なので、この理解で行くと、
一言で「呼び出し元のlistにOrderByの結果が反映された」といっても、
元々ヒープ上に確保されたlistの領域がOrderByで並び替えられたわけではなく、
元々ヒープ上に確保されたlistのデータを使ってOrderByしたリストを別のヒープ上に確保して、それを呼び出し元が見るようになった、ということになる。

考察

上記のような考え方で、自分としてはしっくり来てしまったが、本当に正しいのかどうか、少々自信がない。もし間違いなどお気づきの際は、コメント頂けると大変助かります。

参考

https://docs.microsoft.com/ja-jp/dotnet/csharp/language-reference/builtin-types/value-types

https://docs.microsoft.com/ja-jp/dotnet/csharp/language-reference/keywords/reference-types

Discussion