in/out/refパラメーター修飾子を利用すると、パラメーターの受け渡しを効率的に行える。これらの修飾子の違いと使用する上での注意点をまとめる。
メソッドに引数を渡す方法に、値渡しと参照渡しがある。さらにC#では、メソッドの引数リストで参照渡しを宣言するために、in/out/refの3通りのパラメーター修飾子がある。inパラメーター修飾子はC# 7.2の新機能だ。本稿では、この参照渡しのin/out/refの使い方の違いを解説する。
特定のトピックをすぐに知りたいという方は以下のリンクを活用してほしい。
なお、本稿に掲載したサンプルコードをそのまま試すにはVisual Studio 2017(15.5以降)が必要である。サンプルコードはコンソールアプリの一部であり、コードの冒頭に以下の宣言が必要となる。
using static System.Console;
また、inパラメーター修飾子を使うには、本稿執筆時点ではVisual Studio 2017(15.5以降)で以下の操作が必要だ。
上の3.の操作は、C#のバージョンが異なるものの別記事「Dev Basics/Keyword:C# 7.1」に画像が掲載してあるので参考にしていただきたい。
どれも参照渡しである
メソッドの引数リストに記述するそれぞれの引数にin/out/refパラメーター修飾子のいずれかを付けられる(次のコード)。どの場合でも、引数は参照渡しでこのメソッドに渡される。いずれも付けなければ、引数は値渡しで渡される。
// 参照渡し
void SampleMethodIn(in int n){ ……省略…… }
void SampleMethodRef(ref int n){ ……省略…… }
void SampleMethodOut(out int n){ ……省略…… }
// 値渡し(in/out/refなし)
void SampleMethod(int n){ ……省略…… }
ちなみに、in/out/refパラメーター修飾子にはソースコード上では後述するような違いがあるが、コンパイル結果は同じになる(バイナリレベルでは参照渡しの方法は1つしかない)。in/out/refパラメーター修飾子の違いは、ソースコードを明瞭にするために設けられているのである。
どれも直ちに実行されないメソッドでは使えない
参照渡しは、次の2種類のメソッドでは禁止されている。
修飾子の違いではオーバーロードできない
in/out/refパラメーター修飾子はいずれもバイナリレベルでは同一の参照呼び出しであるため、in/out/refパラメーター修飾子の違いではオーバーロードできない。in/out/refパラメーター修飾子の有無(値渡しか参照渡しか)ならばオーバーロードできる(次のコード)。
// 参照渡し
void SampleMethod(in int n){ ……省略…… }
// 以下の2つはオーバーロードではない(上の行と同じシグネチャと見なされる)
// void SampleMethod(ref int n){ ……省略…… }
// void SampleMethod(out int n){ ……省略…… }
// 値渡し(in/out/refなし)
void SampleMethod(int n){ ……省略…… } // 参照渡しとはシグネチャが異なる
どれも参照渡しであるが、次のように用途によって使い分ける。
細かく見ると、メソッドの呼び出し方と、メソッド内部でその引数に対して行えることに、次の表のような差異がある。
修飾子 | 用途 | 呼び出す前の変数初期化 | 呼び出し時の修飾子付与 | メソッド内での参照先の割り当て/変更 | オプション引数 |
---|---|---|---|---|---|
in | 入力 | 必須 | 任意 | 不可 | 可 |
ref | 変更 | 必須 | 必須 | 可能 | 不可 |
out | 出力 | 不要 | 必須 | 必須 | 不可 |
in/out/refパラメーター修飾子の違い |
inパラメーター修飾子は、その引数をメソッドへの入力として利用するだけの場合に使用する。そのため、メソッドを呼び出す前に変数を初期化して値を設定しておくことが必須だ。メソッド内では、引数の参照先(呼び出し側の変数)の内容を変更したり、別のオブジェクトを割り当てたりすることはできない。呼び出し元からすれば渡した変数を変更される可能性は値渡しと同様であるため、呼び出すときにinキーワードの付与は任意となっている(inと書いても書かなくてもよい)。また、入力専用ということで、オプション引数(デフォルト引数)にもできる。
では、値渡しとinパラメーター修飾子を使った参照渡しの違いは何かというと、大きなサイズの値型を渡すときに高速化が見込めるということだ。小さな値型、例えばint型では、オブジェクトのサイズと参照のサイズは同じようなものなので(32bit CPUをターゲットにビルドした場合には、int型のデータも参照もそのサイズは4バイト)、そのコピーを作る速度も変わらない。大きな構造体の場合は、値渡しでは大きなオブジェクトのコピーを作ることになり、参照渡しでは(そのオブジェクトに比べて)小さい参照を作るだけなので、高速化が見込めるわけだ。
refパラメーター修飾子は、その引数をメソッドへの入力としても出力としても利用できる。入力として利用するため、メソッドを呼び出す前に変数を初期化して値を設定しておくことが必須だ。メソッド内では、引数の参照先(呼び出し側の変数)の内容を変更したり、別のオブジェクトを割り当てたりして、出力としても利用できる。呼び出し元からすれば渡した変数がどのように書き換えられてもよいと覚悟しなければならないので、呼び出すときにrefキーワードの付与が必須だ。
outパラメーター修飾子は、その引数をメソッドからの出力として利用するだけの場合に使用する。そのため、メソッドを呼び出す前に変数を初期化しておく必要はない。C# 7では、メソッドを呼び出す引数リストのかっこの中で変数の宣言さえも行える。メソッド内では、引数の参照先(呼び出し側の変数)にオブジェクトを割り当てることが必須だ。また、呼び出すときにoutキーワードの付与も必須である。
以上の違いについて、値型と参照型を渡す具体例をこれから見ていこう。
値型の例として次のような構造体を例としよう(次のコード)。
public struct SampleStruct
{
public double X { get; set; }
public double Y { get; set; }
}
この構造体を引数として受け取った側のメソッドで、そのプロパティを変更したり、インスタンスを割り当て直したりしたときに、どのような挙動になるか実際に確認していこう。
まず、値渡しの場合。受け取ったメソッドの側では、プロパティの値を変更したり、新しいインスタンスを割り当てたりできる(次のコード)。
static void SampleMethod(SampleStruct s)
{
s.X = 3.0;
s = new SampleStruct();
s.X = 4.0;
}
しかしこの変更や割り当ては、呼び出し元の変数には影響しない(次のコード)。値渡しは変数の中身(この場合はSampleStructオブジェクト)のコピーをメソッドに渡すからだ。
// Mainメソッド内
SampleStruct s = new SampleStruct { X = 1.0, Y = 2.0, };
SampleMethod(s);
WriteLine($"値型の値渡し:X={s.X}, Y={s.Y}");
// 出力:値型の値渡し:X=1, Y=2
次に、inパラメーター修飾子を使って参照渡しをする場合。受け取ったメソッドの側では、プロパティの値を変更したり、新しいインスタンスを割り当てたりできない(次のコード)。
static void SampleMethodIn(in SampleStruct s)
{
// 以下、全てコンパイルエラー
//s.X = 3.0;
//s = new SampleStruct();
//s.X = 4.0;
}
inパラメーター修飾子を使って値型を参照渡しする場合、メソッド内で変更できないのだから、当然ながら呼び出し元の変数には影響がない(次のコード)。
// Mainメソッド内
SampleStruct s = new SampleStruct { X = 1.0, Y = 2.0, };
SampleMethodIn(s);
SampleMethodIn(in s); // inは書いても書かなくてもよい
WriteLine($"値型の参照渡し(in):X={s.X}, Y={s.Y}");
// 出力:値型の参照渡し(in):X=1, Y=2
refパラメーター修飾子を使って参照渡しをする場合。受け取ったメソッドの側では、プロパティの値を変更したり、新しいインスタンスを割り当てたりできる(次のコード)。
static void SampleMethodRef(ref SampleStruct s)
{
s.X = 3.0;
s = new SampleStruct();
s.X = 4.0;
}
refパラメーター修飾子を使って値型を参照渡しした場合、メソッド内でその値が変更されたり、インスタンスが丸ごと入れ替えられたりする(次のコード)。
// Mainメソッド内
SampleStruct s = new SampleStruct { X = 1.0, Y = 2.0, };
SampleMethodRef(ref s); // refを書かないとコンパイルエラー
WriteLine($"値型の参照渡し(ref):X={s.X}, Y={s.Y}");
// 出力:値型の参照渡し(ref):X=4, Y=0
outパラメーター修飾子を使って参照渡しをする場合。受け取ったメソッドの側では、新しいインスタンスを割り当てねばならない(次のコード)。
static void SampleMethodOut(out SampleStruct s)
{
//s.X = 3.0; // この行はコンパイルエラー(未割り当てのパラメーターの使用)
s = new SampleStruct();
s.X = 4.0;
}
outパラメーター修飾子を使って値型を参照渡しする場合は、呼び出す前に変数を初期化しなくてもよい。C# 7では、呼び出し時の引数リスト内で変数宣言もできる(次のコード)。
// Mainメソッド内
SampleMethodOut(out SampleStruct s); // outを書かないとコンパイルエラー
WriteLine($"値型の参照渡し(out):X={s.X}, Y={s.Y}");
// 出力:値型の参照渡し(out):X=4, Y=0
クラスなどの参照型を参照渡しにする意味はあまりないのだが、その挙動を確認しておこう。どうしても使わねばならない理由がない限り、参照型の参照渡しは使わない方がよいだろう。配列やList<T>ジェネリックコレクションなども参照型なので、ここでは整数の配列を例にしよう。
まず、値渡しの場合。受け取ったメソッドの側では、配列の要素を変更したり、新しいインスタンスを割り当てたりできる(次のコード)。
static void SampleMethod(int[] a)
{
a[0] = 2;
a = new int[5];
a[0] = 3;
}
配列要素の変更は呼び出し元の変数に影響するが、新しい配列の割り当ては呼び出し元の変数には影響しない(次のコード)。値渡しは変数の中身(この場合は配列の実体への参照)のコピーをメソッドに渡すため、参照先(ここでは配列の実体)は呼び出し元とメソッドで同じであり、配列要素の変更は呼び出し元に影響する。メソッド内で新しい配列を割り当てるのは、参照のコピーに対してであるため、呼び出し元の参照には影響しない。
// Mainメソッド内
int[] a = { 1, 1, 1 };
SampleMethod(a);
WriteLine($"配列の値渡し:{string.Join(", ", a)}");
// 出力:配列の値渡し:2, 1, 1
次に、inパラメーター修飾子を使って参照渡しをする場合。受け取ったメソッドの側では、参照先に新しいインスタンスを割り当てることはできない。ただし、inパラメーター修飾子でよく誤解されることなのだが、参照先の参照先は変更できる。この場合では、配列の要素は書き換え可能なのである(次のコード)。
static void SampleMethodIn(in int[] a)
{
a[0] = 2;
// a = new int[5]; // この行はコンパイルエラー
a[0] = 3;
}
inパラメーター修飾子を使って参照型を参照渡しする場合、オブジェクトの内容は変更されることがある(次のコード)。
// Mainメソッド内
int[] a = { 1, 1, 1 };
SampleMethodIn(a);
SampleMethodIn(in a); // inは書いても書かなくてもよい
WriteLine($"配列の参照渡し(in):{string.Join(", ", a)}");
// 出力:配列の参照渡し(in):3, 1, 1
refパラメーター修飾子を使って参照渡しをする場合。受け取ったメソッドの側では、配列の要素を変更したり、新しいインスタンスを割り当てたりできる(次のコード)。
static void SampleMethodRef(ref int[] a)
{
a[0] = 2;
a = new int[5];
a[0] = 3;
}
refパラメーター修飾子を使って参照型を参照渡しした場合、メソッド内でその値が変更されたり、インスタンスが丸ごと入れ替えられたりする(次のコード)。
// Mainメソッド内
int[] a = { 1, 1, 1 };
SampleMethodRef(ref a); // refを書かないとコンパイルエラー
WriteLine($"配列の参照渡し(ref):{string.Join(", ", a)}");
// 出力:配列の参照渡し(ref):3, 0, 0, 0, 0
outパラメーター修飾子を使って参照渡しをする場合。受け取ったメソッドの側では、新しいインスタンスを割り当てねばならない(次のコード)。
static void SampleMethodOut(out int[] a)
{
// a[0] = 2; // この行はコンパイルエラー(未割り当てのパラメーターの使用)
a = new int[5];
a[0] = 3;
}
outパラメーター修飾子を使って値型を参照渡しする場合は、呼び出す前に変数を初期化しなくてもよい。C# 7では、呼び出し時の引数リスト内で変数宣言もできる(次のコード)。
// Mainメソッド内
SampleMethodOut(out int[] a); // outを書かないとコンパイルエラー
WriteLine($"配列の参照渡し(out):{string.Join(", ", a)}");
// 出力:配列の参照渡し(out):3, 0, 0, 0, 0
以上の本文では、「値型」「参照型」「値渡し」「参照渡し」が入り乱れてたくさん出てくる。そのため、その違いは分かっているはずなのに混乱してしまった読者もおられるだろう。以下のように考えると分かりやすい。
引数の値渡しは、変数の中身の複製を作ってメソッドに渡す(次の図)。値型の値渡しではオブジェクトのコピーが、参照型の値渡しでは参照のコピーがメソッドに渡されることになる。
値型の値渡しでは、呼び出し元とメソッド内とで扱うオブジェクトは別物である。従って、メソッド内で何をしようと、呼び出し元には影響しない。
参照型の値渡しでは、呼び出し元とメソッド内とで、参照の先につながっているオブジェクトは(メソッドの開始時点では)同一のものだ。ただし、呼び出し元とメソッド内とで扱う参照そのもの(図では緑色のタグ)は別物なので、メソッド内でタグに結び付けるオブジェクトを置き換えても(=引数に別のインスタンスを割り当てても)、呼び出し元には影響しない。
引数の参照渡しは、変数への参照を作ってメソッドに渡す(次の図)。値型の参照渡しではオブジェクトを内包している変数への参照が、参照型の参照渡しではオブジェクトへの参照を内包している変数への参照がメソッドに渡されることになる。参照渡しでは、呼び出し元とメソッド内とで扱うオブジェクトは常に同一のものなのだ。
値型の参照渡しでは、呼び出し元とメソッド内とで扱うオブジェクトは同一のものである。従って、メソッド内でオブジェクトに変更を加えると、呼び出し元にも影響する。また、メソッド内で新しいオブジェクトを割り当てると、それは参照先の変数に割り当てたことになる。
参照型の参照渡しでは、呼び出し元とメソッド内とで、参照の先につながっているオブジェクトは同一のもので、メソッド内でオブジェクトに変更を加えると呼び出し元にも影響する。メソッド内で新しいオブジェクトを割り当てた場合、それは参照先の参照(図では左側、変数内の緑色のタグ)に結び付くオブジェクトを置き換えることになる。
引数の参照渡しは、主に値型で使われる。inパラメーター修飾子は、メソッドの入力として大きな構造体を渡す場面で、高速化のために使う。refパラメーター修飾子は、主に複数の値型をメソッド側で変更してもらうために使う。outパラメーター修飾子は、メソッドから複数の結果を受け取るために使う。
利用可能バージョン:C# 1.0以降(inパラメーター修飾子はC# 7.2以降)
カテゴリ:C# 処理対象:言語構文
関連TIPS:オプション引数が使えるメソッドを作るには?[C#/VB]
関連TIPS:構文:複数のオブジェクトを一時的に1つにまとめるには?[C#/VB、.NET Framework 4.7以降]
関連TIPS:手軽にプロパティを実装するには?[C#、VS 2008、3.5]
関連TIPS:構文:クラス名を書かずに静的メソッドを呼び出すには?[C# 6.0]
関連TIPS:数値を右詰めや0埋めで文字列化するには?[C#、VB]
Copyright© Digital Advantage Corp. All Rights Reserved.