概要
ref: 引数を参照で渡し、値の書き換えと、関数内で参照先の書き換えの両方を許可したい時に使う
out: 関数の引数に計算結果を返したい時に使う
in: 引数を参照で渡し、値の書き換えのみ許可したい時に使う
前提知識:C#における値型と参照型の違いについて
C/C++からプログラムを始めた私にとって、C#の最初のつまづきポイントがここだった。C#プログラミングにおいて、値型と参照型を意識することは非常に重要である。
値型と参照型の違いは単純で、「その型の値を直接保持するか否か」である。以下に簡単な例を示す。
public struct T
{
public int a, b, c;
public T(int _a, int _b, int _c) { a = _a; b = _b; c = _c; }
public override string ToString() { return $"{a}, {b}, {c}"; }
}
public class U
{
public int a, b, c;
public U(int _a, int _b, int _c) { a = _a; b = _b; c = _c; }
public override string ToString() { return $"{a}, {b}, {c}"; }
}
T a1 = new T(1, 1, 1);
T a2 = a1;
T a3 = a2;
a1.a = 2;
Console.WriteLine($"a1: {a1}");
Console.WriteLine($"a2: {a2}");
Console.WriteLine($"a3: {a3}");
Console.WriteLine("=======================");
U b1 = new U(1, 1, 1);
U b2 = b1;
U b3 = b2;
b1.a = 2;
Console.WriteLine($"b1: {b1}");
Console.WriteLine($"b2: {b2}");
Console.WriteLine($"b3: {b3}");
出力結果
a1: 2, 1, 1
a2: 1, 1, 1
a3: 1, 1, 1
=======================
b1: 2, 1, 1
b2: 2, 1, 1
b3: 2, 1, 1
値型の変数は、値そのもの(今回の例でいうと int a, b, cの3つのint型の値
)を保持する。そのため、 =
を使って代入した時、値すべてをコピーして代入先に格納するという挙動になる。
一方参照型の変数は、値の実体がどこにあるかを示す参照情報だけを持ち、実体は別の場所(ヒープ)に用意する。そのため、 =
を使って代入すると参照情報だけをコピーするので、上記コードでの変数 b1, b2, b3
は同じ値の実体を指していることになる。
値型と参照型について、詳しくは以下のWebサイトを見てほしい。
ufcpp.net
techblog.kayac.com
関数の引数は値渡し
C#で引数付きの関数を呼び出す時、関数には値そのものが渡される。これを一般に値渡しと呼ぶ。
では、値型と参照型で「値渡し」した時の挙動がどう変わってくるか見てみる。
void ModifyValueStruct(T t)
{
t.a = 10;
}
void ModifyValueClass(U u)
{
u.a = 10;
}
T t = new T(1, 1, 1);
U u = new U(1, 1, 1);
ModifyValueStruct(t);
ModifyValueClass(u);
Console.WriteLine($"T t: {t}");
Console.WriteLine($"U u: {u}");
出力結果
T t: 1, 1, 1
U u: 10, 1, 1
値型の変数を関数に値渡しすると、「値そのもの」がコピーされ関数に引き渡される。つまり、関数呼び出し側の変数 t
と、関数内の変数 t
は別モノ。関数内でいくら値型引数の値を書き換えても、関数呼び出し元には、その変更は伝わらない。
一方参照型の変数を関数に値渡しすると、「値の参照情報」だけがコピーされ、関数に引き渡される。つまり、関数呼び出し側の変数 u
と 関数内の変数 u
は同じ値を指している。したがって、関数内で参照型引数の値を書き換えると、関数呼び出し元の変数が指す値も書き換わる。
ref/out/inを使った参照渡し
ref/out/in修飾子は、「関数の引数を参照渡し」したい時に使われる。
ref/out/inを使いたいときは、関数宣言側と、引数引数の型名の前にrefなどの修飾子をつければよい。
void RefFunc(ref int i) { /* ... */ } // 型名の前にref修飾子をつける
// ...
int num;
RefFunc(ref num); // 呼び出すときにもref修飾子をつける(注:inはつけなくてもよい)
ref/out/in修飾子はすべて「引数の参照渡し」を実現するためのキーワードだが、それぞれ細かく挙動が変わってくる。以下にその違いをまとめた表を載せた。
キーワード |
引数として渡す前に初期化が必要か |
メソッド内で参照先の値の書き換えが可能か |
呼び出し時の修飾子付与 |
ref |
必須 |
可能 |
必須 |
out |
不要 |
必須 |
必須 |
in |
必須 |
不可 |
任意 |
値型を参照渡しする例
void Func(T t)
{
t.a = 2;
}
void Func2(T t)
{
t = new T(2, 2, 2);
}
void RefFunc(ref T t)
{
t.a = 2;
}
void RefFunc2(ref T t)
{
t = new T(2, 2, 2);
}
void OutFunc(out T t)
{
t = new T(3, 3, 3);
}
void InFunc(in T t)
{
}
T t;
t = new T(1, 1, 1);
Func(t);
Console.WriteLine($"Func() -> {t}");
t = new T(1, 1, 1);
Func2(t);
Console.WriteLine($"Func2() -> {t}");
t = new T(1, 1, 1);
RefFunc(ref t);
Console.WriteLine($"RefFunc() -> {t}");
t = new T(1, 1, 1);
RefFunc2(ref t);
Console.WriteLine($"RefFunc2() -> {t}");
t = new T(1, 1, 1);
OutFunc(out t);
Console.WriteLine($"OutFunc() -> {t}");
t = new T(1, 1, 1);
InFunc(in t);
Console.WriteLine($"InFunc() -> {t}");
出力結果
Func() -> 1, 1, 1
Func2() -> 1, 1, 1
RefFunc() -> 2, 1, 1
RefFunc2() -> 2, 2, 2
OutFunc() -> 3, 3, 3
InFunc() -> 1, 1, 1
参照型を参照渡しする例
void Func(U u)
{
u.a = 2;
}
void Func2(U u)
{
u = new U(2, 2, 2);
}
void RefFunc(ref U u)
{
u.a = 2;
}
void RefFunc2(ref U u)
{
u = new U(2, 2, 2);
}
void OutFunc(out U u)
{
u = new U(3, 3, 3);
}
void InFunc(in U u)
{
}
U u;
u = new U(1, 1, 1);
Func(u);
Console.WriteLine($"Func() -> {u}");
u = new U(1, 1, 1);
Func2(u);
Console.WriteLine($"Func2() -> {u}");
u = new U(1, 1, 1);
RefFunc(ref u);
Console.WriteLine($"RefFunc() -> {u}");
u = new U(1, 1, 1);
RefFunc2(ref u);
Console.WriteLine($"RefFunc2() -> {u}");
u = new U(1, 1, 1);
OutFunc(out u);
Console.WriteLine($"OutFunc() -> {u}");
u = new U(1, 1, 1);
InFunc(in u);
Console.WriteLine($"InFunc() -> {u}");
出力結果
Func() -> 2, 1, 1
Func2() -> 1, 1, 1
RefFunc() -> 2, 1, 1
RefFunc2() -> 2, 2, 2
OutFunc() -> 3, 3, 3
InFunc() -> 1, 1, 1
ref/out/inの使い道
refは、既に値が入っている変数を書き換える時に使う。
気をつけたいのは、参照型の引数を書き換えたいからという理由でrefをつける必要はないこと。参照型の値渡しで値の書き換えは可能なので、その用途でrefをつける必要はない。
参照型引数にrefをつけるのは、「引数が指す先を変えたい(新しいインスタンスを指すようにしたい)」ときのみ。.NET APIでは、 Array.Resize()
で参照型引数にref修飾子を使っている。
outは、関数の返り値を複数持たせたいような時に使うことが多い印象。C++では「非constポインタ渡し」で実現する処理。ただしrefとは異なり、関数内で新たに初期化されるので、まだ宣言しかしていない変数を渡すべき。
ちなみに、下のように変数宣言をoutによる参照渡しと同時にこなすこともできる(outを使うときはこの書き方が安全だと思う)。
bool isValid = Calclate(out int num);
inは、inを使わければならない特別なプログラムがあるわけではない。
inの用途はズバリ「値型引数のコピーコスト削減」。
値型を値渡しする時、その値のコピーが作られる。この値が4Byteのint程度ならコピーにかかるコストは無視できるほど小さい(だろう)。しかし、もし巨大な値型(struct)を値渡ししたら巨大なコピーが作られてしまい、その分コピーに必要な時間もメモリも増えてしまう。こういう時にinは使われる。C++でいうところの「&を使ったconst参照渡し」に相当すると考えて良い。
が、コピーに無視できないほどのコストがかかる巨大な値型(struct)を作るべきではないということは忘れてはならない(そうしないとならない特別な事情って何かあるんでしょうか。知ってる方がいたら教えていください)。
まとめ
C/C++からプログラムを始めた人にとって、C#の値型と参照型の違いは必ず抑えておきたい。それを理解した上で、ref/out/inを使いたい。