連載
» 2018年04月25日 05時00分 公開

.NET TIPS:in/out/refパラメーター修飾子の違いとは?[C#]

in/out/refパラメーター修飾子を利用すると、パラメーターの受け渡しを効率的に行える。これらの修飾子の違いと使用する上での注意点をまとめる。

[山本康彦,BluewaterSoft/Microsoft MVP for Windows Development]
「.NET TIPS」のインデックス

連載「.NET TIPS」

 メソッドに引数を渡す方法に、値渡しと参照渡しがある。さらにC#では、メソッドの引数リストで参照渡しを宣言するために、in/out/refの3通りのパラメーター修飾子がある。inパラメーター修飾子はC# 7.2の新機能だ。本稿では、この参照渡しのin/out/refの使い方の違いを解説する。

POINT in/out/refパラメーター修飾子の違い

in/out/refパラメーター修飾子の違いまとめ in/out/refパラメーター修飾子の違いまとめ


 特定のトピックをすぐに知りたいという方は以下のリンクを活用してほしい。

 なお、本稿に掲載したサンプルコードをそのまま試すにはVisual Studio 2017(15.5以降)が必要である。サンプルコードはコンソールアプリの一部であり、コードの冒頭に以下の宣言が必要となる。

using static System.Console;

本稿のサンプルコードに必要な宣言(C#)

 また、inパラメーター修飾子を使うには、本稿執筆時点ではVisual Studio 2017(15.5以降)で以下の操作が必要だ。

  1. ソリューションエクスプローラでプロジェクトを右クリックして、コンテキストメニューから[プロパティ]を選び、プロジェクトのプロパティウィンドウを表示する
  2. プロジェクトのプロパティウィンドウの[ビルド]タブにある[詳細設定]ボタンをクリックする
  3. [ビルドの詳細設定]ダイアログが表示されるので、[言語バージョン]ドロップダウンで[C# 7.2]を選択する

 上の3.の操作は、C#のバージョンが異なるものの別記事「Dev Basics/Keyword:C# 7.1」に画像が掲載してあるので参考にしていただきたい。

in/out/refパラメーター修飾子の共通点とは?

どれも参照渡しである

 メソッドの引数リストに記述するそれぞれの引数に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パラメーター修飾子を付ける例(C#)

 ちなみに、in/out/refパラメーター修飾子にはソースコード上では後述するような違いがあるが、コンパイル結果は同じになる(バイナリレベルでは参照渡しの方法は1つしかない)。in/out/refパラメーター修飾子の違いは、ソースコードを明瞭にするために設けられているのである。

どれも直ちに実行されないメソッドでは使えない

 参照渡しは、次の2種類のメソッドでは禁止されている。

  • Asyncメソッド:シグネチャにasync修飾子を付けたメソッド
  • イテレータメソッド:yield returnステートメント/yield breakステートメントを含むメソッド

修飾子の違いではオーバーロードできない

 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/out/refパラメーター修飾子の違いではオーバーロードにならない(C#)

in/out/refパラメーター修飾子の違いとは?

 どれも参照渡しであるが、次のように用途によって使い分ける。

  • in:引数をメソッドへの入力として使う
  • ref:汎用。主に値型の引数を変更してもらうために使う
  • out:引数をメソッドからの出力として使う

 細かく見ると、メソッドの呼び出し方と、メソッド内部でその引数に対して行えることに、次の表のような差異がある。

修飾子 用途 呼び出す前の変数初期化 呼び出し時の修飾子付与 メソッド内での参照先の割り当て/変更 オプション引数
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; }
}

値型の例(C#)
この構造体は16バイトある。サイズが参照よりも大きいので、参照渡しにすると値渡しより高速化が見込める。

 この構造体を引数として受け取った側のメソッドで、そのプロパティを変更したり、インスタンスを割り当て直したりしたときに、どのような挙動になるか実際に確認していこう。

 まず、値渡しの場合。受け取ったメソッドの側では、プロパティの値を変更したり、新しいインスタンスを割り当てたりできる(次のコード)。

static void SampleMethod(SampleStruct s)
{
  s.X = 3.0;
  s = new SampleStruct();
  s.X = 4.0;
}

値型を値渡しで受け取るメソッドの例(C#)

 しかしこの変更や割り当ては、呼び出し元の変数には影響しない(次のコード)。値渡しは変数の中身(この場合は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

値型を値渡しで渡すコードの例(C#)
出力結果を見ると、変数sの内容はメソッド呼び出し前と変わっていない。

 次に、inパラメーター修飾子を使って参照渡しをする場合。受け取ったメソッドの側では、プロパティの値を変更したり、新しいインスタンスを割り当てたりできない(次のコード)。

static void SampleMethodIn(in SampleStruct s)
{
  // 以下、全てコンパイルエラー
  //s.X = 3.0;
  //s = new SampleStruct();
  //s.X = 4.0;
}

値型をin参照渡しで受け取るメソッドの例(C#)
inパラメーター修飾子が付いている値型の引数に対しては、そのメンバーの値を変更したり、新しいインスタンスを割り当てたりしようとすると、コンパイルエラーになる。

 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

値型をin参照渡しで渡すコードの例(C#)
出力結果を見ると、変数sの内容はメソッド呼び出し前と変わっていない。
なお、もしもSampleMethodInメソッドに値渡しと参照渡しの2つのオーバーロードがあった場合は、呼び出し時のinキーワードの有無で呼び出されるメソッドが違うものになる。

 refパラメーター修飾子を使って参照渡しをする場合。受け取ったメソッドの側では、プロパティの値を変更したり、新しいインスタンスを割り当てたりできる(次のコード)。

static void SampleMethodRef(ref SampleStruct s)
{
  s.X = 3.0;
  s = new SampleStruct();
  s.X = 4.0;
}

値型をref参照渡しで受け取るメソッドの例(C#)
refパラメーター修飾子が付いている値型の引数に対しては、そのメンバーの値を変更したり、新しいインスタンスを割り当てたりできる。

 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

値型をref参照渡しで渡すコードの例(C#)
出力結果を見ると、プロパティXが変わっているだけでなく、プロパティYが0に変わっている。これはメソッド内で新しいインスタンスが割り当てられたためである。

 outパラメーター修飾子を使って参照渡しをする場合。受け取ったメソッドの側では、新しいインスタンスを割り当てねばならない(次のコード)。

static void SampleMethodOut(out SampleStruct s)
{
  //s.X = 3.0; // この行はコンパイルエラー(未割り当てのパラメーターの使用)
  s = new SampleStruct();
  s.X = 4.0;
}

値型をout参照渡しで受け取るメソッドの例(C#)
outパラメーター修飾子が付いている引数に対しては、新しいインスタンスを割り当てなければならない。

 outパラメーター修飾子を使って値型を参照渡しする場合は、呼び出す前に変数を初期化しなくてもよい。C# 7では、呼び出し時の引数リスト内で変数宣言もできる(次のコード)。

// Mainメソッド内
SampleMethodOut(out SampleStruct s); // outを書かないとコンパイルエラー
WriteLine($"値型の参照渡し(out):X={s.X}, Y={s.Y}");
// 出力:値型の参照渡し(out):X=4, Y=0

値型をout参照渡しで渡すコードの例(C#)
C# 7では、このようにoutパラメーター修飾子の後ろで変数宣言ができる。ここではオーバーロード解決のために型名をSampleStructと書いているが、その心配がなければvarでよい。

配列(参照型)を渡す例

 クラスなどの参照型を参照渡しにする意味はあまりないのだが、その挙動を確認しておこう。どうしても使わねばならない理由がない限り、参照型の参照渡しは使わない方がよいだろう。配列やList<T>ジェネリックコレクションなども参照型なので、ここでは整数の配列を例にしよう。

 まず、値渡しの場合。受け取ったメソッドの側では、配列の要素を変更したり、新しいインスタンスを割り当てたりできる(次のコード)。

static void SampleMethod(int[] a)
{
  a[0] = 2;
  a = new int[5];
  a[0] = 3;
}

参照型を値渡しで受け取るメソッドの例(C#)

 配列要素の変更は呼び出し元の変数に影響するが、新しい配列の割り当ては呼び出し元の変数には影響しない(次のコード)。値渡しは変数の中身(この場合は配列の実体への参照)のコピーをメソッドに渡すため、参照先(ここでは配列の実体)は呼び出し元とメソッドで同じであり、配列要素の変更は呼び出し元に影響する。メソッド内で新しい配列を割り当てるのは、参照のコピーに対してであるため、呼び出し元の参照には影響しない。

// Mainメソッド内
int[] a = { 1, 1, 1 };
SampleMethod(a);
WriteLine($"配列の値渡し:{string.Join(", ", a)}");
// 出力:配列の値渡し:2, 1, 1

参照型を値渡しで渡すコードの例(C#)
配列要素a[0]は変更されている。メソッド内で行われた新しい配列の割り当ては、反映されていない(a[1]以降の値が初期化されず1のまま維持されている)。

 次に、inパラメーター修飾子を使って参照渡しをする場合。受け取ったメソッドの側では、参照先に新しいインスタンスを割り当てることはできない。ただし、inパラメーター修飾子でよく誤解されることなのだが、参照先の参照先は変更できる。この場合では、配列の要素は書き換え可能なのである(次のコード)。

static void SampleMethodIn(in int[] a)
{
  a[0] = 2;
  // a = new int[5];  // この行はコンパイルエラー
  a[0] = 3;
}

参照型をin参照渡しで受け取るメソッドの例(C#)
参照型を渡した場合、inパラメーター修飾子が禁止するのは参照先(この場合は、配列への参照)だけである。参照先の参照先(すなわち、配列オブジェクトそのもの)の中は変更可能である。

 inパラメーター修飾子を使って参照型を参照渡しする場合、オブジェクトの内容は変更されることがある(次のコード)。

// Mainメソッド内
int[] a = { 1, 1, 1 };
SampleMethodIn(a);
SampleMethodIn(in a); // inは書いても書かなくてもよい
WriteLine($"配列の参照渡し(in):{string.Join(", ", a)}");
// 出力:配列の参照渡し(in):3, 1, 1

参照型をin参照渡しで渡すコードの例(C#)

 refパラメーター修飾子を使って参照渡しをする場合。受け取ったメソッドの側では、配列の要素を変更したり、新しいインスタンスを割り当てたりできる(次のコード)。

static void SampleMethodRef(ref int[] a)
{
  a[0] = 2;
  a = new int[5];
  a[0] = 3;
}

参照型をref参照渡しで受け取るメソッドの例(C#)
refパラメーター修飾子が付いている参照型の引数に対しては、参照先を変更したり(=新しいインスタンスの割り当て)、参照先の参照先(ここでは配列オブジェクトそのもの)の中を変更したりできる。

 refパラメーター修飾子を使って参照型を参照渡しした場合、メソッド内でその値が変更されたり、インスタンスが丸ごと入れ替えられたりする(次のコード)。

// Mainメソッド内
int[] a = { 1, 1, 1 };
SampleMethodRef(ref a); // refを書かないとコンパイルエラー
WriteLine($"配列の参照渡し(ref):{string.Join(", ", a)}");
// 出力:配列の参照渡し(ref):3, 0, 0, 0, 0

参照型をref参照渡しで渡すコードの例(C#)
出力結果を見ると、配列の要素数が増えている。これはメソッド内で新しいインスタンスが割り当てられたためである。このように配列の要素数の変更も伴う書き換えができるのは、参照型を参照渡しするメリットだ。

 outパラメーター修飾子を使って参照渡しをする場合。受け取ったメソッドの側では、新しいインスタンスを割り当てねばならない(次のコード)。

static void SampleMethodOut(out int[] a)
{
  // a[0] = 2; // この行はコンパイルエラー(未割り当てのパラメーターの使用)
  a = new int[5];
  a[0] = 3;
}

参照型をout参照渡しで受け取るメソッドの例(C#)
outパラメーター修飾子が付いている引数に対しては、新しいインスタンスを割り当てなければならない。

 outパラメーター修飾子を使って値型を参照渡しする場合は、呼び出す前に変数を初期化しなくてもよい。C# 7では、呼び出し時の引数リスト内で変数宣言もできる(次のコード)。

// Mainメソッド内
SampleMethodOut(out int[] a); // outを書かないとコンパイルエラー
WriteLine($"配列の参照渡し(out):{string.Join(", ", a)}");
// 出力:配列の参照渡し(out):3, 0, 0, 0, 0

参照型をout参照渡しで渡すコードの例(C#)

【補足】値渡しと参照渡し

 以上の本文では、「値型」「参照型」「値渡し」「参照渡し」が入り乱れてたくさん出てくる。そのため、その違いは分かっているはずなのに混乱してしまった読者もおられるだろう。以下のように考えると分かりやすい。

  • 値型:変数にはオブジェクトそのものが入っている
  • 参照型:変数にはオブジェクトへの参照(オブジェクトに結び付けられたタグ=荷札のようなもの)が入っている
  • 値渡し:変数の中身の複製を作って渡す
  • 参照渡し:変数への参照を作って渡す

 引数の値渡しは、変数の中身の複製を作ってメソッドに渡す(次の図)。値型の値渡しではオブジェクトのコピーが、参照型の値渡しでは参照のコピーがメソッドに渡されることになる。

 値型の値渡しでは、呼び出し元とメソッド内とで扱うオブジェクトは別物である。従って、メソッド内で何をしようと、呼び出し元には影響しない。

 参照型の値渡しでは、呼び出し元とメソッド内とで、参照の先につながっているオブジェクトは(メソッドの開始時点では)同一のものだ。ただし、呼び出し元とメソッド内とで扱う参照そのもの(図では緑色のタグ)は別物なので、メソッド内でタグに結び付けるオブジェクトを置き換えても(=引数に別のインスタンスを割り当てても)、呼び出し元には影響しない。

引数の値渡し(考え方) 引数の値渡し(考え方)

 引数の参照渡しは、変数への参照を作ってメソッドに渡す(次の図)。値型の参照渡しではオブジェクトを内包している変数への参照が、参照型の参照渡しではオブジェクトへの参照を内包している変数への参照がメソッドに渡されることになる。参照渡しでは、呼び出し元とメソッド内とで扱うオブジェクトは常に同一のものなのだ。

 値型の参照渡しでは、呼び出し元とメソッド内とで扱うオブジェクトは同一のものである。従って、メソッド内でオブジェクトに変更を加えると、呼び出し元にも影響する。また、メソッド内で新しいオブジェクトを割り当てると、それは参照先の変数に割り当てたことになる。

 参照型の参照渡しでは、呼び出し元とメソッド内とで、参照の先につながっているオブジェクトは同一のもので、メソッド内でオブジェクトに変更を加えると呼び出し元にも影響する。メソッド内で新しいオブジェクトを割り当てた場合、それは参照先の参照(図では左側、変数内の緑色のタグ)に結び付くオブジェクトを置き換えることになる。

引数の参照渡し(考え方) 引数の参照渡し(考え方)

まとめ

 引数の参照渡しは、主に値型で使われる。inパラメーター修飾子は、メソッドの入力として大きな構造体を渡す場面で、高速化のために使う。refパラメーター修飾子は、主に複数の値型をメソッド側で変更してもらうために使う。outパラメーター修飾子は、メソッドから複数の結果を受け取るために使う。

「.NET TIPS」のインデックス

.NET TIPS

Copyright© Digital Advantage Corp. All Rights Reserved.

RSSについて

アイティメディアIDについて

メールマガジン登録

@ITのメールマガジンは、 もちろん、すべて無料です。ぜひメールマガジンをご購読ください。