C#の参照渡しとは?値渡しとの違いとref・out・inの使い方を初心者向けに解説

はじめに

C#でメソッドを作っていると、「引数に渡した値をメソッドの中で変更したい」「1つのメソッドから複数の値を返したい」といった場面があります。

そのようなときに登場するのが、参照渡しです。

C#の参照渡しでは、主に次の3つのキーワードを使います。

キーワード主な用途
ref呼び出し元の変数をメソッド内で読み書きしたいとき
outメソッドから値を返したいとき
in値を変更せず、効率よく渡したいとき

ただし、C#の「参照渡し」は、初心者にとって少し混乱しやすいテーマです。

特に、次のような点でつまずきやすいです。

混乱しやすいポイント内容
値渡しと参照渡しメソッドに値のコピーを渡すのか、変数そのものを渡すのか
値型と参照型intstructと、classや配列の違い
参照型の値渡し参照型は値渡しでもオブジェクトの中身を変更できる
refoutの違いどちらも参照渡しだが、使う目的が違う
inの意味読み取り専用の参照渡し

この記事では、C#の参照渡しについて、値渡しとの違い、refoutinの使い方、参照型との関係、よくあるエラーまで初心者向けにわかりやすく解説します。

1. C#の参照渡しとは?初心者向けにわかりやすく解説

1-1. 参照渡しとは「呼び出し元の変数そのものに影響を与えられる渡し方」

C#の参照渡しとは、メソッドに引数を渡すときに、値のコピーではなく、呼び出し元の変数そのものを操作できるように渡す方法です。

通常、メソッドに変数を渡すと、その変数の値がコピーされます。メソッド内でそのコピーを書き換えても、呼び出し元の変数は変わりません。

しかし、参照渡しを使うと、メソッド内での変更が呼び出し元の変数にも反映されます。

たとえば、次のようなイメージです。

C#
static void AddTen(ref int number)
{
number += 10;
}

int value = 5;
AddTen(ref value);

Console.WriteLine(value); // 15

valueには最初5が入っています。

AddTen(ref value)を呼び出すと、メソッド内でnumber += 10;が実行されます。このとき、numberは単なるコピーではなく、呼び出し元のvalueとつながっています。

そのため、メソッドの実行後、valueの値は15になります。

1-2. C#の引数は基本的に値渡しである

C#では、メソッドの引数は基本的に値渡しです。

値渡しとは、変数の中身をコピーしてメソッドに渡すことです。

C#
static void AddTen(int number)
{
number += 10;
}

int value = 5;
AddTen(value);

Console.WriteLine(value); // 5

このコードでは、AddTenメソッドの中でnumberに10を足しています。

しかし、呼び出し元のvalueは変わりません。出力結果は5です。

なぜなら、AddTen(value)で渡されているのは、valueそのものではなく、valueの値である5のコピーだからです。

メソッド内のnumberを変更しても、呼び出し元のvalueには影響しません。

1-3. 参照渡しを使うとメソッド内の変更が呼び出し元に反映される

参照渡しを使うには、メソッド定義と呼び出し側の両方でrefなどのキーワードを付けます。

C#
static void AddTen(ref int number)
{
number += 10;
}

int value = 5;
AddTen(ref value);

Console.WriteLine(value); // 15

この場合、numberを変更すると、呼び出し元のvalueも変更されます。

つまり、参照渡しは次のような場面で使います。

使いたい場面
メソッド内で呼び出し元の変数を変更したい数値を更新する、変数を入れ替える
複数の値を返したいoutで結果を返す
大きな構造体のコピーを避けたいinで読み取り専用として渡す

ただし、参照渡しを使いすぎると、どこで値が変更されたのかわかりにくくなります。そのため、必要な場面に絞って使うことが大切です。

1-4. まず押さえたい「値」「参照」「変数」の関係

C#の参照渡しを理解するには、まず「値」「参照」「変数」の関係を押さえておく必要があります。

用語意味
変数値や参照を入れておく入れ物
10trueなどのデータそのもの
参照オブジェクトがある場所を指す情報

たとえば、intのような値型では、変数の中に値そのものが入っていると考えるとわかりやすいです。

C#
int a = 10;

この場合、aという変数には10という値が入っています。

一方、classなどの参照型では、変数の中にはオブジェクトそのものではなく、オブジェクトを指す参照が入っています。

C#
Person person = new Person();

この場合、personという変数には、Personオブジェクトを指す参照が入っています。

ここで注意したいのは、参照型参照渡しは別の概念だということです。

参照型はデータの種類の話です。
参照渡しはメソッドに引数を渡す方法の話です。

この2つを混同すると、C#の引数の動きがわかりにくくなります。

2. C#の値渡しと参照渡しの違い

2-1. 値渡しとはコピーを渡すこと

値渡しとは、変数の中身をコピーしてメソッドに渡す方法です。

C#では、通常の引数は値渡しです。

C#
static void ChangeValue(int x)
{
x = 100;
}

int number = 10;
ChangeValue(number);

Console.WriteLine(number); // 10

このコードでは、ChangeValueメソッドの中でx100を代入しています。

しかし、呼び出し元のnumber10のままです。

なぜなら、xnumberのコピーだからです。

値渡しでは、メソッド内で引数の変数を書き換えても、呼び出し元の変数には影響しません。

2-2. 参照渡しとは変数の参照を渡すこと

参照渡しとは、変数そのものをメソッドに渡すようなイメージです。

正確には、呼び出し元の変数をメソッド側から参照できるように渡します。

C#
static void ChangeValue(ref int x)
{
x = 100;
}

int number = 10;
ChangeValue(ref number);

Console.WriteLine(number); // 100

