C#リファレンスとは?値渡し・参照渡し・refの違いと使い方を初心者向けに徹底解説

はじめに

C#を学び始めると、「リファレンス」「参照」「値渡し」「参照渡し」「ref」など、似た言葉がたくさん出てきます。

特に初心者が混乱しやすいのは、「参照型」と「参照渡し」は同じ意味なのか、「ref」を付けると何が変わるのか、という点です。

結論からいうと、C#の引数は通常「値渡し」です。たとえクラスなどの参照型をメソッドに渡しても、基本的には「参照そのものを値として渡している」と考える必要があります。一方で、refを使うと、呼び出し元の変数そのものをメソッド側から書き換えられるようになります。

この記事では、C#リファレンスという言葉の意味から、値渡し・参照渡し・refoutinの違いまで、初心者にもわかりやすく順番に解説します。

1. C#リファレンスとは?初心者が最初に押さえるべき意味

C#で「リファレンス」という言葉を見かけたときは、文脈によって意味が変わります。まずは、どの意味で使われているのかを整理しておきましょう。

1-1. C#で使われる「リファレンス」の主な意味

C#における「リファレンス」は、主に次のような意味で使われます。

1つ目は、公式ドキュメントとしての「C#リファレンス」です。これは、C#の構文、キーワード、演算子、型、機能などを調べるための資料を指します。

2つ目は、プログラム上の「参照」です。たとえば、クラスのインスタンスを変数に代入したとき、その変数にはオブジェクト本体ではなく、オブジェクトを指し示す参照が入っていると考えます。

3つ目は、引数の渡し方としての「参照渡し」です。これは、メソッドに変数を渡すとき、呼び出し元の変数自体をメソッド内から変更できる渡し方です。C#では主にrefキーワードを使って実現します。

このように、同じ「リファレンス」という言葉でも、ドキュメントの意味なのか、オブジェクトへの参照なのか、引数の渡し方なのかによって内容が異なります。

1-2. 公式ドキュメントの「C#リファレンス」とプログラム上の「参照」の違い

「C#リファレンス」と聞くと、Microsoftの公式ドキュメントにあるC#言語の説明を指す場合があります。この場合のリファレンスは、辞書や仕様書のような意味です。

一方、プログラム上で使う「参照」は、メモリ上にあるオブジェクトを指し示す情報のことです。

たとえば、次のようなコードを考えます。

C#
Person person = new Person();

このコードでは、new Person()によって作られたオブジェクトがメモリ上に存在し、person変数はそのオブジェクトを参照します。

つまり、公式ドキュメントとしての「リファレンス」と、プログラム上の「参照」は別の意味です。C#を学ぶときは、この2つを混同しないことが大切です。

1-3. この記事で解説する「値渡し・参照渡し・ref」の全体像

この記事では、主にプログラム上の引数の渡し方に注目します。

C#でメソッドに値を渡す方法には、大きく分けて次の考え方があります。

通常の引数は「値渡し」です。値渡しでは、変数の中身がコピーされてメソッドに渡されます。

refを使うと「参照渡し」になります。参照渡しでは、呼び出し元の変数そのものをメソッド内から変更できます。

また、outinも引数の渡し方に関係するキーワードです。outはメソッドから結果を返すために使われ、inは読み取り専用で渡すために使われます。

1-4. 初心者が混同しやすいポイント

初心者が特に混同しやすいのは、次の3つです。

1つ目は、「参照型」と「参照渡し」を同じものだと思ってしまうことです。参照型は型の分類であり、参照渡しは引数の渡し方です。

2つ目は、クラスをメソッドに渡すと自動的に参照渡しになると思ってしまうことです。C#では、参照型を渡しても通常は値渡しです。ただし、渡される値が「オブジェクトへの参照」であるため、オブジェクトの中身は変更できます。

3つ目は、refを付ければ何でも自由に渡せると思ってしまうことです。refには、変数が初期化済みであること、呼び出し側にもrefを書くこと、プロパティや式は直接渡せないことなど、いくつかのルールがあります。

