1. はじめに
C#ではデフォルトで全てのメソッドパラメータは値渡しなんだ。つまり、メソッドにはコピーが渡されて、元の変数じゃないんだよ。
でもさ、もしメソッドの中から外の変数を変えたい時は?それともメソッドから一気に複数の値を返したい時は?あるいは、でっかい構造体をメソッドに渡したいけど、全部コピーするのは「持ち手のないスーツケース」みたいで嫌だし、メモリ節約のために参照だけ渡したい時は?
こういう時に使うのがパラメータ修飾子だよ。これを使うと、呼び出し元とメソッドの間でデータがどうやって渡るかを自分でコントロールできるんだ。
修飾子の種類
| 修飾子 | データの流れ | 呼び出し前に初期化必要? | 中で読み取りOK? | 中で書き込みOK? | よくある使い方 |
|---|---|---|---|---|---|
| ref | 両方向 | はい | はい | はい | 変数を読み書きしたい時に渡す |
| out | 外向きだけ | いいえ | いいえ(代入前) | はい(必須!) | メソッドから複数の値を返す |
| in | 内向きだけ | はい | はい | いいえ | 大きな構造体を「読み取り専用」で渡して節約 |
心配しなくて大丈夫、これから一つずつ見ていくよ。思ったより全然簡単だから!
2. ref修飾子:参照渡しで読み書きできる
例えばさ、友達にコーヒーカップを持っていって「これ、飲んでもいいし、温めてもいいし、何か足してもいいよ」って言う感じ。友達はそのコーヒーで何でもできて、残ったもの(あるいは何も残らなくても)が君に戻ってくる。これがrefの動きだよ。
void DoSomething(ref int x)
{
x = x + 10; // 入力パラメータを変更する
}
refとしてパラメータを渡すには、宣言時も呼び出し時も同じ修飾子を付ける必要があるよ:
int myNumber = 5;
DoSomething(ref myNumber);
Console.WriteLine(myNumber); // 15が出力される
これは必ず変数じゃないとダメで、式を参照渡しすることはできない。こんなコードは動かないよ:
DoSomething(ref 10); // ここは変数じゃないとダメ!
- 呼び出し前:変数は初期化されている必要がある(値が入ってること)。
- メソッド内:値の読み書きができる。
- 呼び出し後:変更がそのまま残る。
例:変数の値を入れ替える
たまに2つの変数の値を入れ替えたい時があるよね。でも注意点があって、C#の値型(例えばint)はデフォルトで値渡しなんだ。つまり、メソッドに渡すときは値そのものがコピーされて、変数の参照じゃない。呼び出し元の変数自体を変えたいなら、refキーワードが必要だよ。
refなしの例 — うまくいかない:
// 値を入れ替えようとしてもダメ
void Swap(int a, int b)
{
int temp = a;
a = b;
b = temp;
}
int x = 10;
int y = 20;
Swap(x, y); // 値のコピーが渡される
Console.WriteLine($"{x}, {y}"); // → 10, 20 — 何も変わらない!
Swap関数の中のaとbはxとyのコピー。コピーだけを変えて、元はそのままなんだ。
refを使った例 — 変数が入れ替わる
// refを使って変数の値を入れ替える
void Swap(ref int a, ref int b)
{
int temp = a;
a = b;
b = temp;
}
int x = 10;
int y = 20;
Swap(ref x, ref y); // 変数の参照が渡される
Console.WriteLine($"{x}, {y}"); // → 20, 10 — ちゃんと入れ替わった!
refを使うと、値じゃなくて変数自体の参照を渡せるんだ。
だからSwap関数はxとyそのものを直接いじれるってわけ。
refを使うタイミングは?
- 渡した変数を変更したい時(例えば増やしたり、入れ替えたり)。
- 新しい値をreturnで返す必要がなくて、引数自体を直接変えたい時。
- いろんな値を「ごちゃっと」返したい時は使わない方がいい。他の修飾子が向いてる。
3. out修飾子:外に値を渡す
今度はこう考えてみて。友達のところに空のカップを持っていって「何でもいいから入れて!」って頼む感じ。友達は絶対に何か入れなきゃいけなくて、そうしないとコンパイラが怒る。
outは、メソッドから複数の値を返したい時に使うんだ。戻り値を使わずに値を返せる。こういうパラメータはメソッドの中で必ず初期化しないとダメ。
void GetCoordinates(out int x, out int y)
{
x = 5;
y = 10; // これがないとエラー!
}
- 呼び出し前:変数は初期化しなくてもOK。呼び出しの中でout int xみたいに書いてもいい。
- メソッド内:必ず値を代入しないと、コンパイラがストライキする。
- 呼び出し後:変数には新しい値が入ってる。
例:メソッドから2つの値を返す
void ParseNameAndAge(string input, out string name, out int age)
{
string[] parts = input.Split(','); // 文字列を配列に分割する - ','で区切る
name = parts[0];
age = int.Parse(parts[1]);
}
string userInput = "イワン,25";
ParseNameAndAge(userInput, out string userName, out int userAge);
Console.WriteLine($"{userName} — {userAge} 歳");
outを使うタイミングは?
- メソッドから複数の値を返したい時(例えば上の例みたいに文字列を分解する時)。
- 戻り値の型が事前に決まってない時(例えば文字列を数値に変換する時)。
.NETの例:メソッドint.TryParse(string, out int) — これぞ定番!
string input = "123";
if (int.TryParse(input, out int parsedNumber))
{
Console.WriteLine($"変換できた: {parsedNumber}");
}
else
{
Console.WriteLine("変換エラー!");
}
int.TryParseメソッドは、渡した文字列を数値に変換できたらtrue、できなかったらfalseを返す。数値自体はoutで返してくれるんだ。
refとoutでよくあるミスや面白い話
- ref用の変数を初期化し忘れると、コンパイラが怒る。
- outで値を代入し忘れると、コンパイラはもっと怒る。
- 呼び出し時に修飾子を付け忘れる:DoSomething(x)と書いてDoSomething(ref x)にしないとダメ。
- refやoutは定数やリテラルでは使えない!変数だけ。変数だけがメモリアドレスを持ってて、参照を作れるんだ。
4. in修飾子:読み取り専用で参照渡し
たまに、でっかくて複雑なオブジェクトをメソッドに渡したい時があるよね。普通は参照で渡すけど、そうするとメソッドの中でうっかり変更されるかもしれない。だって参照を渡してるから、フルアクセスできちゃう。
もちろん、完全なコピーを作って渡すこともできるけど、オブジェクトが大きいと無駄にデータをコピーすることになる。それより、参照で渡して「絶対に誰も変更しないで!」ってコンパイラに見張ってもらう方がいいよね。
inは新しい修飾子で、構造体を参照で渡しつつ、読み取り専用にできるんだ。これは「ガラスケースに入った宝剣」みたいなもので、見るだけOK、触るのはダメって感じ。
void PrintPoint(in Point pt)
{
pt.X = 5; // エラー! 読み取り専用。
Console.WriteLine($"ポイント: {pt.X}, {pt.Y}"); //これはOK
}
- 呼び出し前:変数は初期化されている必要がある。
- メソッド内:読み取りだけOK、変更はダメ。
- メモリ節約:構造体はコピーされず、参照で渡される。
例えば大きな構造体の配列を渡す時に、無駄なコピーを避けるために使うよ。ただしクラス(参照型)にはinはあまり意味がない。
例:大きな構造体をコピーせず参照で渡す
struct BigData
{
public int A, B, C, D, E;
}
void PrintBigData(in BigData data)
{
data.A = 10; // ダメ - 読み取り専用!
Console.WriteLine(data.A + data.B + data.C + data.D + data.E);
}
BigData myData = new BigData { A = 10, B = 20, C = 30, D = 40, E = 50 };
PrintBigData(in myData);
5. よくある質問:
ref/out/inは配列や参照型でも使える?
うん、使えるよ!でも注意:配列自体が参照型だから、refで配列を渡すと、配列そのものの参照を変えられる。大体は構造体で使うことが多いかな。
refとoutの違いは?どっちも値を受け取れるけど?
refは使う前に変数を初期化しておく必要がある。outは初期化不要だけど、メソッド内で必ず一度は値を代入しないとダメ。
リテラル(例えば5)をref/out/inで使ったら?
コンパイラが即エラー出すよ。変数だけOK!
ref/out/inパラメータはいくつまでOK?
いくつでもOK、制限なし!でもコードの読みやすさのために2〜3個以上になったら、タプルや構造体、クラスで返すことを考えた方がいいかも。
GO TO FULL VERSION