この場合、xを変更すると、呼び出し元のnumberも変更されます。

値渡しではコピーを変更していましたが、参照渡しでは呼び出し元の変数に直接影響を与えます。

そのため、参照渡しを使うときは「このメソッドを呼ぶと元の変数が変わる可能性がある」と意識する必要があります。

2-3. 値渡しと参照渡しの動作をコードで比較

値渡しと参照渡しの違いを、同じようなコードで比較してみましょう。

まずは値渡しです。

C#
static void Increment(int x)
{
x++;
}

int value = 1;
Increment(value);

Console.WriteLine(value); // 1

Incrementメソッドの中ではx++によって値が増えています。

しかし、呼び出し元のvalueは変わりません。

次に参照渡しです。

C#
static void Increment(ref int x)
{
x++;
}

int value = 1;
Increment(ref value);

Console.WriteLine(value); // 2

今度はrefを付けているため、メソッド内での変更が呼び出し元のvalueに反映されます。

違いを表にすると、次のようになります。

渡し方メソッド内で変更呼び出し元の変数
値渡しコピーが変更される変わらない
参照渡し元の変数が変更される変わる

2-4. 呼び出し元の値が変わるケース・変わらないケース

C#でメソッドを呼び出したとき、呼び出し元の値が変わるかどうかは、引数の渡し方によって変わります。

値型の場合は特にわかりやすいです。

C#
static void SetToZero(int x)
{
x = 0;
}

int number = 10;
SetToZero(number);

Console.WriteLine(number); // 10

値渡しなので、numberは変わりません。

一方、refを使うと変わります。

C#
static void SetToZero(ref int x)
{
x = 0;
}

int number = 10;
SetToZero(ref number);

Console.WriteLine(number); // 0

呼び出し元の値が変わるケースは、主に次のとおりです。

ケース呼び出し元は変わるか
値型を値渡しする変わらない
値型をrefで渡す変わる
値型をoutで渡す変わる
参照型を値渡しして、オブジェクトの中身を変更する変わる
参照型を値渡しして、引数に別オブジェクトを再代入する変わらない
参照型をrefで渡して、別オブジェクトを再代入する変わる

特に参照型では、値渡しでもオブジェクトのプロパティ変更が呼び出し元に反映されるため、初心者が混乱しやすいです。

2-5. 値型と参照型の違いと混同しやすいポイント

C#には、大きく分けて値型参照型があります。

種類代表例特徴
値型int, double, bool, struct, enum値そのものを扱う
参照型class, string, 配列, List<T>オブジェクトへの参照を扱う

ここで大切なのは、値型・参照型の違いと、値渡し・参照渡しの違いは別だということです。

たとえば、参照型の変数をメソッドに渡す場合でも、何もキーワードを付けなければ引数は値渡しです。

C#
static void ChangeName(Person p)
{
p.Name = "佐藤";
}

この場合、pには参照のコピーが渡されています。

参照のコピーではありますが、同じオブジェクトを指しているため、プロパティを変更すると呼び出し元にも反映されます。

一方で、次のようにpに新しいオブジェクトを代入しても、呼び出し元の変数自体は変わりません。

C#
static void CreateNewPerson(Person p)
{
p = new Person { Name = "田中" };
}

この違いは、C#の参照渡しを理解するうえで非常に重要です。

3. C#で参照渡しを行う3つのキーワード

3-1. ref・out・inの違いを一覧表で比較

C#で参照渡しを行う代表的なキーワードは、refoutinの3つです。

それぞれの違いは次のとおりです。

キーワード読み取り書き換え呼び出し前の初期化メソッド内での代入主な用途
refできるできる必要任意値を受け取り、変更して返す
out原則、代入前は読めないできる不要必須メソッドから値を返す
inできるできない必要不可読み取り専用で効率よく渡す

refoutは、どちらもメソッド内から呼び出し元の変数に値を設定できます。

ただし、目的が違います。

refは「元の値を読み取り、それを変更する」ときに使います。

outは「メソッドから結果を返す」ときに使います。

inは「値を変更しないことを保証しつつ、参照渡ししたい」ときに使います。

3-2. refは読み取りと書き換えの両方を行う参照渡し

refは、メソッド内で引数の値を読み取り、必要に応じて書き換えるための参照渡しです。

C#
static void DoubleValue(ref int number)
{
number *= 2;
}

int value = 10;
DoubleValue(ref value);

Console.WriteLine(value); // 20

この例では、numberの現在値を読み取り、その値を2倍にしています。

refを使う場合、呼び出し元の変数は事前に初期化されている必要があります。

C#
int value = 10;
DoubleValue(ref value);

未初期化の変数をrefで渡すことはできません。

C#
int value;
DoubleValue(ref value); // コンパイルエラー

refは、元の値を利用して変更する処理に向いています。

3-3. outはメソッドから値を返すための参照渡し

outは、メソッドから値を返すために使う参照渡しです。

代表的な例がint.TryParseです。

C#
string text = "123";

bool success = int.TryParse(text, out int result);

Console.WriteLine(success); // True
Console.WriteLine(result); // 123

TryParseは、文字列を数値に変換できたかどうかをboolで返し、変換後の数値をout引数に入れます。

out引数は、呼び出し前に初期化されていなくても使えます。

C#
int result;
bool success = int.TryParse("123", out result);

また、C#では次のように、呼び出し時にout変数を宣言できます。

C#
bool success = int.TryParse("123", out int result);

outは、メソッドの戻り値だけでは表現しにくい結果を返したいときに便利です。

3-4. inは読み取り専用で渡す参照渡し

inは、引数を読み取り専用の参照として渡すキーワードです。