2. C#の値渡しとは?基本の仕組みをわかりやすく解説

C#を理解するうえで、まず押さえるべきなのが「値渡し」です。C#の引数は、何も指定しなければ基本的に値渡しになります。

2-1. 値渡しとは何か

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

たとえば、次のようなメソッドがあるとします。

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

このメソッドに変数を渡しても、メソッド内で変更されるのはコピーされた値です。呼び出し元の変数そのものは変更されません。

C#
int x = 10;
ChangeNumber(x);

Console.WriteLine(x);

実行結果は次のようになります。

C#
10

ChangeNumberメソッドの中ではnumber100を代入していますが、呼び出し元のx10のままです。

2-2. 値渡しではメソッド内の変更が呼び出し元に反映されない理由

値渡しでは、呼び出し元の変数とメソッド内の引数は別物です。

先ほどの例では、xの値である10がコピーされ、そのコピーがnumberに渡されます。メソッド内でnumberを変更しても、xとは別の変数を変更しているだけなので、呼び出し元には影響しません。

イメージとしては、原本を渡すのではなく、コピーを渡している状態です。コピーに書き込みをしても、原本は変わりません。

2-3. intやboolなど値型を値渡しする例

intboolなどの値型を値渡しすると、メソッド内の変更は呼び出し元に反映されません。

C#
static void ChangeValues(int count, bool isActive)
{
count = 999;
isActive = false;
}

int count = 10;
bool isActive = true;

ChangeValues(count, isActive);

Console.WriteLine(count);
Console.WriteLine(isActive);

実行結果は次のとおりです。

C#
10
True

メソッド内ではcount999isActivefalseを代入しています。しかし、呼び出し元のcountisActiveは変更されません。

2-4. 値渡しを使う場面

値渡しは、C#で最も基本的な引数の渡し方です。通常は値渡しを使えば問題ありません。

たとえば、計算に使うだけの値、条件判定に使う値、メソッド内で一時的に利用する値などは、値渡しで十分です。

C#
static int AddTax(int price)
{
return (int)(price * 1.1);
}

このようなメソッドでは、元のpriceを書き換える必要はありません。計算結果を戻り値として返せばよいので、値渡しが適しています。

初心者のうちは、まず「基本は値渡し」と覚えておきましょう。呼び出し元の変数を直接変更したい特別な理由があるときだけ、refなどを検討します。

3. C#の参照渡しとは?値渡しとの違い

値渡しに対して、呼び出し元の変数そのものをメソッド内から変更できる渡し方が「参照渡し」です。

3-1. 参照渡しとは何か

参照渡しとは、変数の値をコピーするのではなく、変数そのものをメソッドに渡すような仕組みです。

C#では、refキーワードを使うことで参照渡しを行えます。

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

int x = 10;
ChangeNumber(ref x);

Console.WriteLine(x);

実行結果は次のようになります。

C#
100

値渡しのときはx10のままでしたが、refを使うとx自体が100に変更されます。

3-2. 参照渡しでは呼び出し元の変数が変更される理由

参照渡しでは、メソッド内の引数が呼び出し元の変数と結びつきます。

そのため、メソッド内で引数に値を代入すると、呼び出し元の変数にも変更が反映されます。

C#
static void Reset(ref int value)
{
value = 0;
}

int score = 85;
Reset(ref score);

Console.WriteLine(score);

実行結果は次のとおりです。

C#
0

Resetメソッド内でvalue = 0;と代入すると、呼び出し元のscore0になります。

3-3. 値渡しと参照渡しの違いを表で比較

比較項目値渡し参照渡し
基本の動き値をコピーして渡す変数そのものを渡す
呼び出し元への影響原則として変数自体は変更されない変数自体を変更できる
C#での指定何も付けないrefを付ける
初期化通常の変数として必要refでは事前に初期化が必要
可読性わかりやすい変更される可能性があり注意が必要
主な用途計算、判定、表示など呼び出し元の値を直接更新したい場合

