Menu

C#のref、out、in引数:参照渡しの仕組み

C#のref、out、in引数の仕組みを解説します。コピーではなく変数そのものを渡す方法、追加の結果を返すoutとTryParseパターン、out var、読み取り専用のin引数、確実な代入のルール、そして引数が参照型のときに何が変わるかを扱います。

このページのコードはエディタで実行できます - 編集してすぐに結果を確認できます。

既定では、C#はすべての引数を値渡しします。メソッドはコピーを受け取り、引数に代入しても呼び出し側には影響しません。ref、out、in の修飾子を付けると、代わりに呼び出し側の変数そのものが渡されます。

出力:

by value: 50
by ref:   60

キーワードは2か所に現れます。引数リスト(ref int score)と呼び出し箇所(ref score)です。呼び出し側での記述は必須で、これは意図的なものです。AddPoints(ref score) を読んだ人は誰でも、score が変わるかもしれないとわかります。書き忘れると、コンパイラはCS1620 Argument 1 must be passed with the 'ref' keyword を報告します。

ref:呼び出し側の変数を読み書きする

ref 引数は呼び出し側の変数の別名です。メソッドは現在の値を読み、置き換えられます。典型的な例は、通常の引数では不可能な値の入れ替えです。

出力:

Linus Ada
volume 100

ref のルール:

  • 実引数は変数(ローカル変数、引数、フィールド、配列の要素)でなければならず、リテラルや式は使えません。プロパティも使えません:Clamp(ref player.Volume, 0, 100) はエラーCS0206 A non ref-returning property or indexer may not be used as an out or ref value になります。プロパティをローカル変数にコピーしてそれを渡し、後で代入し直します。
  • 変数は呼び出し前に確実に代入されていなければなりません。int n; Clamp(ref n, 0, 10); はCS0165 Use of unassigned local variable 'n' になります。

out:追加の結果

out はメソッドが生成する値のためのものです。呼び出し側は変数を初期化する必要がなく、メソッドは戻る前にすべての経路でそれに代入しなければなりません。

static void Split(string fullName, out string first, out string last)
{
    // error CS0177: The out parameter 'last' must be assigned to before control leaves the current method
    first = fullName.Split(' ')[0];
}

メソッドの中では、out 引数は書き込むまで未代入とみなされるので、呼び出し側の古い値を読むことはできません。これが ref との違いです。ref は双方向、out はメソッドから呼び出し側への一方向です。

TryParseパターン

out の最もよく知られた使い方は、基本ライブラリの Try パターンです。成功を bool で返し、結果を out 引数に入れるので、不正な入力でも例外は投げられません。

出力:

"vip25" is a coupon: 25% off
"42" is a quantity: 42
"BOGUS" is not recognized
"" is not recognized