C#
static int Calculate(in int number)
{
return number * 2;
}

int value = 10;
int result = Calculate(in value);

Console.WriteLine(result); // 20

inを付けた引数は、メソッド内で変更できません。

C#
static void Change(in int number)
{
// number = 100; // コンパイルエラー
}

inは、特に大きな構造体を渡すときに役立つことがあります。

通常、値型の構造体をメソッドに渡すとコピーが発生します。構造体が大きい場合、そのコピーにコストがかかる可能性があります。

inを使うと、コピーを避けつつ、メソッド内で変更できないようにできます。

3-5. ref・out・inを使い分ける判断基準

refoutinは、次のように使い分けるとわかりやすいです。

やりたいこと使うキーワード
渡された値を読み取り、変更して返したいref
メソッドから追加の値を返したいout
値を変更せず、読み取り専用で効率よく渡したいin
ただ値を渡すだけ何も付けない

初心者のうちは、まず次のように考えるとよいです。

refは、呼び出し元の変数を書き換えたいときに使います。

outは、メソッドの結果を受け取りたいときに使います。

inは、値を書き換えたくないが、コピーを避けたいときに使います。

ただし、通常の処理では、まず戻り値を使う設計を考えるのがおすすめです。

参照渡しは便利ですが、メソッドの副作用が増えるため、使いすぎるとコードの流れが追いにくくなります。

4. refの使い方:値を変更したいときの参照渡し

4-1. refを使ったメソッド定義と呼び出し方

refを使うには、メソッド定義側と呼び出し側の両方にrefを書きます。

C#
static void Add(ref int number)
{
number += 1;
}

int value = 10;
Add(ref value);

Console.WriteLine(value); // 11

メソッド定義側では、引数の前にrefを付けます。

C#
static void Add(ref int number)

呼び出し側でも、渡す変数の前にrefを付けます。

C#
Add(ref value);

どちらか一方だけにrefを書いても、コンパイルエラーになります。

4-2. ref引数は事前に初期化が必要

ref引数として渡す変数は、呼び出し前に必ず初期化されている必要があります。

正しい例です。

C#
static void Update(ref int number)
{
number = number + 10;
}

int value = 5;
Update(ref value);

Console.WriteLine(value); // 15

valueには事前に5が代入されています。

一方、次のコードはコンパイルエラーになります。

C#
static void Update(ref int number)
{
number = number + 10;
}

int value;
Update(ref value); // コンパイルエラー

refは、メソッド内で引数の現在値を読み取る可能性があります。

そのため、呼び出し前に値が入っていない変数は渡せません。

4-3. refで数値を変更するサンプルコード

refを使って、メソッド内で数値を変更する例を見てみましょう。

C#
static void ApplyDiscount(ref int price)
{
price -= 100;
}

int itemPrice = 1000;

ApplyDiscount(ref itemPrice);

Console.WriteLine(itemPrice); // 900

このコードでは、ApplyDiscountメソッドの中でpriceから100を引いています。

priceitemPriceを参照しているため、メソッドの実行後、itemPrice900になります。

通常の値渡しでは、呼び出し元のitemPriceは変わりません。

C#
static void ApplyDiscount(int price)
{
price -= 100;
}

int itemPrice = 1000;

ApplyDiscount(itemPrice);

Console.WriteLine(itemPrice); // 1000

このように、呼び出し元の変数を変更したい場合にrefを使います。

4-4. refで変数の入れ替えを行うサンプルコード

refの代表的なサンプルとして、2つの変数の値を入れ替える処理があります。

C#
static 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); // 20
Console.WriteLine(y); // 10

Swapメソッドでは、abの値を入れ替えています。

axbyに対応しているため、メソッドの実行後、呼び出し元のxyの値も入れ替わります。

値渡しで同じことをしようとしても、呼び出し元の変数は変わりません。

C#
static 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); // 10
Console.WriteLine(y); // 20

この違いが、refによる参照渡しのわかりやすい例です。

4-5. refを使うときの注意点

refは便利ですが、使うときには注意が必要です。

まず、refを使うと、メソッドが呼び出し元の変数を変更する可能性があります。

C#
Update(ref value);

このようなコードを見たとき、valueがメソッド内で変更されるかもしれないと考える必要があります。

つまり、refを多用すると、どこで値が変わったのか追いかけにくくなります。

また、refには変数を渡す必要があります。次のように、リテラルや計算式を直接渡すことはできません。

C#
Add(ref 10);       // コンパイルエラー
Add(ref (x + y)); // コンパイルエラー

refで渡せるのは、基本的に代入可能な変数です。

C#
int value = 10;
Add(ref value); // OK

さらに、プロパティをrefに渡せないケースにも注意が必要です。

C#
person.Age = 20;
// Update(ref person.Age); // 通常のプロパティはref引数に渡せない

refは強力な機能ですが、必要な場面に限定して使うのが基本です。

5. outの使い方:複数の値を返したいとき

5-1. outを使ったメソッド定義と呼び出し方

outは、メソッドから値を返すために使います。

メソッド定義側と呼び出し側の両方にoutを書きます。

C#
static void GetSize(out int width, out int height)
{
width = 1920;
height = 1080;
}

GetSize(out int w, out int h);

Console.WriteLine(w); // 1920
Console.WriteLine(h); // 1080

この例では、GetSizeメソッドからwidthheightの2つの値を返しています。

C#のメソッドは通常、戻り値を1つだけ返します。

C#
static int GetNumber()
{
return 10;
}

しかし、outを使うと、戻り値とは別に値を返せます。

C#
static bool TryGetUserName(out string name)
{
name = "山田";
return true;
}