値渡しは安全でわかりやすいため、通常はこちらを使います。参照渡しは便利ですが、メソッド内で呼び出し元の変数が変わるため、使いすぎるとコードの流れが追いにくくなります。

3-4. 参照渡しを使うメリット・デメリット

参照渡しのメリットは、メソッド内から呼び出し元の変数を直接変更できることです。

たとえば、2つの値を入れ替えるメソッドを作る場合、refを使うと自然に書けます。

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

一方、デメリットもあります。メソッドを呼び出しただけで変数の値が変わるため、コードを読む人が注意しなければなりません。

また、戻り値やタプルを使えば同じことをよりわかりやすく表現できる場合もあります。参照渡しは必要な場面では便利ですが、何でもrefにすればよいわけではありません。

4. C#のrefとは?参照渡しを実現するキーワード

C#で参照渡しを行う代表的なキーワードがrefです。refを正しく理解すると、値渡しとの違いがはっきりわかります。

4-1. refキーワードの役割

refは、引数を参照渡しするためのキーワードです。

通常の引数では、変数の値がコピーされてメソッドに渡されます。しかし、refを付けると、メソッド内から呼び出し元の変数を直接操作できます。

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

このメソッドは、渡された変数の値に10を加算します。

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

Console.WriteLine(value);

実行結果は次のようになります。

C#
15

4-2. refを使った基本構文

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

C#
static void MethodName(ref int value)
{
value = 100;
}

int number = 10;
MethodName(ref number);

基本構文は次のように考えるとわかりやすいです。

C#
戻り値の型 メソッド名(ref  引数名)
{
// 引数を書き換える処理
}

メソッド名(ref 変数名);

重要なのは、呼び出し側にもrefが必要という点です。

4-3. メソッド定義側と呼び出し側の両方にrefが必要な理由

refは、メソッドの中で呼び出し元の変数が変更される可能性があることを示します。

メソッド定義側だけにrefを書くと、メソッドを作る人にはわかりますが、呼び出す人には変数が変更される可能性が見えにくくなります。

そのためC#では、呼び出し側にもrefを書かせることで、「このメソッド呼び出しによって変数が変わるかもしれない」と明確にしています。

C#
Update(ref count);

このように書いてあれば、countがメソッド内で変更される可能性があるとすぐにわかります。

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

次のコードでは、refを使って数値を2倍にしています。

C#
using System;

class Program
{
static void DoubleValue(ref int number)
{
number *= 2;
}

static void Main()
{
int value = 20;

DoubleValue(ref value);

Console.WriteLine(value);
}
}

実行結果は次のとおりです。

C#
40

DoubleValueメソッド内でnumber *= 2;を実行すると、呼び出し元のvalueも変更されます。

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

refを使うときは、いくつかのルールがあります。

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

C#
int x;
ChangeNumber(ref x); // エラー

xに値が入っていないため、refでは渡せません。

次に、呼び出し側にも必ずrefを書きます。

C#
ChangeNumber(x); // エラー
ChangeNumber(ref x); // OK

また、プロパティや計算式を直接ref引数に渡すことはできません。

C#
ChangeNumber(ref person.Age); // プロパティの場合はエラーになることがある
ChangeNumber(ref (x + 1)); // 式は渡せない

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

5. 値型と参照型の違いも理解しよう

refを理解するには、「値型」と「参照型」の違いも知っておく必要があります。ただし、「参照型」と「参照渡し」は別物です。

5-1. 値型とは

値型とは、変数が値そのものを持つ型です。

代表的な値型には、次のようなものがあります。

C#
int
double
bool
char
struct
enum

たとえば、int型の変数を別の変数に代入すると、値がコピーされます。

C#
int a = 10;
int b = a;

b = 20;

Console.WriteLine(a);
Console.WriteLine(b);

実行結果は次のとおりです。

C#
10
20

bを変更しても、aには影響しません。これは、aの値がbにコピーされているからです。

5-2. 参照型とは

参照型とは、変数がオブジェクトそのものではなく、オブジェクトへの参照を持つ型です。

代表的な参照型には、次のようなものがあります。

