はじめに
ref・out・inは参照渡しをするためのパラメータ修飾子です。
outとinはインターフェイスおよびデリゲートのジェネリック修飾子としての役割もありますが、今回はパラメータ修飾子についてです。
それぞれの違いを知らなかったので調べてみました。
主な違いをまとめると以下のとおりです。
修飾子 | メソッドに渡す前の初期化 | メソッド内での値の書き換え | 用途 |
---|---|---|---|
ref | 必須 | 可 | 読み書き両方(入力・出力) |
out | 可 | 必須 | 値の書き換え(出力) |
in | 必須 | 不可 | 値の読み取り(入力) |
値渡しと参照渡し
メソッドに引数を渡す方法は2種類あります。値渡しと参照渡しです。
値渡しは、実態のコピーを渡します。コピーなので変更を加えても実態には影響しません。C#はデフォルトで値渡しになります。
参照渡しは、実態の場所を渡します。実態の場所を参照するので、実態が変更されます。ref・out・inを付けると参照渡しになります。
以下の例は、値渡しのパラメタに5を足し入れるAddFiveByVal
と、参照渡しのパラメタに5を足し入れるAddFiveByRef
を実行するコードです。
値渡しでは実態は書き変わらないので10が出力されます。参照渡しでは実態が書き変わるので15が出力されます。
using System;
{
// 値渡しなので書き変わらない
// 結果:10
static void AddFiveByVal(int x) => x += 5;
int val = 10;
AddFiveByVal(val);
Console.WriteLine(val);
}
{
// 参照渡しなので書き変わる
// 結果:15
static void AddFiveByRef(ref int x) => x += 5;
int val = 10;
AddFiveByRef(ref val);
Console.WriteLine(val);
}
参照渡しをするメリットは2つあります。
1つ目は、実態をコピーしないので新しくメモリを確保しないことです。
2つ目は、返り値を使わずに値を書き換えられることです。
以下の例はstring型の数値をint32型に変換するInt32.TryParse
を使う様子を表しています。この関数の第2引数で参照渡しがされています。返り値は変換に成功したかを表すbool値ですが、変換した結果は参照渡しをしたnumberに格納されています。
このように、返り値を使わずに変数の値を書き換えられることが参照渡しの利点です。
// 結果:Converted '12' to 12.
string value = "12";
bool success = int.TryParse(value, out int number);
if (success)
{
System.Console.WriteLine($"Converted '{value}' to {number}.");
}
参照型の値渡しに注意
参照型の値渡しは、参照をコピーするので実態を書き換えることができます。
C#には値型と参照型が存在し、それぞれに値渡しと参照渡しがあります。
詳しくは、岩永様の記事がとても分かりやすいです。
refは初期化が必須・メソッド内で書き換え可
refは以下の特徴があります。
- メソッドに渡す前に初期化を忘れるとコンパイルエラーになる
- メソッド内で代入をしなくてもコンパイルエラーにならない
そのため以下の利点があります。
- 実装側は安心して値を読むことができる
- 実装側で値を書き換えることができる
よって、refは値を読み取ることと、値を書き換えることの両方の目的に使うことができます。
以下はrefを使って参照渡しをするサンプルです。MultValueは掛け算をする関数です。参照渡しのパラメタに結果を代入します。実行結果は56となります。
// 実行結果:56
static void MultValue(int x, int y, ref int z) {
z = x * y;
}
int val = 0; // 初期化しないとエラー
MultValue(7, 8, ref val);
System.Console.WriteLine(val);
変数を初期化をせずに実行するとCS0165コンパイルエラーとなります。
CS0165
Use of unassigned local variable 'name'
未割り当てのローカル変数 'val' が使用されました
outは初期化が必要ない・メソッド内で書き換え必須
outは以下の特徴があります。
- メソッドに渡す前に初期化をしなくてもコンパイルエラーにならない
- メソッド内で代入を忘れるとコンパイルエラーになる
そのため、以下の利点があります。
- 実装する側は書き込みを忘れずに済む
- 呼び出し側は初期化していない変数を渡すことができる
- 呼び出し後は値が入っていることが保証される
よって、outは値を書き換える目的(出力)に使うことができます。
下記はoutを使って参照渡しをするサンプルです。実行結果は49になります。
// 実行結果:49
static void MultValue(int x, int y, out int z)
{
// System.Console.WriteLine(z); // 初期化前に読み取ろうとするとエラー
z = x * y; // 関数内で初期化しないとエラー
}
MultValue(7, 7, out int val);
System.Console.WriteLine(val);
初期化前に読み取ろうとするとCS0269コンパイルエラーとなります。
CS0269
Use of unassigned out parameter 'z'
未割り当ての out パラメーター 'z' が使用されました
関数内で初期化をせずに実行するとCS0177コンパイルエラーとなります。
CS0177
The out parameter 'z' must be assigned to before control leaves the current method
パラメーター 'z' はコントロールが現在のメソッドを抜ける前に割り当てられる必要があります
inは初期化が必須・メソッド内で書き換え不可
inは以下の特徴があります。
- メソッドに渡す前に初期化を忘れるとコンパイルエラーになる
- メソッド内で書き換えようとするとコンパイルエラーになる
そのため以下の利点があります。
- 呼び出し側は初期化を忘れずに済む
- 実装側は安心して値を読むことができる
- 実装する側は誤って書き込むことを防止できる
よって、inは値を読み取る目的(入力)に使うことができます。
下記のサンプルを実行すると36と表示されます。ShowValueは変数を表示するだけの関数です。変数を渡す側でin修飾子をつける必要はありません。
static void ShowValue(in int x) {
// x = 10; // 書き換えようとするとエラー
System.Console.WriteLine(x);
};
int val = 36; // 初期化しないとエラー
ShowValue(val);
関数内で書き換えようとするとCS8331コンパイルエラーとなります。
CS8331
Cannot assign to variable 'in int' because it is a readonly variable
読み取り専用の変数であるため、変数 'in int' に割り当てることができません
関数に渡す前に初期化をしないとCS0165コンパイルエラーとなります。
CS0165
Use of unassigned local variable 'val'
未割り当てのローカル変数 'val' が使用されました
メソッド呼び出し時に引数に修飾子を付ける理由
refとoutはメソッド呼び出し時に引数に修飾子を付ける必要があります。
その理由は、プログラマが参照渡しであることを知らずにメソッドを呼び出してしまうと、意図せず値が書き変わってしまうかもしれないからです。
呼び出しの際に明示的にref・out修飾子を付けるという制約を持たせることで、知らないうちに参照渡しをしているということを防いでいます。
inは書き変わることがないので、付ける必要はありません。
MultValue(7, 8, ref val);
MultValue(7, 8, out int val);
ShowValue(val); // inは付けなくてよい
ShowValue(in val); // 付けてもよい
所感
参照渡しをするためのパラメータ修飾子ref・out・inの違いについてまとめました。それぞれに入力や出力といった役割があることが分かりました。正しく使い分けられるようになりたいと感じました。
参考
in/out/refパラメーター修飾子の違いとは?[C#]:.NET TIPS - @IT
C#のref・out・inについてわかりやすく解説!ref・out・inの違いと制約 | C#のref・out・inについてわかりやすく解説!ref・out・inの違いと制約.NETコラム
参照渡し - C# によるプログラミング入門 | ++C++; // 未確認飛行 C