このように、処理が成功したかどうかを戻り値で返し、実際の結果をout引数で返す設計によく使われます。

5-2. out引数はメソッド内で必ず代入する必要がある

out引数は、メソッド内で必ず値を代入する必要があります。

次のコードは正しい例です。

C#
static void SetValue(out int number)
{
number = 100;
}

numberに必ず値を代入しているため、問題ありません。

一方、次のコードはコンパイルエラーになります。

C#
static void SetValue(out int number)
{
// numberに値を代入していない
}

outは「メソッドから値を返すための引数」です。

そのため、メソッドが終了するまでに、必ずout引数へ値を入れなければなりません。

条件分岐がある場合も注意が必要です。

C#
static bool TryGetValue(bool flag, out int number)
{
if (flag)
{
number = 100;
return true;
}

number = 0;
return false;
}

このように、どのルートを通ってもnumberに値が代入される必要があります。

次のように、片方の分岐で代入されていない場合はエラーになります。

C#
static bool TryGetValue(bool flag, out int number)
{
if (flag)
{
number = 100;
return true;
}

return false; // numberに値が入っていないためコンパイルエラー
}

5-3. TryParseで使われるoutの代表例

outの代表的な使用例が、TryParseです。

C#
string text = "123";

bool success = int.TryParse(text, out int number);

if (success)
{
Console.WriteLine($"変換成功: {number}");
}
else
{
Console.WriteLine("変換失敗");
}

int.TryParseは、文字列を整数に変換できるかを試すメソッドです。

変換に成功した場合はtrueを返し、変換結果をout引数に入れます。

変換に失敗した場合はfalseを返します。

C#
bool success = int.TryParse("abc", out int number);

Console.WriteLine(success); // False
Console.WriteLine(number); // 0

TryParseが便利なのは、例外を使わずに安全に変換できる点です。

C#
if (int.TryParse(input, out int age))
{
Console.WriteLine($"年齢は{age}歳です");
}
else
{
Console.WriteLine("数値を入力してください");
}

ユーザー入力やファイルの読み込みなど、変換に失敗する可能性がある場面でよく使われます。

5-4. out変数宣言の書き方

C#では、out引数を渡すときに、その場で変数を宣言できます。

C#
bool success = int.TryParse("123", out int result);

この書き方を使うと、事前に変数を宣言する必要がありません。

従来の書き方は次のようになります。

C#
int result;
bool success = int.TryParse("123", out result);

現在は、次のように書くことが多いです。

C#
if (int.TryParse("123", out int number))
{
Console.WriteLine(number);
}

out変数は、varを使って宣言することもできます。

C#
if (int.TryParse("123", out var number))
{
Console.WriteLine(number);
}

型が明らかな場合は、out varを使うとコードがすっきりします。

ただし、初心者のうちは型がわかりやすいように、out int numberのように明示してもよいでしょう。

5-5. outを使うべき場面と戻り値との使い分け

outは便利ですが、何でもoutで返せばよいわけではありません。

基本的には、まず通常の戻り値で返せないかを考えます。

C#
static int Add(int a, int b)
{
return a + b;
}

このように、返す値が1つだけなら戻り値を使う方が自然です。

outが向いているのは、次のような場面です。

場面
成功・失敗と結果を同時に返したいTryParse
複数の値を返したい幅と高さを返す
戻り値を別の意味に使いたいboolで成否を返す

たとえば、次のようなメソッドではoutが自然です。

C#
static bool TryDivide(int a, int b, out int result)
{
if (b == 0)
{
result = 0;
return false;
}

result = a / b;
return true;
}

呼び出し側は次のように書けます。

C#
if (TryDivide(10, 2, out int answer))
{
Console.WriteLine(answer);
}
else
{
Console.WriteLine("割り算できません");
}

ただし、返す値が多すぎる場合は、outをたくさん並べるよりも、専用のクラスや構造体、タプルを使った方が読みやすくなることがあります。

C#
static (int width, int height) GetSize()
{
return (1920, 1080);
}

outは便利な機能ですが、可読性を意識して使い分けることが大切です。

6. inの使い方:値を変更せず効率よく渡したいとき

6-1. inを使ったメソッド定義と呼び出し方

inは、読み取り専用の参照渡しを行うためのキーワードです。

メソッド定義では、引数の前にinを付けます。

C#
static int Square(in int number)
{
return number * number;
}

呼び出し側では、次のようにinを付けて呼び出せます。

C#
int value = 5;
int result = Square(in value);

Console.WriteLine(result); // 25

なお、in引数は呼び出し側のinを省略できる場合もあります。

C#
int result = Square(value);

ただし、読み取り専用の参照渡しであることを明示したい場合は、inを付けると意図が伝わりやすくなります。

refoutと同じように、inも参照渡しの一種ですが、メソッド内で値を書き換えられない点が大きな違いです。

6-2. in引数はメソッド内で変更できない

in引数は読み取り専用です。

そのため、メソッド内で値を変更しようとするとコンパイルエラーになります。

C#
static void Update(in int number)
{
// number = 100; // コンパイルエラー
}

inを使うことで、「このメソッドでは引数を変更しない」という意図を明確にできます。

たとえば、次のように読み取りだけを行う処理に向いています。

C#
static void PrintValue(in int number)
{
Console.WriteLine(number);
}

一方、呼び出し元の変数を変更したい場合は、inではなくrefを使います。

C#
static void Update(ref int number)
{
number = 100;
}

inは、値を守りながら渡したいときに使うキーワードです。

6-3. 大きな構造体を渡すときにinが役立つ理由

inが特に役立つのは、大きな構造体を引数として渡す場合です。

通常、値型である構造体をメソッドに渡すと、値のコピーが行われます。

