C#のrefとは?使い方・out/inとの違い・注意点を初心者向けにわかりやすく解説
はじめに
C#のrefは、メソッドに変数を渡すときに「値のコピー」ではなく「変数そのものへの参照」を渡すためのキーワードです。通常のメソッド呼び出しでは、引数として渡した値がメソッド内で変更されても、呼び出し元の変数には影響しません。しかしrefを使うと、メソッド内での変更が呼び出し元の変数にも反映されます。MicrosoftのC#リファレンスでも、refはメソッド定義とメソッド呼び出しの両方で使い、引数を参照渡しするためのキーワードとして説明されています。Microsoft Learn
この記事では、C#のrefとは何か、基本的な使い方、outやinとの違い、注意点、よくあるエラー、実務での使いどころまで、初心者にもわかりやすく解説します。
1. C#のrefとは?参照渡しの基本を初心者向けに解説
1-1. refは変数の値ではなく「参照」をメソッドに渡す仕組み
C#のrefは、メソッドに変数を渡すときに、その変数の値そのものではなく、変数が存在する場所への参照を渡す仕組みです。ここでいう「参照」とは、呼び出し元の変数をメソッド側から直接操作できるようにするイメージです。
たとえば、通常の引数渡しでは、int number = 10;をメソッドに渡しても、メソッド内でnumberに相当する値を変更しても、呼び出し元のnumberは変わりません。一方、refを使うと、メソッド内で変更した結果が呼び出し元の変数にも反映されます。
C#void AddTen(ref int value)
{
value += 10;
}
int number = 5;
AddTen(ref number);
Console.WriteLine(number); // 15
この例では、AddTenメソッドの中でvalueを変更すると、呼び出し元のnumberも変更されます。つまり、refを使うことで、メソッドが呼び出し元の変数を書き換えられるようになります。
1-2. 値渡しと参照渡しの違い
C#では、通常の引数は「値渡し」です。値渡しとは、変数の中身をコピーしてメソッドに渡す方法です。
C#void ChangeValue(int value)
{
value = 100;
}
int number = 10;
ChangeValue(number);
Console.WriteLine(number); // 10
この場合、ChangeValueメソッド内でvalueに100を代入しても、呼び出し元のnumberは10のままです。メソッドに渡されたのはnumberそのものではなく、numberの値のコピーだからです。
一方、refを付けると参照渡しになります。
C#void ChangeValue(ref int value)
{
value = 100;
}
int number = 10;
ChangeValue(ref number);
Console.WriteLine(number); // 100
この場合、メソッド内のvalueは呼び出し元のnumberと同じ変数を指すような状態になります。そのため、value = 100;と書くと、呼び出し元のnumberも100に変わります。
値渡しと参照渡しの違いは、初心者がrefを理解するうえで非常に重要です。値渡しは「コピーを渡す」、参照渡しは「元の変数を操作できるように渡す」と考えると理解しやすいでしょう。
1-3. refを使うと呼び出し元の変数を書き換えられる
refの最大の特徴は、メソッド内から呼び出し元の変数を書き換えられることです。
C#void Reset(ref int count)
{
count = 0;
}
int score = 80;
Reset(ref score);
Console.WriteLine(score); // 0
このように、Resetメソッドの中でcountを0にすると、呼び出し元のscoreも0になります。
これは便利な反面、注意も必要です。通常のメソッド呼び出しでは、引数として渡した変数が勝手に変更されることはあまり意識しません。しかしrefを使うと、メソッドの中で呼び出し元の変数が変わるため、コードを読む人が「このメソッドは引数を変更する可能性がある」と理解しておく必要があります。
そのため、refは便利だからといって多用するのではなく、「呼び出し元の変数を書き換える必要がある場合」に限定して使うのが基本です。
1-4. refが使われる主な場面
C#でrefが使われる主な場面は、次のようなケースです。
1つ目は、メソッド内で呼び出し元の変数を変更したい場合です。たとえば、数値を更新する処理や、2つの変数の値を入れ替える処理などで使われます。
2つ目は、大きな構造体をコピーせずに渡したい場合です。構造体は値型なので、通常はメソッドに渡すとコピーが発生します。サイズの大きい構造体を何度も渡す場合、refやinを使うことでコピーを避けられる場合があります。
3つ目は、配列の要素やフィールドを直接参照して操作したい場合です。後述するref returnやref localを使うと、メソッドから変数への参照を返したり、ローカル変数として参照を保持したりできます。
ただし、実務ではrefを使わなくても、戻り値、タプル、クラス設計、専用の戻り値用オブジェクトなどで解決できるケースが多くあります。初心者のうちは、まず通常の戻り値で書けないかを考え、それでも必要な場合にrefを検討するとよいでしょう。
2. C#でrefを使う基本的な書き方
2-1. メソッド定義側でrefを付ける
refを使うには、まずメソッド定義側のパラメーターにrefを付けます。
C#void UpdateValue(ref int value)
{
value = 100;
}
このように、型名の前にrefを付けます。
C#ref int value
これにより、このメソッドのvalueパラメーターは参照渡しになります。メソッド内でvalueを変更すると、呼び出し元の変数にも変更が反映されます。
2-2. メソッド呼び出し側でもrefを付ける
refは、メソッド定義側だけでなく、呼び出し側にも付ける必要があります。
C#void UpdateValue(ref int value)
{
value = 100;
}
int number = 10;
UpdateValue(ref number);
Console.WriteLine(number); // 100
呼び出し側でrefを付ける理由は、「このメソッド呼び出しによって、渡した変数が変更される可能性がある」と明示するためです。
もし呼び出し側でrefを書かずに次のように呼び出すと、コンパイルエラーになります。
C#UpdateValue(number); // エラー
C#では、メソッド定義側と呼び出し側の両方でrefを書くことで、参照渡しであることを明確にします。
2-3. ref引数には事前に初期化済みの変数が必要
ref引数に渡す変数は、メソッドを呼び出す前に必ず初期化されている必要があります。MicrosoftのC#リファレンスでも、refは呼び出し前に引数を初期化する必要があり、outは呼び出し前の初期化が不要でメソッド側が値を割り当てる必要がある、と説明されています。Microsoft Learn
C#void SetValue(ref int value)
{
value = 100;
}
int number;
SetValue(ref number); // エラー
このコードでは、numberが初期化されていないため、ref引数として渡せません。
正しくは、次のように事前に値を代入します。
C#int number = 0;
SetValue(ref number);
Console.WriteLine(number); // 100
refは、メソッドに渡す時点で「すでに存在している変数」を参照する仕組みです。そのため、未初期化の変数は渡せません。
2-4. refを使ったサンプルコード
ここでは、refを使った基本的なサンプルコードを見てみましょう。
C#using System;
class Program
{
static void DoubleValue(ref int value)
{
value *= 2;
}
static void Main()
{
int number = 10;
DoubleValue(ref number);
Console.WriteLine(number); // 20
}
}
このコードでは、DoubleValueメソッドにref int valueを指定しています。呼び出し側でもDoubleValue(ref number);と書いています。
メソッド内でvalue *= 2;を実行すると、呼び出し元のnumberが直接変更されます。そのため、出力結果は20になります。
refの基本形は次の3点を押さえれば理解しやすくなります。
メソッド定義側でrefを付ける。
呼び出し側でもrefを付ける。
渡す変数は事前に初期化しておく。
この3つが、C#でrefを使うときの基本ルールです。
2-5. refを付け忘れたときに起きるコンパイルエラー
refを使うメソッドでは、定義側と呼び出し側の両方でrefが必要です。どちらか一方だけではコンパイルエラーになります。
C#void Change(ref int value)
{
value = 50;
}
int number = 10;
Change(number); // エラー
この場合、メソッドはref intを受け取るように定義されていますが、呼び出し側でrefが付いていません。
正しくは次のように書きます。
C#Change(ref number);
反対に、メソッド定義側にrefがないのに、呼び出し側だけrefを付けてもエラーになります。
C#void Change(int value)
{
value = 50;
}
int number = 10;
Change(ref number); // エラー
refはメソッドのシグネチャの一部として扱われます。そのため、定義側と呼び出し側で一致している必要があります。
3. refを使った具体例
3-1. 数値をメソッド内で変更する例
まずは、数値をメソッド内で変更する例です。
C#void AddPoint(ref int score)
{
score += 10;
}
int playerScore = 70;
AddPoint(ref playerScore);
Console.WriteLine(playerScore); // 80
この例では、AddPointメソッド内でscore += 10;を実行しています。scoreはrefで渡されているため、呼び出し元のplayerScoreも変更されます。
通常の値渡しでは、メソッド内でscoreを変更してもplayerScoreには影響しません。しかしrefを使うことで、メソッドを通じて元の変数を更新できます。
3-2. 複数の値を入れ替える例
refの代表的な例として、2つの値を入れ替える処理があります。
C#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メソッドでは、aとbの値を入れ替えています。aとbはそれぞれ呼び出し元のxとyを参照しているため、メソッドの実行後にxとyの値が入れ替わります。
このような処理は、refの動きを理解するための典型的なサンプルです。
3-3. 構造体をrefで渡す例
構造体は値型です。そのため、通常の引数として渡すとコピーが発生します。refを使うと、構造体をコピーせずに元の変数を直接操作できます。
C#struct Position
{
public int X;
public int Y;
}
void Move(ref Position position)
{
position.X += 10;
position.Y += 20;
}
Position pos = new Position { X = 1, Y = 2 };
Move(ref pos);
Console.WriteLine(pos.X); // 11
Console.WriteLine(pos.Y); // 22
この例では、Position構造体をrefで渡しています。Moveメソッド内でposition.Xとposition.Yを変更すると、呼び出し元のposにも変更が反映されます。
構造体が大きい場合、コピーを避ける目的でrefが使われることがあります。ただし、読み取りだけでよい場合は、refよりもinの方が適していることがあります。
3-4. 配列・クラス型でrefを使うときの挙動
配列やクラスは参照型です。そのため、通常の値渡しでも、オブジェクトの中身を変更することはできます。
C#void ChangeArrayElement(int[] numbers)
{
numbers[0] = 999;
}
int[] values = { 1, 2, 3 };
ChangeArrayElement(values);
Console.WriteLine(values[0]); // 999
この例ではrefを使っていませんが、配列の要素は変更されています。なぜなら、配列変数の中には配列オブジェクトへの参照が入っており、その参照のコピーがメソッドに渡されるからです。
では、参照型にrefを使うと何が変わるのでしょうか。refを使うと、配列変数やクラス型変数そのものを別のオブジェクトに差し替えることができます。
C#void ReplaceArray(ref int[] numbers)
{
numbers = new int[] { 10, 20, 30 };
}
int[] values = { 1, 2, 3 };
ReplaceArray(ref values);
Console.WriteLine(values[0]); // 10
refなしの場合、メソッド内でnumbers = new int[] { 10, 20, 30 };としても、呼び出し元のvaluesは差し替わりません。しかしrefを使うと、呼び出し元の変数自体が新しい配列を指すようになります。
つまり、参照型におけるrefは「オブジェクトの中身を変更するため」ではなく、「呼び出し元の変数が指す参照先を変更するため」に使うことが多いです。
3-5. 実務でrefが役立つケース
実務でrefが役立つケースは限られていますが、いくつか代表的な場面があります。
たとえば、ゲーム開発や数値計算などで、大きな構造体を頻繁に扱う場合です。座標、ベクトル、行列、色、物理演算用のデータなどを大量に処理する場合、コピーを避けるためにrefが使われることがあります。
また、パフォーマンスを重視するライブラリや低レベルな処理では、配列要素や構造体のフィールドを直接参照して更新するためにrefが使われる場合があります。
ただし、一般的な業務アプリケーションでは、refを使う場面はそれほど多くありません。メソッドの戻り値、クラスのプロパティ、タプル、専用の結果オブジェクトなどで表現した方が、読みやすく保守しやすいことが多いです。
4. ref・out・inの違い
4-1. refとoutの違い
refとoutはどちらも参照渡しに関係するキーワードです。しかし、使い方には大きな違いがあります。
refは、呼び出し前に変数を初期化しておく必要があります。そして、メソッド内でその値を読み取ることも、書き換えることもできます。
C#void Add(ref int value)
{
value += 10;
}
int number = 5;
Add(ref number);
一方、outは、呼び出し前に変数を初期化しておく必要がありません。その代わり、メソッド側では必ず値を代入する必要があります。
C#void CreateNumber(out int value)
{
value = 100;
}
int number;
CreateNumber(out number);
Console.WriteLine(number); // 100
refは「すでにある値を受け取り、必要に応じて変更する」ときに使います。outは「メソッドから値を返すための入れ物として使う」ときに適しています。
4-2. refとinの違い
inも参照渡しに関係するキーワードですが、refとは目的が異なります。
refは、メソッド内で引数を書き換えることができます。
C#void Update(ref int value)
{
value = 100;
}
一方、inは読み取り専用の参照渡しです。メソッド内で引数を書き換えることはできません。
C#void Print(in int value)
{
Console.WriteLine(value);
// value = 100; // エラー
}
inは、主に大きな構造体をコピーせずに渡したいが、メソッド内で変更されたくない場合に使います。MicrosoftのC#リファレンスでも、inは参照で引数を渡す意図を示し、inに渡す引数は直接参照できる場所を表す必要があると説明されています。Microsoft Learn
4-3. outは戻り値以外で値を返したいときに使う
outは、メソッドから複数の値を返したいときによく使われます。
代表的な例がint.TryParseです。
C#string text = "123";
bool success = int.TryParse(text, out int result);
if (success)
{
Console.WriteLine(result); // 123
}
TryParseは、変換に成功したかどうかをboolで返し、変換結果をout引数に入れます。このように、戻り値とは別に結果を返したい場合にoutが使われます。
ただし、最近のC#ではタプルを使って複数の値を返すこともできます。
C#(bool success, int value) ParseNumber(string text)
{
bool success = int.TryParse(text, out int result);
return (success, result);
}
outは便利ですが、必ずしもすべての場面で必要ではありません。APIの設計や可読性を考えて使い分けることが大切です。
4-4. inは読み取り専用の参照渡しに使う
inは、引数をコピーせずに渡しつつ、メソッド内では変更できないようにしたい場合に使います。
C#struct LargeData
{
public int A;
public int B;
public int C;
public int D;
}
void PrintData(in LargeData data)
{
Console.WriteLine(data.A);
Console.WriteLine(data.B);
// data.A = 10; // エラー
}
この例では、LargeDataをinで渡しています。inを使うことで、構造体のコピーを避けながら、メソッド内で値が変更されないことを示せます。
refは「変更する可能性がある参照渡し」、inは「変更しない参照渡し」と考えるとわかりやすいです。
4-5. ref・out・inの使い分け早見表
ref、out、inの違いを整理すると、次のようになります。
| キーワード | 呼び出し前の初期化 | メソッド内で読み取り | メソッド内で代入 | 主な用途 |
|---|---|---|---|---|
ref | 必要 | できる | できる | 既存の値を変更したい |
out | 不要 | 代入前はできない | 必須 | メソッドから値を返したい |
in | 必要 | できる | できない | コピーを避けつつ読み取り専用にしたい |
refは、呼び出し元の値を変更する目的で使います。
outは、メソッドから追加の値を返す目的で使います。
inは、大きな値型をコピーせず、読み取り専用で渡す目的で使います。
この3つは似ていますが、意図が異なります。コードを読む人に対して「この引数は変更されるのか」「値を受け取るためのものなのか」「読み取り専用なのか」を明確に伝える役割もあります。
4-6. どれを使うべきか判断するポイント
どれを使うべきか迷ったときは、まず「メソッドが引数を変更する必要があるか」を考えます。
呼び出し元の変数を変更したいならrefを検討します。
メソッドから値を返すための引数が必要ならoutを検討します。
大きな構造体をコピーせずに読み取り専用で渡したいならinを検討します。
ただし、初心者のうちは、まず通常の戻り値で書けないかを考えることが大切です。たとえば、次のように戻り値で表現できるなら、refを使わない方がわかりやすい場合があります。
C#int AddTen(int value)
{
return value + 10;
}
int number = 5;
number = AddTen(number);
このコードは、refを使った次のコードよりも意図が明確です。
C#void AddTen(ref int value)
{
value += 10;
}
int number = 5;
AddTen(ref number);
refは便利ですが、使うとメソッドの副作用が増えます。可読性を優先するなら、戻り値で表現できる処理は戻り値で書くのがおすすめです。
5. refを使うときの注意点
5-1. 可読性が下がりやすい
refを使うと、メソッドの中で呼び出し元の変数が変更される可能性があります。そのため、コードを読む人は、メソッド呼び出しを見ただけで「この変数はこのあと変わるかもしれない」と意識しなければなりません。
C#UpdateUserStatus(ref status);
このようなコードでは、statusがメソッド内で変更されることがわかります。しかし、どのように変更されるのかはメソッドの中身を見ないとわかりません。
戻り値を使えば、変更の流れがより明確になる場合があります。
C#status = UpdateUserStatus(status);
この書き方なら、statusに新しい値を代入していることが一目でわかります。
refを使うときは、処理の意図が明確か、呼び出し元の変数が変更されることが自然かを考えることが大切です。
5-2. 呼び出し元の値が変わるため副作用に注意する
refは呼び出し元の変数を変更できます。これは便利ですが、副作用の原因にもなります。
C#void Discount(ref int price)
{
price -= 100;
}
int productPrice = 1000;
Discount(ref productPrice);
Console.WriteLine(productPrice); // 900
この例では意図的に価格を変更しているため問題ありません。しかし、コードが大きくなると、どのメソッドがどの変数を変更しているのか把握しづらくなることがあります。
副作用が多いコードは、バグの原因になります。特に、同じ変数を複数のメソッドにrefで渡している場合、どこで値が変わったのか追跡しにくくなります。
refを使う場合は、メソッド名やコメントで「引数を変更する」ことがわかるようにしておくとよいでしょう。
5-3. 初期化していない変数はrefに渡せない
refに渡す変数は、事前に初期化されている必要があります。
C#void SetDefault(ref int value)
{
value = 1;
}
int number;
SetDefault(ref number); // エラー
このコードはコンパイルエラーになります。numberに値が代入されていないためです。
正しくは次のように書きます。
C#int number = 0;
SetDefault(ref number);
refは、既存の変数をメソッドに渡して、その変数を読み取ったり変更したりする仕組みです。未初期化の変数を渡したい場合は、refではなくoutの方が適しています。
C#void SetDefault(out int value)
{
value = 1;
}
int number;
SetDefault(out number);
Console.WriteLine(number); // 1
5-4. プロパティや一時的な値はref引数に渡せない場合がある
ref引数には、変数として直接参照できるものを渡す必要があります。そのため、通常のプロパティや計算結果、一時的な値はref引数に渡せません。MicrosoftのC#リファレンスでも、in、out、ref引数に関する一般ルールとして、定数、通常のプロパティ、値を生成する式は使えないと説明されています。Microsoft Learn
たとえば、次のようなコードはエラーになります。
C#class User
{
public int Age { get; set; }
}
void Increment(ref int value)
{
value++;
}
User user = new User { Age = 20 };
Increment(ref user.Age); // エラー
user.Ageはプロパティです。プロパティは見た目はフィールドのように見えますが、実際にはgetやsetメソッドを通じてアクセスされます。そのため、通常はref引数として渡せません。
一時的な値も渡せません。
C#Increment(ref 10); // エラー
Increment(ref (1 + 2)); // エラー
refに渡せるのは、基本的に変数、配列要素、フィールドなど、実際に格納場所を持つものです。
5-5. 使いすぎるとバグの原因になる
refを多用すると、プログラムの状態がどこで変更されているのか追いにくくなります。
C#UpdateA(ref value);
UpdateB(ref value);
UpdateC(ref value);
このようなコードでは、valueがどのメソッドでどのように変更されたのかを追跡する必要があります。処理が単純なうちは問題ありませんが、コードが複雑になるとバグの原因になります。
特に、複数人で開発するプロジェクトでは、refを使ったメソッドは注意して設計する必要があります。引数を変更するメソッドは、呼び出す側にも影響を与えるため、想定外の変更が起きやすくなります。
refは「使えるから使う」のではなく、「呼び出し元の変数を直接変更する設計が本当に必要か」を考えて使いましょう。
5-6. 通常は戻り値やタプルで代替できないか検討する
多くの場合、refを使わなくても戻り値で代替できます。
C#int Normalize(int value)
{
if (value < 0)
{
return 0;
}
return value;
}
int score = -10;
score = Normalize(score);
複数の値を返したい場合は、タプルを使う方法もあります。
C#(int min, int max) GetMinMax(int a, int b)
{
if (a < b)
{
return (a, b);
}
return (b, a);
}
var result = GetMinMax(10, 3);
Console.WriteLine(result.min); // 3
Console.WriteLine(result.max); // 10
refを使うと、メソッドの中で引数が変更されます。一方、戻り値やタプルを使うと、データの流れが明確になります。
初心者のうちは、まず戻り値で書けないかを考え、それでも必要な場合にrefを使うのが安全です。
6. ref戻り値・refローカル変数とは
6-1. ref returnの基本
refは引数だけでなく、戻り値にも使えます。これをref return、または参照戻り値と呼びます。MicrosoftのC#リファレンスでは、参照戻り値はメソッドが値ではなく変数への参照を返す仕組みであり、呼び出し元は返された変数を値としても参照としても扱えると説明されています。Microsoft Learn
通常の戻り値は値のコピーを返します。
C#int GetFirst(int[] numbers)
{
return numbers[0];
}
一方、ref returnを使うと、配列要素などへの参照を返せます。
C#ref int GetFirst(ref int[] numbers)
{
return ref numbers[0];
}
より一般的には、次のように書きます。
C#ref int FindFirst(int[] numbers)
{
return ref numbers[0];
}
このメソッドは、numbers[0]の値ではなく、numbers[0]そのものへの参照を返します。
6-2. ref localの基本
ref localは、参照を受け取るローカル変数です。
C#ref int first = ref FindFirst(numbers);
このように、refで返された参照を、ローカル変数として保持できます。
C#int[] numbers = { 1, 2, 3 };
ref int first = ref FindFirst(numbers);
first = 100;
Console.WriteLine(numbers[0]); // 100
firstはnumbers[0]への参照です。そのため、first = 100;と書くと、numbers[0]も100に変わります。
通常のローカル変数では、配列要素の値がコピーされます。
C#int first = numbers[0];
first = 100;
Console.WriteLine(numbers[0]); // 1
一方、ref localでは参照を保持するため、元の要素を直接変更できます。
6-3. 配列要素やフィールドを直接参照する例
ref returnとref localを使うと、配列要素を直接参照できます。
C#ref int GetElement(int[] numbers, int index)
{
return ref numbers[index];
}
int[] scores = { 70, 80, 90 };
ref int target = ref GetElement(scores, 1);
target = 100;
Console.WriteLine(scores[1]); // 100
この例では、GetElementメソッドがscores[1]への参照を返しています。targetはその参照を受け取っているため、target = 100;とするとscores[1]が100になります。
この仕組みは、配列や構造体の要素を効率よく扱いたい場合に便利です。ただし、コードの動きがわかりにくくなることもあるため、通常のアプリケーションコードでは慎重に使うべきです。
6-4. ref戻り値を使うメリット
ref戻り値のメリットは、値のコピーを避けながら、元のデータを直接参照できることです。
たとえば、大きな構造体の一部を取得して変更する場合、通常の戻り値ではコピーが発生することがあります。ref returnを使うと、元のデータへの参照を返せるため、コピーを避けられる場合があります。
また、配列やバッファのようなデータ構造を扱う場合、特定の要素への参照を返すことで、効率的に更新できることがあります。
ただし、ref returnは高度な機能です。使い方を誤ると、どの変数がどのデータを参照しているのか分かりにくくなります。初心者が最初に覚えるべきなのは、まず通常のref引数の使い方です。
6-5. ref戻り値を使う際の注意点
ref returnでは、参照として返せるものに制限があります。たとえば、メソッド内で作成したローカル変数への参照を返すことはできません。
C#ref int Invalid()
{
int value = 10;
return ref value; // エラー
}
このコードが許されないのは、valueがメソッド終了時に使えなくなるローカル変数だからです。メソッド終了後に存在しない変数への参照を返すと危険なため、C#コンパイラが防いでいます。
ref returnを使う場合は、返す参照の有効期間に注意する必要があります。配列要素、フィールド、呼び出し元から渡された参照など、有効期間が十分にあるものを返す必要があります。
7. refとパフォーマンスの関係
7-1. 大きな構造体をコピーせずに渡せる
refは、パフォーマンス改善のために使われることがあります。特に、大きな構造体をメソッドに渡す場合、通常の値渡しでは構造体全体がコピーされます。
C#struct LargeStruct
{
public int A;
public int B;
public int C;
public int D;
public int E;
public int F;
}
このような構造体を何度もメソッドに渡す場合、コピーのコストが気になることがあります。
C#void Process(ref LargeStruct data)
{
data.A += 1;
}
refを使うと、構造体をコピーせずに参照で渡せます。そのため、大きな値型を頻繁に処理する場面では、パフォーマンス面で有利になる場合があります。
7-2. パフォーマンス改善につながるケース
refがパフォーマンス改善につながる可能性があるのは、主に次のようなケースです。
大きな構造体を何度もメソッドに渡す場合。
大量のデータを扱う処理で、コピーを避けたい場合。
ゲーム開発、画像処理、数値計算、物理演算など、処理回数が非常に多い場合。
配列やバッファの要素を直接操作したい場合。
たとえば、座標やベクトルを表す構造体を大量に処理する場合、値のコピーを避けることで効率がよくなることがあります。
ただし、実際に速くなるかどうかは、構造体のサイズ、処理回数、JITコンパイラの最適化、実行環境によって変わります。パフォーマンス目的でrefを使う場合は、必ず計測することが大切です。
7-3. 小さな値型では効果が少ない場合がある
int、double、boolのような小さな値型では、refを使っても大きなパフォーマンス改善は期待できないことが多いです。
C#void Add(ref int value)
{
value++;
}
このような小さな値型の場合、値をコピーするコストは非常に小さいため、refを使うメリットは限定的です。むしろ、refを使うことでコードの可読性が下がる可能性があります。
たとえば、次のように戻り値で書いた方がわかりやすい場合があります。
C#int Add(int value)
{
return value + 1;
}
refは、パフォーマンスのために常に使うべきものではありません。小さな値型では、可読性を優先した方がよいケースが多いです。
7-4. パフォーマンス目的だけで安易に使わない
refはパフォーマンス改善に役立つ場合がありますが、安易に使うべきではありません。
理由は、refを使うとコードの意味が複雑になるからです。メソッド内で引数が変更される可能性があり、データの流れを追いにくくなります。
また、現代のC#や.NETでは、JITコンパイラによる最適化が行われます。そのため、開発者が手動でrefを使わなくても十分に高速な場合があります。
パフォーマンスを理由にrefを使うなら、実際にベンチマークを取り、改善効果があることを確認しましょう。体感や推測だけでrefを増やすと、保守性が下がるだけになることがあります。
7-5. まずは可読性と保守性を優先する
初心者がC#でコードを書くときは、まず可読性と保守性を優先するのがおすすめです。
refを使ったコードは、慣れていない人にとって読みにくく感じられることがあります。特に、引数がメソッド内で変更される処理は、意図が明確でないとバグにつながります。
次のように戻り値で書ける処理は、戻り値で書いた方が自然です。
C#int Increase(int value)
{
return value + 1;
}
int count = 10;
count = Increase(count);
一方、2つの値を入れ替える、既存の変数を明確に更新する、大きな構造体のコピーを避けるなど、refを使う理由がはっきりしている場合は、refを使う価値があります。
refは「必要なときに使う道具」です。読みやすさを犠牲にしてまで使うものではありません。
8. refに関するよくあるエラーと対処法
8-1. 「refキーワードが必要です」というエラー
refを使うメソッドでは、呼び出し側にもrefが必要です。
C#void Update(ref int value)
{
value = 100;
}
int number = 10;
Update(number); // エラー
この場合、メソッド定義ではref int valueとなっているのに、呼び出し側でrefを付けていません。
正しくは次のように書きます。
C#Update(ref number);
このエラーは、初心者がよく遭遇します。refは定義側と呼び出し側の両方に必要だと覚えておきましょう。
8-2. 「変数は使用前に割り当てる必要があります」というエラー
refに渡す変数は、事前に初期化されている必要があります。
C#void SetValue(ref int value)
{
value = 10;
}
int number;
SetValue(ref number); // エラー
このコードでは、numberに値が代入されていないため、ref引数として渡せません。
対処法は、呼び出し前に初期化することです。
C#int number = 0;
SetValue(ref number);
もし「メソッド側で必ず値を設定する」目的なら、refではなくoutを使うことも検討しましょう。
C#void SetValue(out int value)
{
value = 10;
}
int number;
SetValue(out number);
refは初期化済みの変数を渡すもの、outはメソッドから値を受け取るもの、と覚えると判断しやすくなります。
8-3. プロパティをref引数に渡せないエラー
通常のプロパティはref引数に渡せません。
C#class Product
{
public int Price { get; set; }
}
void Discount(ref int price)
{
price -= 100;
}
Product product = new Product { Price = 1000 };
Discount(ref product.Price); // エラー
プロパティはフィールドのように見えますが、実際にはメソッド呼び出しに近い仕組みです。そのため、通常はrefで直接渡せません。
対処法としては、一度ローカル変数に取り出し、処理後にプロパティへ戻します。
C#int price = product.Price;
Discount(ref price);
product.Price = price;
または、メソッドを戻り値形式に変更する方法もあります。
C#int Discount(int price)
{
return price - 100;
}
product.Price = Discount(product.Price);
後者の方が、可読性が高くなることが多いです。
8-4. 非同期メソッドやイテレーターでrefが使えないケース
asyncメソッドでは、ref、in、outパラメーターを使えません。MicrosoftのC#コンパイラメッセージのリファレンスでも、CS1988として「asyncメソッドはref、in、outパラメーターを持てない」と説明されています。Microsoft Learn
C#async Task UpdateAsync(ref int value) // エラー
{
await Task.Delay(100);
value = 10;
}
このようなコードはコンパイルできません。
非同期処理で値を返したい場合は、戻り値を使います。
C#async Task<int> UpdateAsync(int value)
{
await Task.Delay(100);
return value + 10;
}
複数の値を返したい場合は、タプルや専用クラスを使うとよいでしょう。
C#async Task<(bool success, int value)> GetValueAsync()
{
await Task.Delay(100);
return (true, 10);
}
イテレーターでも、refやoutの使用には制限があります。yield returnを使うメソッドでは、通常の戻り値設計を使う方が安全です。
8-5. エラーを防ぐための確認ポイント
refに関するエラーを防ぐには、次の点を確認しましょう。
メソッド定義側にrefが付いているか。
呼び出し側にもrefが付いているか。
渡す変数は初期化済みか。
プロパティや一時的な値を渡していないか。
asyncメソッドで使おうとしていないか。
本当にrefが必要な設計か。
特に初心者は、refを使う前に「戻り値で書けないか」を確認するのがおすすめです。戻り値で書けるなら、その方がエラーも少なく、コードの意図も伝わりやすくなります。
9. refを使うべきケース・使わない方がよいケース
9-1. refを使うべきケース
refを使うべきケースは、呼び出し元の変数をメソッド内で直接変更する必要がある場合です。
たとえば、2つの値を入れ替える処理ではrefが自然です。
C#void Swap(ref int a, ref int b)
{
int temp = a;
a = b;
b = temp;
}
また、大きな構造体をコピーせずに渡し、かつメソッド内で変更する必要がある場合にもrefが役立ちます。
C#void Move(ref Position position)
{
position.X += 1;
position.Y += 1;
}
さらに、低レベルなライブラリやパフォーマンス重視の処理では、ref戻り値やref localを使って配列要素やフィールドを直接操作することがあります。
ただし、これらは目的が明確な場合に限ります。通常の業務コードでは、refを使わなくても解決できるケースが多いです。
9-2. outを使った方がよいケース
outを使った方がよいのは、メソッドから値を返したいが、戻り値だけでは足りない場合です。
代表例は、処理が成功したかどうかと、結果の値を同時に返すケースです。
C#bool TryGetValue(string key, out int value)
{
if (key == "A")
{
value = 100;
return true;
}
value = 0;
return false;
}
このようなTry...パターンでは、outがよく使われます。
refとの違いは、outは呼び出し前の初期化が不要で、メソッド側が必ず値を代入する点です。既存の値を変更する目的ならref、メソッドから値を受け取る目的ならoutを使うと考えるとわかりやすいです。
9-3. inを使った方がよいケース
inを使った方がよいのは、大きな構造体をコピーせずに渡したいが、メソッド内で変更したくない場合です。
C#void Draw(in Rectangle rect)
{
Console.WriteLine(rect.Width);
Console.WriteLine(rect.Height);
}
inを使うと、参照渡しでありながら読み取り専用になります。そのため、メソッド内で誤って値を変更することを防げます。
パフォーマンス目的で大きな値型を渡す場合でも、変更が不要ならrefではなくinを検討しましょう。
refは「変更できる参照渡し」、inは「変更できない参照渡し」です。意図を明確にするためにも、読み取り専用でよいならinの方が適しています。
9-4. 戻り値・タプル・クラス設計で代替できるケース
refを使わなくても、戻り値やタプル、クラス設計で代替できるケースは多くあります。
1つの値を返すだけなら、通常の戻り値で十分です。
C#int CalculateTaxIncludedPrice(int price)
{
return (int)(price * 1.1);
}
複数の値を返したい場合は、タプルを使えます。
C#(int width, int height) GetSize()
{
return (1920, 1080);
}
より意味のあるデータを返したい場合は、専用のクラスやレコードを作る方法もあります。
C#record Result(bool Success, int Value, string Message);
Result Execute()
{
return new Result(true, 100, "成功しました");
}
このような設計にすると、データの流れが明確になり、refによる副作用を避けられます。
refを使う前に、戻り値で表現できないか、タプルで十分ではないか、クラス設計を見直せないかを考えることが大切です。
9-5. 初心者がrefを使うときの判断基準
初心者がrefを使うかどうか判断するときは、次のように考えるとよいでしょう。
まず、メソッド内で呼び出し元の変数を変更する必要が本当にあるかを確認します。変更する必要がないなら、refは不要です。
次に、戻り値で書けないかを考えます。戻り値で自然に書けるなら、戻り値を使った方が読みやすくなります。
複数の値を返したいだけなら、outやタプルを検討します。
大きな構造体を読み取り専用で渡したいだけなら、inを検討します。
それでも、呼び出し元の変数を直接変更する必要がある場合にrefを使いましょう。
初心者にとって大切なのは、refの書き方を覚えることだけではありません。refを使うとコードの意味がどう変わるのか、呼び出し元にどんな影響があるのかを理解することです。
まとめ
C#のrefは、メソッドに変数を参照渡しするためのキーワードです。通常の値渡しでは、メソッド内で引数を変更しても呼び出し元の変数は変わりません。しかしrefを使うと、メソッド内での変更が呼び出し元の変数にも反映されます。
refを使うには、メソッド定義側と呼び出し側の両方にrefを付ける必要があります。また、ref引数に渡す変数は、呼び出し前に初期化されていなければなりません。
refと似たキーワードにoutとinがあります。refは既存の値を読み書きしたいとき、outはメソッドから値を返したいとき、inはコピーを避けながら読み取り専用で渡したいときに使います。
一方で、refは呼び出し元の変数を変更するため、副作用が発生しやすく、可読性が下がることがあります。通常は、戻り値、タプル、クラス設計などで代替できないかを先に考えることが大切です。
refは、正しく使えば便利で強力な機能です。しかし、初心者のうちは多用せず、「呼び出し元の変数を直接変更する必要がある場合」「大きな構造体のコピーを避けたい場合」など、目的が明確なときに使うようにしましょう。