C#
class
string
array
interface
delegate

たとえば、クラスのインスタンスを変数に代入する場合を考えます。

C#
class Person
{
public string Name;
}

Person p1 = new Person();
p1.Name = "Alice";

Person p2 = p1;
p2.Name = "Bob";

Console.WriteLine(p1.Name);

実行結果は次のようになります。

C#
Bob

p1p2は同じオブジェクトを参照しているため、p2.Nameを変更すると、p1.Nameを見たときにも変更後の値が表示されます。

5-3. 「参照型」と「参照渡し」は別物

ここが初心者にとって最も重要なポイントです。

「参照型」は型の分類です。クラスや配列など、オブジェクトへの参照を扱う型を指します。

一方、「参照渡し」はメソッドへの引数の渡し方です。refを使って、呼び出し元の変数そのものを変更できるようにすることを指します。

つまり、参照型を渡したからといって、自動的に参照渡しになるわけではありません。

C#では、参照型を通常の引数として渡した場合も、引数の渡し方としては値渡しです。ただし、コピーされる値が「オブジェクトへの参照」であるため、オブジェクトの中身は変更できます。

5-4. 参照型を値渡ししたときの挙動

次のコードを見てみましょう。

C#
class Person
{
public string Name;
}

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

Person p = new Person();
p.Name = "Alice";

ChangeName(p);

Console.WriteLine(p.Name);

実行結果は次のようになります。

C#
Bob

この結果だけを見ると、「参照渡しされたのでは?」と思うかもしれません。しかし、実際には参照型を値渡ししています。

メソッドに渡されているのは、オブジェクトへの参照のコピーです。コピーされた参照も同じオブジェクトを指しているため、person.Nameを変更すると、呼び出し元のpから見ても変更されています。

ただし、メソッド内で引数そのものに別のオブジェクトを代入しても、呼び出し元の変数は変わりません。

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

Person p = new Person();
p.Name = "Alice";

ReplacePerson(p);

Console.WriteLine(p.Name);

実行結果は次のとおりです。

C#
Alice

ReplacePersonメソッド内でpersonに新しいオブジェクトを代入しても、呼び出し元のpは元のオブジェクトを参照したままです。

5-5. 参照型にrefを付けると何が変わるのか

参照型にrefを付けると、オブジェクトの中身だけでなく、呼び出し元の変数が参照している先そのものを変更できます。

C#
class Person
{
public string Name;
}

static void ReplacePerson(ref Person person)
{
person = new Person();
person.Name = "Charlie";
}

Person p = new Person();
p.Name = "Alice";

ReplacePerson(ref p);

Console.WriteLine(p.Name);

実行結果は次のようになります。

C#
Charlie

refを付けたことで、メソッド内のperson = new Person();が呼び出し元のpにも反映されます。

つまり、参照型を通常の値渡しで渡した場合は、オブジェクトの中身は変更できますが、変数が指す先を差し替えることはできません。参照型にrefを付けると、変数が指す先も変更できます。

6. ref・out・inの違いと使い分け

C#には、引数の渡し方に関係するキーワードとしてrefのほかにoutinがあります。それぞれ役割が違うため、使い分けを理解しておきましょう。

6-1. refは読み取りと書き換えができる

refは、メソッド内で引数を読み取ることも、書き換えることもできます。

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

int score = 50;
AddPoint(ref score);

Console.WriteLine(score);

実行結果は次のとおりです。

C#
60

refで渡す変数は、呼び出し前に初期化されている必要があります。メソッド内では、現在の値を使いながら変更できます。

6-2. outは結果を返すために使う

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

refと違い、呼び出し前に変数が初期化されていなくても使えます。ただし、メソッド内では必ず値を代入する必要があります。

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

int w;
int h;

GetSize(out w, out h);

Console.WriteLine(w);
Console.WriteLine(h);

実行結果は次のようになります。

C#
1920
1080

outは、複数の結果を返したいときによく使われます。代表的な例はint.TryParseです。

C#
string text = "123";