小さな構造体なら大きな問題になりにくいですが、多くのフィールドを持つ大きな構造体では、コピーのコストが気になる場合があります。

C#
struct LargeData
{
public int A;
public int B;
public int C;
public int D;
public int E;
public int F;
}

このような構造体を値渡しすると、メソッド呼び出しのたびにコピーされます。

C#
static int Sum(LargeData data)
{
return data.A + data.B + data.C + data.D + data.E + data.F;
}

inを使うと、コピーを避けつつ、メソッド内で変更されないようにできます。

C#
static int Sum(in LargeData data)
{
return data.A + data.B + data.C + data.D + data.E + data.F;
}

ただし、inを使えば必ず速くなるわけではありません。

小さな値型では、通常の値渡しの方が単純で十分な場合も多いです。

また、構造体の設計によっては、inを使っても内部的にコピーが発生することがあります。

そのため、inは「大きな構造体を読み取り専用で渡す必要がある場合」に検討するとよいでしょう。

6-4. inを使ったサンプルコード

inを使った具体例を見てみましょう。

C#
struct Point
{
public int X;
public int Y;
}

static double GetDistanceFromOrigin(in Point point)
{
return Math.Sqrt(point.X * point.X + point.Y * point.Y);
}

Point p = new Point { X = 3, Y = 4 };

double distance = GetDistanceFromOrigin(in p);

Console.WriteLine(distance); // 5

このコードでは、Point構造体をinで渡しています。

GetDistanceFromOriginメソッドは、pointの値を読み取るだけで変更しません。

もしメソッド内で次のように変更しようとするとエラーになります。

C#
static double GetDistanceFromOrigin(in Point point)
{
// point.X = 10; // コンパイルエラー
return Math.Sqrt(point.X * point.X + point.Y * point.Y);
}

inを使うことで、メソッドが引数を変更しないことをコード上で保証できます。

6-5. inを使うときの注意点

inを使うときは、いくつか注意点があります。

まず、inは読み取り専用なので、メソッド内で引数に代入できません。

C#
static void Test(in int value)
{
// value = 10; // コンパイルエラー
}

次に、inは主に値型、特に大きな構造体で効果を検討するものです。

intboolのような小さな値型に対して、無理にinを使う必要はほとんどありません。

C#
static int Add(in int a, in int b)
{
return a + b;
}

このようなコードは動作しますが、通常は次のように書くだけで十分です。

C#
static int Add(int a, int b)
{
return a + b;
}

また、inはパフォーマンス改善を目的に使われることがありますが、実際に速くなるかどうかは状況によります。

可読性を下げてまで使うのではなく、必要な場面でだけ使うのがよいでしょう。

7. 参照型の値渡しと参照渡しの違い

7-1. classなどの参照型は何も付けなくても中身を変更できる

C#のclassは参照型です。

参照型の変数には、オブジェクトそのものではなく、オブジェクトを指す参照が入っています。

そのため、参照型をメソッドに値渡ししても、同じオブジェクトを指す参照のコピーが渡されます。

C#
class Person
{
public string Name { get; set; } = "";
}

static void ChangeName(Person person)
{
person.Name = "佐藤";
}

Person p = new Person { Name = "山田" };

ChangeName(p);

Console.WriteLine(p.Name); // 佐藤

このコードでは、ChangeNameメソッドにpを渡しています。

refは使っていません。

それでも、person.Nameを変更すると、呼び出し元のp.Nameも変わります。

これは、ppersonが同じオブジェクトを指しているからです。

7-2. 参照型を値渡しした場合の動作

参照型を値渡しした場合、渡されるのは参照のコピーです。

つまり、メソッド内の引数と呼び出し元の変数は、同じオブジェクトを指しています。

C#
static void ChangeName(Person person)
{
person.Name = "佐藤";
}

このようにオブジェクトの中身を変更すると、呼び出し元にも反映されます。

しかし、メソッド内で引数に新しいオブジェクトを代入した場合は、呼び出し元の変数は変わりません。

C#
static void CreateNewPerson(Person person)
{
person = new Person { Name = "田中" };
}

Person p = new Person { Name = "山田" };

CreateNewPerson(p);

Console.WriteLine(p.Name); // 山田

CreateNewPersonメソッド内では、personに新しいPersonオブジェクトを代入しています。

しかし、これはメソッド内のpersonという変数の向き先を変えただけです。

呼び出し元のpは、元のオブジェクトを指したままです。

7-3. 参照型にrefを付けた場合の動作

参照型にrefを付けると、呼び出し元の変数そのものを変更できます。

つまり、メソッド内で別のオブジェクトを代入すると、呼び出し元の変数も新しいオブジェクトを指すようになります。

C#
static void CreateNewPerson(ref Person person)
{
person = new Person { Name = "田中" };
}

Person p = new Person { Name = "山田" };

CreateNewPerson(ref p);

Console.WriteLine(p.Name); // 田中

この場合、person = new Person { Name = "田中" };によって、呼び出し元のpも新しいオブジェクトを指すようになります。

参照型にrefを付ける意味は、主に次のような場合にあります。

やりたいことrefが必要か
オブジェクトのプロパティを変更したい不要
オブジェクトの中身を変更したい不要
呼び出し元の変数を別のオブジェクトに差し替えたい必要

初心者が混乱しやすいのは、参照型はrefなしでも中身を変更できるという点です。

7-4. オブジェクトのプロパティ変更と再代入の違い

参照型で重要なのは、プロパティ変更再代入の違いです。

プロパティ変更は、同じオブジェクトの中身を書き換えることです。

C#
static void ChangeName(Person person)
{
person.Name = "佐藤";
}