out var quantity(C# 7.0)は呼び出しの中で直接変数を宣言し、その型は引数から決まります。失敗すると TryParse は変数を 0 にして false を返します。解析が成功するかどうかだけが重要なら、out _ で値を破棄します:bool isNumber = int.TryParse(input, out _);。

TryGetDiscount が両方の経路で rate に代入していることに注目してください。1回は明示的に、もう1回はそれ自体が out の呼び出しである Dictionary.TryGetValue に渡すことで代入しています。

outとタプルを返す方法の比較

C# 7より前は、複数の値を返す主な方法は out でした。現在はタプルのほうがより直接的に表せます。

// with out parameters
static void MinMax(int[] values, out int min, out int max) { ... }
MinMax(temps, out int lo, out int hi);

// with a tuple
static (int Min, int Max) MinMax(int[] values) { ... }
var (lo, hi) = MinMax(temps);

単純な複数の結果にはタプル(または小さなクラス)を使います。out は、bool の戻り値によって呼び出しを if の条件に置ける Try パターンのために残しておきます。

参照型:オブジェクトの変更と置き換え

クラスのインスタンスを値渡しした時点で、メソッドはすでにオブジェクトを変更できます。コピーされる値が同じオブジェクトへの参照だからです。ref が加えるのはもう1つの能力で、呼び出し側の変数が指すオブジェクトを置き換えることです。

出力:

after AddItem:      2 items
after ResetByValue: 2 items
after ResetByRef:   0 items

ResetByValue は自分が持つ参照のコピーを新しいリストに向けただけで、呼び出し側には伝わりませんでした。ResetByRef は呼び出し側の変数そのものを変更しました。普段のコードでクラスの引数に ref が必要になることはまれで、代わりに新しいオブジェクトを返します。

構造体では違いがより大きくなります。構造体の実引数はフィールドごとにコピーされるからです。呼び出し側の構造体(Point や大きな設定用の構造体)を変更しなければならないメソッドには ref が必要です。

in:読み取り専用の参照渡し

C# 7.2で追加された in は、参照で渡しますが代入を禁止します。目的は性能で、呼び出しのたびに大きな構造体がコピーされず、呼び出し側には値が変わらないことが保証されます。

// C# 7.2 and later
struct Matrix4
{
    public double M11, M12, M13, M14, M21, M22, M23, M24,
                  M31, M32, M33, M34, M41, M42, M43, M44;
}

static double Trace(in Matrix4 m) => m.M11 + m.M22 + m.M33 + m.M44;

static void Reset(in Matrix4 m)
{
    m.M11 = 0;
    // error CS8332: Cannot assign to a member of variable 'm' or use it as the right hand side of a ref assignment because it is a readonly variable
}

呼び出し箇所での in は省略可能で、Trace(matrix) と Trace(in matrix) のどちらも動きます。int、double、DateTime などの小さな型では in による利点はなく、わずかに遅くなることもあります。頻繁に渡す、フィールドを複数持つ構造体に使います。

制限

  • ref と out の引数は省略可能にできません(CS1741)。in 引数はできます:static void Connect(in int retries = 3) は正しい構文です。
  • オーバーロードは ref と out(または in)の違いだけでは区別できません。ランタイムのレベルでは同じだからです。F(int x) と F(ref int x) は共存できます。
  • async メソッドとイテレーター(yield return を使うメソッド)は、ref、out、in の引数を持てません。それらからはタプルか結果のオブジェクトを返します。
  • ラムダやローカル関数は、外側のメソッドの ref、out、in 引数をキャプチャできません。先にローカル変数にコピーします。

ref、out、inの比較

呼び出し側の事前の代入メソッドでの代入メソッドでの読み取り呼び出し箇所のキーワード
(なし)必要不要可(コピー)なし
ref必要不要可必須
out不要すべての経路で必要代入した後のみ必須
in必要不可可省略可能

よくある質問

C#のrefとoutの違いは何ですか?

どちらもコピーではなく呼び出し側の変数そのものを渡します。ref では、呼び出し前に変数に代入されている必要があり、メソッドはそれを読んだり変更したりできます。out では、事前の代入は不要で、メソッドは書き込む前にそれを読むことができず、戻る前に必ず代入しなければなりません(そうでなければCS0177)。out は「これは追加の結果です」という意味です。

C#のout varとはどういう意味ですか?

C# 7.0以降、呼び出しの中でout変数を宣言できます:if (int.TryParse(text, out var number))。変数の型は引数から推論され、その文の後でもスコープ内にあります。out _ は不要な結果を破棄します。

C#ではオブジェクトは参照渡しされますか?

いいえ。ref、out、in を書かない限り、すべての引数は値渡しです。クラスの場合、コピーされる値が参照なので、メソッドはオブジェクトのフィールドを変更でき、呼び出し側からもその変更が見えます。ref なしでできないのは、呼び出し側の変数に別のオブジェクトを指させることです。

C#のin引数修飾子とは何ですか?

in(C# 7.2以降)は引数を参照で渡しますが、読み取り専用です。メソッドはそれに代入できません。呼び出しのたびに大きな構造体をコピーするのを避けるために存在します。int や DateTime のような小さな型では利点はありません。

「Argument 1 must be passed with the 'ref' keyword」になるのはなぜですか?

メソッドが ref 引数を宣言しているのに、呼び出しでキーワードを省いたからです。C#では呼び出し箇所にも ref(と out)が必要で、変数が変わるかもしれないことを読む人にわかるようにしています:Increment(count); ではなく Increment(ref count); と書きます。

Coddy programming languages illustration

Coddyでコードを学ぼう

始める