if (int.TryParse(text, out int number))
{
Console.WriteLine(number);
}

TryParseは変換に成功したかどうかをboolで返し、変換結果をout引数に入れます。

6-3. inは読み取り専用で渡す

inは、引数を読み取り専用の参照として渡すために使います。

メソッド内で値を変更することはできません。

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

// value = 100; // エラー
}

int x = 10;
PrintValue(in x);

inは、大きな構造体をコピーせずに渡したいが、メソッド内で変更されたくない場合に使われます。

通常の小さな値型であれば、無理にinを使う必要はありません。パフォーマンス上の理由がある場合に検討するものです。

6-4. ref・out・inの違いを比較表で整理

キーワード呼び出し前の初期化メソッド内で読み取りメソッド内で代入主な用途
ref必要できるできる既存の値を更新する
out不要代入前はできない必須結果を返す
in必要できるできない読み取り専用で効率よく渡す

refは「読み取って変更する」、outは「結果を入れて返す」、inは「読み取り専用で渡す」と覚えるとわかりやすいです。

6-5. どれを使うべきか判断するポイント

どれを使うべきか迷ったときは、まず通常の値渡しで書けないかを考えます。

戻り値で表現できるなら、戻り値を使う方が自然です。

C#
static int AddTen(int value)
{
return value + 10;
}

複数の結果を返したい場合は、outまたはタプルを検討します。

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

既存の変数をメソッド内で更新したい場合は、refが候補になります。

大きな構造体をコピーしたくないが変更もさせたくない場合は、inが候補になります。

ただし、初心者のうちはrefoutinを多用するよりも、戻り値を使ったわかりやすい設計を優先するのがおすすめです。

7. C#のrefでよくあるエラーと解決方法

refは便利ですが、使い方を間違えるとコンパイルエラーになります。ここでは初心者がよく遭遇するエラーと解決方法を紹介します。

7-1. 変数を初期化せずにrefで渡してしまう

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

C#
static void SetValue(ref int value)
{
value = 100;
}

int x;
SetValue(ref x); // エラー

このコードは、xが初期化されていないためエラーになります。

解決するには、呼び出す前に値を代入します。

C#
int x = 0;
SetValue(ref x);

初期値が不要で、メソッド内で必ず値を設定する目的なら、refではなくoutを使う方が適しています。

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

int x;
SetValue(out x);

7-2. 呼び出し側にrefを書き忘れる

refを使うメソッドを呼び出すときは、呼び出し側にもrefを書く必要があります。

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

int x = 5;
AddTen(x); // エラー

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

C#
AddTen(ref x);

呼び出し側にrefを書くことで、このメソッドによってxが変更される可能性があることを明示できます。

7-3. プロパティや式をref引数に渡そうとする

ref引数には、変更可能な変数を渡す必要があります。プロパティや計算式を直接渡そうとすると、エラーになることがあります。

C#
ChangeNumber(ref (x + 1)); // エラー

式は一時的な値であり、代入先として使える変数ではありません。

また、プロパティも内部的にはメソッド呼び出しのように扱われるため、通常の変数のようにrefで渡せない場合があります。

C#
class Person
{
public int Age { get; set; }
}

Person person = new Person();
ChangeNumber(ref person.Age); // エラーになるケース

解決するには、一度ローカル変数に取り出してからrefで渡し、必要に応じて戻します。

C#
int age = person.Age;
ChangeNumber(ref age);
person.Age = age;

7-4. asyncメソッドやイテレーターで使えないケース

asyncメソッドでは、refoutin引数を使えないケースがあります。

C#
static async Task UpdateAsync(ref int value)
{
await Task.Delay(1000);
value = 10;
}

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

また、yield returnを使うイテレーターでも、refoutin引数に制限があります。

非同期処理で結果を返したい場合は、戻り値を使う設計にしましょう。

C#
static async Task<int> UpdateAsync(int value)
{
await Task.Delay(1000);
return value + 10;
}

このように、Task<int>として結果を返す方が自然です。

7-5. エラーを防ぐためのチェックリスト