この場合、呼び出し元にも変更が反映されます。

一方、再代入は、引数の変数が別のオブジェクトを指すようにすることです。

C#
static void ReplacePerson(Person person)
{
person = new Person { Name = "田中" };
}

値渡しの場合、再代入しても呼び出し元の変数は変わりません。

C#
Person p = new Person { Name = "山田" };

ReplacePerson(p);

Console.WriteLine(p.Name); // 山田

refを付けると、再代入も呼び出し元に反映されます。

C#
static void ReplacePerson(ref Person person)
{
person = new Person { Name = "田中" };
}

Person p = new Person { Name = "山田" };

ReplacePerson(ref p);

Console.WriteLine(p.Name); // 田中

この違いを理解すると、参照型と参照渡しの混乱がかなり減ります。

7-5. 初心者が混乱しやすい参照型と参照渡しの違い

初心者がよく混乱するのは、次のような考え方です。

「classは参照型だから、メソッドに渡すと参照渡しになるのでは?」

実際には、これは正確ではありません。

classの変数を通常の引数として渡した場合、それは参照の値渡しです。

つまり、参照そのものがコピーされて渡されます。

その結果、メソッド内の引数と呼び出し元の変数は同じオブジェクトを指します。

だから、オブジェクトの中身を変更すると呼び出し元にも反映されます。

しかし、引数に別のオブジェクトを代入しても、呼び出し元の変数は変わりません。

まとめると、次のようになります。

状況呼び出し元に反映されるか
参照型を値渡ししてプロパティを変更反映される
参照型を値渡しして新しいオブジェクトを代入反映されない
参照型をrefで渡して新しいオブジェクトを代入反映される

参照型と参照渡しは、名前が似ていますが別の概念です。

この違いを押さえておくと、C#の引数の動きが理解しやすくなります。

8. ref・out・inでよくあるコンパイルエラーと対処法

8-1. ref引数に未初期化の変数を渡している

ref引数に渡す変数は、事前に初期化されている必要があります。

次のコードはコンパイルエラーになります。

C#
static void AddTen(ref int number)
{
number += 10;
}

int value;
AddTen(ref value); // コンパイルエラー

valueに値が代入されていない状態でrefに渡しているためです。

ref引数は、メソッド内で現在の値を読み取る可能性があります。

そのため、呼び出し前に必ず値を入れておく必要があります。

修正するには、次のように初期化します。

C#
int value = 0;
AddTen(ref value);

または、初期値が意味を持つ場合は、適切な値を入れておきます。

C#
int value = 100;
AddTen(ref value);

refを使うときは、「呼び出す前に変数へ値が入っているか」を確認しましょう。

8-2. out引数にメソッド内で値を代入していない

out引数は、メソッド内で必ず値を代入する必要があります。

次のコードはコンパイルエラーになります。

C#
static bool TryGetNumber(out int number)
{
return false;
}

numberに値を代入せずにメソッドが終了しているためです。

修正するには、必ず値を代入します。

C#
static bool TryGetNumber(out int number)
{
number = 0;
return false;
}

条件分岐がある場合も、すべてのルートで代入が必要です。

C#
static bool TryGetNumber(bool success, out int number)
{
if (success)
{
number = 100;
return true;
}

number = 0;
return false;
}

outは「メソッドが値を返すための引数」なので、成功時だけでなく失敗時にも何らかの値を入れる必要があります。

8-3. 呼び出し側でref・out・inを書き忘れている

refoutを使う場合、メソッド定義側だけでなく呼び出し側にもキーワードを書く必要があります。

C#
static void Update(ref int number)
{
number = 100;
}

int value = 10;

Update(value); // コンパイルエラー

呼び出し側にもrefを付けます。

C#
Update(ref value); // OK

outも同じです。

C#
static void GetValue(out int number)
{
number = 100;
}

int value;

GetValue(value); // コンパイルエラー

正しくは次のように書きます。

C#
GetValue(out value);

または、呼び出し時に宣言します。

C#
GetValue(out int value);

inの場合は、呼び出し側のinを省略できることもあります。

C#
static void Print(in int number)
{
Console.WriteLine(number);
}

int value = 10;

Print(value); // OK
Print(in value); // OK

ただし、コードの意図を明確にしたい場合は、inを付けて呼び出すと読みやすくなることがあります。

8-4. in引数を書き換えようとしている

in引数は読み取り専用です。

そのため、メソッド内で値を変更しようとするとコンパイルエラーになります。

C#
static void Update(in int number)
{
number = 100; // コンパイルエラー
}

inを使う場合は、引数を読み取るだけにします。

C#
static void Print(in int number)
{
Console.WriteLine(number);
}

呼び出し元の変数を変更したい場合は、inではなくrefを使います。

C#
static void Update(ref int number)
{
number = 100;
}

また、outはメソッド内で値を代入するために使います。

C#
static void CreateValue(out int number)
{
number = 100;
}

inrefoutは似ていますが、書き換えできるかどうかが大きく違います。

8-5. エラーを防ぐためのチェックポイント

refoutinのエラーを防ぐには、次のポイントを確認しましょう。

チェック項目確認内容
ref引数は初期化済みか呼び出し前に値を代入しているか
out引数に値を代入しているかメソッドのすべての分岐で代入しているか
呼び出し側にキーワードを書いているかrefoutを書き忘れていないか
in引数を書き換えていないか読み取り専用として扱っているか
変数を渡しているかリテラルや計算式を渡していないか

特に初心者がよく間違えるのは、refの初期化忘れと、outの代入忘れです。

refは「すでにある値を使って変更する」ものです。

outは「メソッド内で新しく値を入れて返す」ものです。

