アピリッツの知識共有サイト「ナレッジベース」で公開されている内容をアピスピでも紹介します。こちらはES部のエンジニア・桑添敏生さんによる記事です。
これは、C#の話でゴリゴリにエンジニア専用の記事です。
突然ですが問題です。
(コンパイルが通る様に元から少し改修しましたが解答内容は同じです。)
public class TestCase1 : MonoBehaviour
{
private void Start()
{
Test1();
}
private void Test1()
{
List<int> val1 = new List<int>(){0};
List<int> val2 = new List<int>(){1};
SetValue(val1, val2);
Debug.Log($"合計はいくつになるでしょうか? {val1[0] + val2[0]}");
}
private void SetValue(List<int> val1, List<int> val2)
{
val1[0] = 3;
val2 = new List<int>(){2};
val2[0] = 5;
}
}
Debug.Log($"合計はいくつになるでしょうか? {val1[0] + val2[0]}");
この部分の答えは何になるかわかるでしょうか?
解答
正解は、「4」です。(ちなみに自分は「8」と答えました。)
解説
今回、「SetValue」関数の引数にListを入れていますが参照型の値渡しをしているので、val1[0] = 3;
を代入すると値が変わります。
その後の処理は、val2 = new List(){2};
と、 関数内で引数で値渡したもの(コピー)に対して改めて生成をした後に以下の代入を行なっても、val2[0] = 5;
コピーのアドレスを変えて値を入れただけなので大元の値は変わらず、
val1[0] = 3;
val2[0] = 1;
なので合計は「4」になります。
問題自体は終わりでここからは余談です。
発展した話 : どうすれば「8」になるのか
なぜ自分が「8」と答えたか、ここの辺りはふわふわした知識でしたが、
追加で何をすれば8になったのかを考えるのが大事だと思ったので今回まとめることにしました。
C#では値型と参照型の区別が大事ですが、
これに加えて値渡しと参照渡しがあります。めちゃくちゃ紛らわしい。
(値型と参照型自体もプリミティブとかオブジェクトなんて別の言い回しがあるのでさらに訳わかんなくなります)
自分は参照型で渡しているから生成を行うと実体の方も変わると勘違いしましたが、
C#では基本的に値渡しとなるので、実際は参照渡しをしないとダメです。
なのでタイトルのどうすれば「8」になるか、の答えを言ってしまうと、
自分が勘違いした通りに実体を渡してあげればいいので「参照型の参照渡し」をすれば良いだけです。
もっというと「参照渡し」という単語を聞いて想像できるパラメータ修飾子があると思いますがそのままで、
「ref」をつけてあげればいいんです。
SetValue(val1, ref val2);
private void SetValue(List<int> val1, ref List<int> val2)
{
val1[0] = 3;
val2 = new List<int>(){2};
val2[0] = 5;
}
これで試してみた所、「8」と出力されました。
なので今回は問題として出題されましたが、
関数内で参照型の実体に変更を加える実装を行う場合は、
「参照型の参照渡し」、紛らわしいので言い方を変えると実体(インスタンス)を渡すことで代入ができるようになりました。
また振る舞いとしては「out」パラメータ修飾子も同じ参照渡しで、
かつoutパラメータ修飾子を使用している場合は値が入っている事が保証されるのでこちらの方が安全そうです。
表にするとこういう感じです。
プロパティ書き換え | インスタンス置き換え | ||
値型 | 値渡し | ✕ | ✕ |
値型 | 参照渡し | ○ | ○ |
参照型 | 値渡し | ○ | ✕ |
参照型 | 参照渡し | ○ | ○ |
図を元にインスタンスの置き換え部分の挙動に注目して見ると問題の箇所は、参照型 値渡し : インスタンス ×
となっていたので想像した挙動とは異なった、という結果でした。
まとめ
1つ渡し方に気をつけて、
値渡しはコピー、
参照渡しは変数へのアクセス、
を渡している解釈でいれば問題なさそうです。