refでエラーを防ぐには、次の点を確認しましょう。

チェック項目確認内容
初期化されているかrefで渡す変数には事前に値が入っているか
呼び出し側にもrefがあるかメソッド呼び出し時にrefを書いているか
変数を渡しているか式や一時的な値を渡していないか
プロパティを渡していないか必要ならローカル変数に取り出しているか
非同期処理で使っていないかasyncでは戻り値で返せないか検討しているか
本当にrefが必要か戻り値やタプルで代替できないか

refは使い方が明確な場面では便利ですが、制約も多いキーワードです。エラーが出たときは、まず「本当に呼び出し元の変数を書き換える必要があるか」を確認しましょう。

8. refを使うべきケース・使わない方がよいケース

refは強力な機能ですが、使うべき場面と使わない方がよい場面があります。初心者は特に、必要以上にrefを使わないことが大切です。

8-1. 複数の値を変更したい場合

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);
Console.WriteLine(y);

実行結果は次のとおりです。

C#
20
10

Swapのように、呼び出し元の複数の変数を直接変更する処理ではrefが役立ちます。

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

8-2. 大きな構造体のコピーを避けたい場合

値型の構造体は、通常の値渡しではコピーされます。構造体が大きい場合、コピーのコストが気になることがあります。

そのような場面では、refinを使ってコピーを避けることがあります。

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

static void Update(ref LargeData data)
{
data.A = 100;
}

ただし、単に読み取るだけならrefよりinの方が適しています。

C#
static void Print(in LargeData data)
{
Console.WriteLine(data.A);
}

大きな構造体を扱う場面では、パフォーマンスを意識してrefinを使うことがあります。しかし、通常のアプリケーション開発では、まず可読性を優先して問題ありません。

8-3. 戻り値やタプルで代替できる場合

refを使わなくても、戻り値で表現できる場合は多くあります。

C#
static int AddTen(int value)
{
return value + 10;
}

int x = 5;
x = AddTen(x);

このコードは、refを使うよりも処理の流れがわかりやすいです。

複数の値を返したい場合も、タプルを使えます。

C#
static (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);
Console.WriteLine(result.max);

戻り値やタプルで自然に書けるなら、無理にrefを使う必要はありません。

8-4. 可読性が下がるケース

refを使うと、メソッド呼び出しによって変数が変更されます。そのため、コードを読む人は「この変数はどこで変更されたのか」を追いかける必要があります。

C#
UpdateUser(ref user);
Calculate(ref total);
Normalize(ref data);

このような呼び出しが増えると、処理の流れが見えにくくなります。

特に、1つのメソッド内で多くのref引数を扱うと、どの変数がどのタイミングで変更されるのか把握しづらくなります。

可読性を重視する場合は、戻り値を使ったり、処理を小さなメソッドに分けたりする方がよいでしょう。

8-5. 初心者がrefを使いすぎない方がよい理由

初心者がrefを使いすぎない方がよい理由は、コードの動きがわかりにくくなるからです。

通常の値渡しでは、メソッドに値を渡しても呼び出し元の変数は勝手に変わりません。このルールはシンプルで理解しやすいです。

一方、refを使うと、メソッドの中で呼び出し元の変数が直接変更されます。便利ではありますが、予想外の変更が起きやすくなります。

初心者はまず、次の優先順位で考えるのがおすすめです。

優先順位方法考え方
1通常の値渡し基本はこれ
2戻り値結果を返したい場合
3タプル複数の結果を返したい場合
4out結果を引数で受け取りたい場合
5ref呼び出し元の変数を直接変更したい場合
6in大きな構造体を読み取り専用で効率よく渡したい場合

refは最後の手段ではありませんが、必要性を説明できる場面で使うのが基本です。

9. 実践で理解するC#の値渡し・参照渡し・ref

ここでは、実際のコードを使って、値渡し・参照渡し・参照型・refoutinの違いを確認します。

9-1. 値渡しで変数を変更するサンプル

まずは、通常の値渡しです。

C#
using System;