この違いを意識すると、エラーを減らせます。

9. C#の参照渡しはいつ使うべきか

9-1. 戻り値だけでは表現しにくい値を返したいとき

参照渡しは、戻り値だけでは表現しにくい値を返したいときに役立ちます。

特にoutは、成功・失敗と結果を同時に返す場面でよく使われます。

C#
static bool TryFindUserName(int id, out string name)
{
if (id == 1)
{
name = "山田";
return true;
}

name = "";
return false;
}

呼び出し側は、次のように書けます。

C#
if (TryFindUserName(1, out string name))
{
Console.WriteLine(name);
}
else
{
Console.WriteLine("ユーザーが見つかりません");
}

このように、戻り値のboolで成功・失敗を表し、out引数で実際の結果を受け取る設計はよく使われます。

ただし、複数の値を返したいだけなら、タプルや専用クラスを使った方が読みやすい場合もあります。

C#
static (bool success, string name) TryFindUserName(int id)
{
if (id == 1)
{
return (true, "山田");
}

return (false, "");
}

outを使うか、戻り値としてまとめて返すかは、コードの読みやすさで判断しましょう。

9-2. メソッド内で呼び出し元の変数を変更したいとき

呼び出し元の変数をメソッド内で変更したい場合は、refを使います。

たとえば、値を加算する処理です。

C#
static void AddPoint(ref int score, int point)
{
score += point;
}

int totalScore = 100;

AddPoint(ref totalScore, 20);

Console.WriteLine(totalScore); // 120

refを使うと、呼び出し元のtotalScoreを直接変更できます。

また、値の入れ替えにも使えます。

C#
static void Swap(ref int a, ref int b)
{
int temp = a;
a = b;
b = temp;
}

ただし、呼び出し元の変数を変更する処理は、副作用が強くなります。

メソッド名から何をするのかが明確にわかるようにすることが大切です。

たとえば、Calculateという名前のメソッドが引数を書き換えると、呼び出し側から見ると意外な動きに感じられるかもしれません。

C#
Calculate(ref value);

このような場合は、UpdateValueNormalizeValueのように、変更することがわかる名前にするとよいでしょう。

9-3. 大きな構造体のコピーを避けたいとき

大きな構造体をメソッドに渡す場合、コピーのコストを避けるためにinを使うことがあります。

C#
struct Rectangle
{
public double Width;
public double Height;
public double X;
public double Y;
}

このような構造体を読み取り専用で使う場合、inを検討できます。

C#
static double GetArea(in Rectangle rect)
{
return rect.Width * rect.Height;
}

inを使うことで、構造体のコピーを避けつつ、メソッド内で変更できないようにできます。

ただし、すべての値型にinを使えばよいわけではありません。

intdoubleのような小さな値型は、通常の値渡しで十分です。

C#
static int Add(int a, int b)
{
return a + b;
}

inは、パフォーマンス上の理由がある場合や、読み取り専用であることを明示したい場合に使うとよいでしょう。

9-4. 参照渡しを使わない方がよいケース

参照渡しは便利ですが、使わない方がよいケースもあります。

まず、単純に値を返すだけなら、戻り値を使う方が自然です。

C#
static int Add(int a, int b)
{
return a + b;
}

これを無理にoutで書くと、かえって読みにくくなります。

C#
static void Add(int a, int b, out int result)
{
result = a + b;
}

また、refを多用すると、どのメソッドがどの変数を変更しているのか追いにくくなります。

C#
Process(ref a, ref b, ref c);

このようなコードは、呼び出し側から見ても、何がどう変更されるのかわかりにくくなりがちです。

複数の値を返したい場合は、タプルやクラスを使う方が読みやすいこともあります。

C#
class Result
{
public int Count { get; set; }
public int Total { get; set; }
}

また、参照型のオブジェクトの中身を変更するだけなら、refは不要です。

C#
static void ChangeName(Person person)
{
person.Name = "佐藤";
}

このように、参照渡しは「本当に必要な場合」に使うのが基本です。

9-5. 可読性を重視した設計の考え方

C#で参照渡しを使うときは、可読性を重視することが大切です。

プログラムは、動けばよいだけではありません。後から読んだ人が理解しやすいことも重要です。

参照渡しを使うときは、次の点を意識しましょう。

観点考え方
メソッド名引数を変更することがわかる名前にする
引数の数refoutを増やしすぎない
戻り値との使い分け1つの値なら戻り値を優先する
副作用呼び出し元の変数が変わることを意識する
必要性参照渡しでなければならない理由があるか確認する

初心者のうちは、まず通常の値渡しと戻り値を使った設計を基本にするとよいです。

そのうえで、どうしても呼び出し元の変数を変更したい場合にrefを使います。

メソッドから結果を追加で返したい場合にoutを使います。

大きな構造体を読み取り専用で効率よく渡したい場合にinを使います。

このように目的を明確にして使えば、C#の参照渡しはとても便利な機能になります。

10. C#の参照渡しに関するよくある質問

10-1. refとoutはどちらを使えばよい?

refoutは、どちらも参照渡しですが、使う目的が違います。

refは、呼び出し前に値が入っている変数をメソッドに渡し、その値を読み取ったうえで変更したいときに使います。

C#
static void AddTen(ref int number)
{
number += 10;
}

outは、メソッドから値を返したいときに使います。

C#
static bool TryGetNumber(out int number)
{
number = 100;
return true;
}

判断基準は次のとおりです。

判断ポイント使うもの
呼び出し前の値を使うref
メソッド内で新しく値を設定するout
成功・失敗と結果を返すout
元の値を更新するref

たとえば、現在の値に10を足すならrefです。

