Qiita Advent Calendar 2回目の参加です。よろしくお願いします。
C#を習得するときかなり大コケをした「値渡しと参照渡し」について振り返り、まとめました。
値型の値と参照・参照型の値と参照
以下、全ての画像はイメージです。アドレスやメモリの配置などはそれっぽいものとして受け取ってください。
<値型> 値=「1」参照=「0x2742」
値型の値は「1 や 10 などの実体(インスタンス)」です。
<参照型> 値=「0x2904」参照=「0x2742」
参照型の値は「1 や 10 などの実体(インスタンス)」ではなく参照先にある実体の「アドレス」を持っています。
この他にポインターという型が存在しますが、今回は扱いません。▶ アンセーフ コード、ポインター型、関数ポインター
値渡しと参照渡しについて
「値渡し」「参照渡し」渡し方は2種類あります。ですが、C#には型の種類が2つあるため組み合わせとしては2×2=4パターンとなります。値型を渡すことが値渡し、参照型を渡すことが参照渡し、と呼ばない点が大きなポイントです。C#では参照型と参照渡しの概念は別物です。
- 値型の値渡し
- 参照型の値渡し
- 値型の参照渡し
- 参照型の参照渡し
値渡しと参照渡しの違い
名前の通り、値を渡すのが「値渡し」、参照を渡すのが「参照渡し」です。先程の画像を例に挙げます。
値型 paramValue
は値渡しだと「10」参照渡しだと「0x2742」を渡します。
参照型 paramRef
は値渡しだと「0x2904」参照渡しだと「0x2742」を渡します。
参照渡しで出来ること
- 参照渡しで引数を渡す
- 参照戻り値
- ref ローカル変数
- ref readonly
- ref 条件式 などなど…
出来ることは色々ありますが(説明できるかどうか自信がないので)今回は 参照渡しで引数を渡す をピックアップします。
参照渡しのキーワード
C#で参照渡しを使用する際は、以下のキーワードを使用します。
1. ref
private void ReferenceTypes_PassByReference_ref(ref int[] param)
{
param = new int[] { 1000, 2000, 3000 };
}
public void Call()
{
// 変数の初期化をしないとエラー
var paramRef2 = new int[] { 1, 2, 3 };
ReferenceTypes_PassByReference_ref(ref paramRef2);
}
- 呼び出し時に
ref
キーワードが必須 - メソッド内で書き換え可能(必須ではない)
- 変数の初期化必須
2. in
private void ReferenceTypes_PassByReference_in(in int[] param)
{
// メソッド内で代入をするとエラーになる 書き換え不可
// param = new int[] { 1000, 2000, 3000 };
}
public void Call()
{
// 変数の初期化をしないとエラー
var paramRef = new int[] { 1, 2, 3 };
ReferenceTypes_PassByReference_in(paramRef);
}
3. out
private void ReferenceTypes_PassByReference_out(out int[] param)
{
// メソッド内で代入をしなければエラーになる 書き換え必須
param = new int[] { 1000, 2000, 3000 };
}
public void Call()
{
// 変数の初期化をしなくてもエラーが出ない
int[] paramRef;
ReferenceTypes_PassByReference_out(out paramRef);
}
値渡し/参照渡しで引数を渡す
この記事内で「書き換え」という言葉を使用しますが、これは参照自体の値を変更することを指します。
値型でいえば param = 10;
参照型でいえば param = new int[] { 1000, 2000, 3000 };
などのコードが該当します。
param[1] = 10;
や param.Name = "AAA";
など、参照先のオブジェクトに属するデータを変更するコード(配列の要素を変更、クラスのメンバーを変更)は該当しません。要素やメンバーを変更すると、参照渡しでなくとも変更は反映されるからです。
参考: 参照型パラメーターの引き渡し (C# プログラミング ガイド) より
参照型の変数には、そのデータが直接入っているのではなく、そのデータへの参照が含まれています。値(渡し)で参照型パラメーターを渡す場合、クラスメンバの値など参照先のオブジェクトに属するデータを変更することができます。ただし、参照自体の値を変更することはできません。参照自体の値を変更する場合は ref または out キーワードを使用してパラメーターを渡します。
前述の通り ref
in
out
どのキーワードでも「参照渡し」はできますが、「参照渡し+書き換える」ことが可能なのは ref
と out
です。
以下の表は値渡しで書き換えた場合と、参照渡し(ref
または out
)で書き換えた場合の比較表です。
「値渡し」で書き換え | 「参照渡し(ref/out)」で書き換え | |
---|---|---|
値型 | 呼び出し元の引数(実引数)に変更が反映されない | 呼び出し元の引数(実引数)に変更が反映される |
参照型 | 呼び出し元の引数(実引数)に変更が反映されない | 呼び出し元の引数(実引数)に変更が反映される |
引数を渡して書き換える① 値型の値渡し・値型の参照渡し
サンプルコードでは ref
を使用します。
private void ValueTypes_PassByValue(int param)
{
param = 1000;
}
private void ValueTypes_PassByReference_ref(ref int param)
{
param = 1000;
}
public void Call()
{
// 値型の値渡し
var paramValue = 1;
Console.WriteLine($"呼び出し前 {paramValue}"); // 呼び出し前 1
ValueTypes_PassByValue(paramValue);
Console.WriteLine($"呼び出し後 {paramValue}"); // 呼び出し後 1
// 値型の値渡し
var paramValue2 = 1;
Console.WriteLine($"呼び出し前 {paramValue2}"); // 呼び出し前 1
ValueTypes_PassByReference_ref(ref paramValue2);
Console.WriteLine($"呼び出し後 {paramValue2}"); // 呼び出し後 1000
}
参照渡しは仮引数の param
に実引数である paramValue2
の参照が渡されるため、その先にある実体に書き換えの結果が反映されます。=呼び出し元に書き換え結果が反映されます。
引数を渡して書き換える② 参照型の値渡し・参照型の参照渡し
サンプルコードでは ref
を使用します。
private void ReferenceTypes_PassByValue(int[] param)
{
param = new int[] { 1000, 2000, 3000 };
}
private void ReferenceTypes_PassByReference_ref(ref int[] param)
{
param = new int[] { 1000, 2000, 3000 };
}
public void Call()
{
// 参照型の値渡し
var paramRef = new int[] { 1, 2, 3 };
Console.WriteLine($"呼び出し前 {string.Join(", ", paramRef)}"); // 呼び出し前 1, 2, 3
ReferenceTypes_PassByValue(paramRef);
Console.WriteLine($"呼び出し後 {string.Join(", ", paramRef)}"); // 呼び出し後 1, 2, 3
// 参照型の参照渡し
var paramRef2 = new int[] { 1, 2, 3 };
Console.WriteLine($"呼び出し前 {string.Join(", ", paramRef2)}"); // 呼び出し前 1, 2, 3
ReferenceTypes_PassByReference_ref(ref paramRef2);
Console.WriteLine($"呼び出し後 {string.Join(", ", paramRef2)}"); // 呼び出し後 1000, 2000, 3000
}
参照型の参照渡しも同じく仮引数の param
に実引数である paramRef2
の参照が渡されるため、その先にある実体を書き換えていることになります。そのため、呼び出し元に書き換え結果が反映されます。参照型の参照渡しは実体がヒープに存在するため複雑に見えますが、値型の参照渡しと動作は同じです。
おまけ 参照型で値渡しをしたはずなのに、引数が変更されてしまった?
それは、参照型であるクラスのメンバーを変更しているからなのでは……?
値渡しも参照渡しもバッチリ🎶と思い込んでいたとき、値渡しでクラスのメンバーを変更するコードを書いたのですが「どうして引数に変更が反映されているんだろう??」と混乱しました。なぜか参照型の特徴を無視して「値渡しなら呼び出し元の引数は絶対に変わらない」と思い込んでいたようです。
参照型パラメーターの引き渡し (C# プログラミング ガイド) に「参照型の変数には、そのデータが直接入っているのではなく、そのデータへの参照が含まれています。値(渡し)で参照型パラメーターを渡す場合、クラスメンバの値など参照先のオブジェクトに属するデータを変更することができます」と、書いてあります。その通りです。
class SampleClass
{
public string Name { get; set; } = "STAR";
public string[] Names { get; set; } = new string[] { "A", "B", "C" };
}
class MemberChangeClass
{
public void Call()
{
var sample = new SampleClass();
Console.WriteLine($"呼出前 {sample.Name} {sample.Names[1]}"); // 呼出前 STAR B
Sample(sample);
Console.WriteLine($"呼出後 {sample.Name} {sample.Names[1]}"); // 呼出後 MOON BBB
}
private void MemberChange(Sample sample)
{
// 値渡しでもメンバーの書き換えは反映される
sample.Name = "MOON";
sample.Names[1] = "BBB";
}
}
まとめ
-
渡し方
C#では値型も参照型も明示的に指定しなければ値渡し。逆に言えば、値型も参照型も明示的に指定すれば参照渡し。 -
参照渡しで出来ること
引数を渡す(メソッド内で書き換え可能なのはref
out
)、戻り値を返す、ローカル引数を設定する、など。 -
参照渡しのキーワード
ref
in
out
というキーワードがある。どれも参照渡しが可能だがそれぞれ制約がある。 -
メソッドに引数を渡して書き換えたときの違い
値渡しをすると呼び出し元の引数(実引数)に変更が反映されない。参照渡しをすると呼び出し元の引数(実引数)に変更が反映される。メソッドの仮引数に「値」が渡されているのか、それとも「参照」が渡されているのか、という違いがあるため。
記事内で使用したサンプルコードはこちら▶ CsharpRefSample
参考URL
- 値の受け渡し ++C++; // 未確認飛行 C
- 参照渡しの引数 ++C++; // 未確認飛行 C
- 参照戻り値と参照ローカル変数 ++C++; // 未確認飛行 C
- 値型の参照渡し ++C++; // 未確認飛行 C
- C#でわかる値渡し、参照渡し
- C# out と ref
- ref (C# リファレンス)
- ref (C# リファレンス) - 参照渡しで引数を渡す
- ref (C# リファレンス) - 参照戻り値
- in パラメーター修飾子 (C# リファレンス)
- out パラメーター修飾子 (C# リファレンス)
最後まで見ていただきありがとうございました。