class Program
{
static void ChangeValue(int number)
{
number = 100;
Console.WriteLine("メソッド内: " + number);
}

static void Main()
{
int x = 10;

ChangeValue(x);

Console.WriteLine("呼び出し元: " + x);
}
}

実行結果は次のとおりです。

C#
メソッド内: 100
呼び出し元: 10

メソッド内ではnumber100になっていますが、呼び出し元のx10のままです。

9-2. refで変数を変更するサンプル

次に、refを使って参照渡しします。

C#
using System;

class Program
{
static void ChangeValue(ref int number)
{
number = 100;
Console.WriteLine("メソッド内: " + number);
}

static void Main()
{
int x = 10;

ChangeValue(ref x);

Console.WriteLine("呼び出し元: " + x);
}
}

実行結果は次のようになります。

C#
メソッド内: 100
呼び出し元: 100

refを使っているため、メソッド内での変更が呼び出し元のxにも反映されます。

9-3. 参照型のオブジェクトを渡すサンプル

次は、参照型であるクラスを通常の値渡しで渡す例です。

C#
using System;

class Person
{
public string Name;
}

class Program
{
static void ChangeName(Person person)
{
person.Name = "Bob";
}

static void ReplacePerson(Person person)
{
person = new Person();
person.Name = "Charlie";
}

static void Main()
{
Person p = new Person();
p.Name = "Alice";

ChangeName(p);
Console.WriteLine(p.Name);

ReplacePerson(p);
Console.WriteLine(p.Name);
}
}

実行結果は次のとおりです。

C#
Bob
Bob

ChangeNameでは、同じオブジェクトのNameを書き換えているため、呼び出し元にも反映されます。

一方、ReplacePersonでは、メソッド内のpersonに新しいオブジェクトを代入しています。しかし、通常の値渡しなので、呼び出し元のpが指す先は変わりません。そのため、結果はBobのままです。

9-4. ref・out・inを使い分けるサンプル

次のコードでは、refoutinをまとめて確認します。

C#
using System;

class Program
{
static void AddBonus(ref int score)
{
score += 10;
}

static void GetResult(out int result)
{
result = 100;
}

static void PrintScore(in int score)
{
Console.WriteLine("読み取り専用: " + score);

// score = 0; // エラー
}

static void Main()
{
int score = 50;

AddBonus(ref score);
Console.WriteLine("ref後: " + score);

int result;
GetResult(out result);
Console.WriteLine("out後: " + result);

PrintScore(in score);
}
}

実行結果は次のようになります。

C#
ref後: 60
out後: 100
読み取り専用: 60

refは既存の値を読み取って更新しています。outはメソッド内で結果を代入しています。inは読み取り専用で値を参照しています。

9-5. 実行結果から違いを確認する

ここまでの実行結果を整理すると、次のようになります。

サンプル呼び出し元の変数理由
値型を値渡し変わらない値がコピーされるため
値型をrefで渡す変わる変数そのものを変更できるため
参照型を値渡しして中身を変更中身は変わる参照のコピーが同じオブジェクトを指すため
参照型を値渡しして代入し直す変数の参照先は変わらない参照のコピーを書き換えているだけのため
参照型をrefで渡して代入し直す参照先も変わる呼び出し元の変数そのものを変更できるため

C#の引数を理解するポイントは、「何がコピーされているのか」を考えることです。

値型の場合は値そのものがコピーされます。参照型の場合は、オブジェクトへの参照がコピーされます。refを付けた場合は、呼び出し元の変数そのものを変更できます。

10. C#リファレンスに関するよくある質問

最後に、C#のリファレンス、値渡し、参照渡し、refに関するよくある質問をまとめます。

10-1. C#のrefはいつ使いますか?

refは、メソッド内で呼び出し元の変数を直接変更したいときに使います。

たとえば、2つの変数を入れ替える処理や、既存の値を更新する処理などです。

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

ただし、戻り値で表現できる場合は、戻り値を使う方がわかりやすいことが多いです。

C#
static int Increment(int value)
{
return value + 1;
}