文字列を数値に変換し、結果を受け取るならoutです。

10-2. 参照型ならrefは不要?

参照型のオブジェクトの中身を変更するだけなら、基本的にrefは不要です。

C#
static void ChangeName(Person person)
{
person.Name = "佐藤";
}

このように、プロパティを変更するだけなら、通常の値渡しで呼び出し元にも反映されます。

C#
Person p = new Person { Name = "山田" };

ChangeName(p);

Console.WriteLine(p.Name); // 佐藤

しかし、呼び出し元の変数そのものを別のオブジェクトに差し替えたい場合は、refが必要です。

C#
static void ReplacePerson(ref Person person)
{
person = new Person { Name = "田中" };
}

つまり、次のように考えるとわかりやすいです。

やりたいことrefは必要か
オブジェクトの中身を変更する不要
プロパティを変更する不要
呼び出し元の変数を別オブジェクトに差し替える必要

参照型だから常にrefが必要、というわけではありません。

10-3. 参照渡しを使うと必ず処理は速くなる?

参照渡しを使っても、必ず処理が速くなるわけではありません。

たとえば、intのような小さな値型では、通常の値渡しで十分です。

C#
static int Add(int a, int b)
{
return a + b;
}

これを無理にinrefにしても、読みやすさが下がるだけの場合があります。

C#
static int Add(in int a, in int b)
{
return a + b;
}

参照渡しがパフォーマンス面で役立つ可能性があるのは、主に大きな構造体をコピーしたくない場合です。

C#
static double Calculate(in LargeStruct data)
{
// 読み取り専用で使う
}

ただし、実際に速くなるかどうかは、構造体の大きさや処理内容、実行環境によって変わります。

初心者のうちは、パフォーマンス目的で安易に参照渡しを使うよりも、まずは読みやすいコードを書くことを優先しましょう。

10-4. ref・out・inは戻り値の代わりになる?

refoutは、戻り値の代わりのように使える場合があります。

たとえば、outを使うとメソッドから値を返せます。

C#
static void GetValue(out int value)
{
value = 100;
}

しかし、返す値が1つだけなら、通常は戻り値を使う方が自然です。

C#
static int GetValue()
{
return 100;
}

outが向いているのは、戻り値を別の目的で使いたい場合です。

C#
static bool TryGetValue(out int value)
{
value = 100;
return true;
}

この例では、戻り値のboolで成功・失敗を返し、outで実際の値を返しています。

refは、戻り値の代わりというより、呼び出し元の変数を変更するために使います。

inは、戻り値の代わりではなく、読み取り専用で効率よく渡すために使います。

つまり、次のように考えるとよいです。

キーワード戻り値との関係
ref元の変数を変更する
out追加の結果を返す
in読み取り専用で渡す
戻り値基本的な結果を返す

10-5. 初心者はまずどこまで理解すればよい?

初心者がC#の参照渡しを学ぶときは、まず次のポイントを押さえれば十分です。

1つ目は、C#の引数は基本的に値渡しであることです。

C#
static void Change(int x)
{
x = 100;
}

int value = 10;
Change(value);

Console.WriteLine(value); // 10

2つ目は、refを使うと呼び出し元の変数を変更できることです。

C#
static void Change(ref int x)
{
x = 100;
}

int value = 10;
Change(ref value);

Console.WriteLine(value); // 100

3つ目は、outはメソッドから値を返すために使うことです。

C#
if (int.TryParse("123", out int number))
{
Console.WriteLine(number);
}

4つ目は、参照型と参照渡しは別の概念であることです。

C#
static void ChangeName(Person person)
{
person.Name = "佐藤";
}

参照型はrefを付けなくてもオブジェクトの中身を変更できますが、変数そのものを差し替えるにはrefが必要です。

5つ目は、inは読み取り専用の参照渡しであり、主に大きな構造体を扱うときに検討するものだということです。

初心者のうちは、まずrefoutをしっかり理解しましょう。

inは、構造体やパフォーマンスを意識する段階で学べば問題ありません。

まとめ

C#の参照渡しとは、メソッドに引数を渡すときに、呼び出し元の変数へ影響を与えられる渡し方です。

C#の引数は基本的に値渡しです。

値渡しでは、変数の値がコピーされてメソッドに渡されます。そのため、メソッド内で引数を書き換えても、呼び出し元の変数は変わりません。

C#
static void Change(int x)
{
x = 100;
}

int value = 10;
Change(value);

Console.WriteLine(value); // 10

一方、refを使うと参照渡しになり、メソッド内での変更が呼び出し元に反映されます。

C#
static void Change(ref int x)
{
x = 100;
}

int value = 10;
Change(ref value);

Console.WriteLine(value); // 100

C#で参照渡しに関係する主なキーワードは、refoutinです。

キーワード用途
ref呼び出し元の変数を読み書きしたいとき
outメソッドから値を返したいとき
in読み取り専用で効率よく渡したいとき

refは、事前に初期化された変数を渡し、メソッド内で値を変更したいときに使います。

outは、メソッドから結果を返したいときに使います。代表例はint.TryParseです。

inは、引数を変更せず、読み取り専用で渡したいときに使います。特に大きな構造体を扱う場合に役立つことがあります。

また、参照型と参照渡しは別の概念です。

classなどの参照型は、通常の値渡しでもオブジェクトの中身を変更できます。

しかし、呼び出し元の変数そのものを別のオブジェクトに差し替えたい場合は、refが必要です。

参照渡しは便利な機能ですが、使いすぎるとコードの流れがわかりにくくなります。

基本は通常の値渡しと戻り値を使い、必要な場面でrefoutinを使い分けることが大切です。