初心者のうちは、refを使う前に「戻り値で書けないか」を考えるとよいでしょう。

10-2. refとoutの違いは何ですか?

refoutの大きな違いは、呼び出し前に初期化が必要かどうかです。

refは、呼び出し前に変数を初期化しておく必要があります。メソッド内では、その値を読み取ることも変更することもできます。

outは、呼び出し前に初期化していなくても使えます。ただし、メソッド内で必ず値を代入する必要があります。

C#
static void UseRef(ref int value)
{
value += 10;
}

static void UseOut(out int value)
{
value = 10;
}

既存の値を更新したいならref、メソッドから結果を受け取りたいならoutと考えるとわかりやすいです。

10-3. 参照型ならrefは不要ですか?

参照型だからといって、常にrefが不要とは限りません。

参照型のオブジェクトの中身を変更するだけなら、通常の値渡しで十分です。

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

この場合、refは不要です。

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

C#
static void Replace(ref Person person)
{
person = new Person();
}

つまり、オブジェクトの中身を変えたいだけならrefは不要です。変数の参照先そのものを変えたい場合はrefを使います。

10-4. 値渡しと参照渡しはどちらを使うべきですか?

基本的には値渡しを使うべきです。

値渡しは動きがわかりやすく、呼び出し元の変数が予想外に変更されにくいからです。

参照渡しは、呼び出し元の変数を直接変更する必要がある場合に使います。ただし、戻り値やタプルで表現できる場合は、そちらの方が読みやすいことが多いです。

たとえば、次のような書き方はシンプルです。

C#
int result = Calculate(value);

一方、次のようなコードは、valueがメソッド内で変更されることを意識して読む必要があります。

C#
Calculate(ref value);

可読性を重視するなら、まず値渡しと戻り値を基本に考えましょう。

10-5. 初心者はまず何を覚えればよいですか?

初心者は、まず次の5つを覚えるとよいです。

覚えること内容
C#の通常の引数は値渡し何も付けなければ値がコピーされる
値型は値そのものを持つintboolなどは値がコピーされる
参照型はオブジェクトへの参照を持つクラスや配列は参照を通じてオブジェクトを扱う
参照型と参照渡しは別物参照型を渡しても通常は値渡し
refは変数そのものを変更できる定義側と呼び出し側の両方にrefを書く

特に重要なのは、「参照型」と「参照渡し」を混同しないことです。

C#では、参照型を通常の引数として渡しても、引数の渡し方としては値渡しです。ただし、渡される値がオブジェクトへの参照なので、オブジェクトの中身は変更できます。

この考え方がわかると、refが必要な場面と不要な場面を判断しやすくなります。

まとめ

C#リファレンスという言葉には、公式ドキュメントとしての意味と、プログラム上の参照に関する意味があります。この記事では、特にC#の値渡し、参照渡し、refの違いを中心に解説しました。

C#の通常の引数は値渡しです。値型を値渡しすると、メソッド内で変更しても呼び出し元の変数には反映されません。

一方、refを使うと参照渡しになり、メソッド内から呼び出し元の変数そのものを変更できます。refを使う場合は、メソッド定義側と呼び出し側の両方にrefを書く必要があり、渡す変数は事前に初期化されていなければなりません。

また、参照型と参照渡しは別物です。クラスなどの参照型を通常の引数として渡した場合も、引数の渡し方としては値渡しです。ただし、参照のコピーが同じオブジェクトを指しているため、オブジェクトの中身は変更できます。参照型にrefを付けると、呼び出し元の変数が参照している先そのものを差し替えられます。

refoutinの違いも重要です。refは既存の値を読み取って変更したいとき、outはメソッドから結果を返したいとき、inは読み取り専用で効率よく渡したいときに使います。

初心者は、まず通常の値渡しを基本として理解し、必要に応じて戻り値、タプル、outrefinを使い分けるのがおすすめです。refは便利な機能ですが、使いすぎると可読性が下がるため、本当に呼び出し元の変数を直接変更する必要がある場面で使いましょう。