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

はじめに

C#を学び始めると、「参照」「値渡し」「参照渡し」「ref」といった言葉が出てきます。これらはC#の基礎でありながら、初心者がつまずきやすいポイントです。

特に混乱しやすいのが、次のような疑問です。

「参照型なのに値渡しとはどういう意味?」
「refを付けると何が変わるの?」
「配列をメソッドに渡すと元の配列が変わるのはなぜ?」
「stringは参照型なのに、なぜ値型のように見えるの?」

C#の参照を理解するには、まず「値型」と「参照型」の違いを押さえる必要があります。そのうえで、メソッド引数における「値渡し」と「参照渡し」、そしてrefキーワードの役割を整理すると、かなり理解しやすくなります。

この記事では、C#の参照について、初心者向けにコード例を交えながらわかりやすく解説します。

1. C#の「参照」とは?初心者が最初に理解すべき基本

1-1. 参照とは「データそのもの」ではなく「場所を指す情報」のこと

C#における「参照」とは、簡単に言うと「データが存在する場所を指す情報」です。

たとえば、あるオブジェクトがメモリ上に作られたとします。変数には、そのオブジェクトそのものが丸ごと入っているのではなく、「そのオブジェクトがどこにあるか」という情報が入ります。この「場所を指す情報」が参照です。

イメージとしては、家そのものではなく「住所」を持っているようなものです。

C#
Person p = new Person();

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

つまり、pはオブジェクトそのものではなく、オブジェクトへアクセスするための情報を持っていると考えるとわかりやすいです。

1-2. C#で参照が重要になる理由

C#で参照を理解することは非常に重要です。なぜなら、クラス、配列、文字列、Listなど、C#でよく使う多くのデータが参照型だからです。

参照を理解していないと、次のような挙動で混乱しやすくなります。

C#
int[] a = { 1, 2, 3 };
int[] b = a;

b[0] = 100;

Console.WriteLine(a[0]);

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

100

b[0]を変更しただけなのに、a[0]も変わったように見えます。これは、abが同じ配列を参照しているからです。

このように、C#の参照を理解しておくと、「なぜ元の値が変わったのか」「なぜ変わらなかったのか」を正しく判断できるようになります。

1-3. 参照を理解すると配列・クラス・メソッド引数がわかりやすくなる

C#の参照を理解すると、次のようなテーマが一気にわかりやすくなります。

配列を別の変数に代入したときの挙動、クラスのインスタンスをメソッドに渡したときの挙動、refoutを使った引数の挙動、NullReferenceExceptionが起きる理由などです。

特にメソッド引数では、「参照型を渡しているから参照渡し」と誤解されることがあります。しかしC#では、通常のメソッド引数は値渡しです。参照型を渡す場合でも、「参照の値」がコピーされて渡されます。

この違いは後ほど詳しく解説します。

2. C#の値型と参照型の違い

2-1. 値型とは?int・double・bool・structの特徴

C#の値型とは、変数が値そのものを保持する型です。

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

C#
int number = 10;
double price = 99.5;
bool isActive = true;

intdoubleboolchardecimalstructenumなどは値型です。

値型の特徴は、変数に直接データが入っているように扱われることです。別の変数に代入すると、値がコピーされます。

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

b = 20;

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

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

10
20

bを変更しても、aは変わりません。これは、aの値がbにコピーされているためです。

2-2. 参照型とは?class・string・array・objectの特徴

参照型とは、変数がデータそのものではなく、データの場所を指す参照を保持する型です。

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

C#
string name = "Alice";
int[] numbers = { 1, 2, 3 };
Person person = new Person();
object obj = new object();

classstringarrayobjectinterfacedelegateなどは参照型です。

参照型では、変数にオブジェクトそのものが入るのではなく、オブジェクトを指す参照が入ります。そのため、別の変数に代入すると、オブジェクトがコピーされるのではなく、参照がコピーされます。

C#
int[] a = { 1, 2, 3 };
int[] b = a;

b[0] = 100;

Console.WriteLine(a[0]);

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

100

abは同じ配列を参照しているため、bから配列の中身を変更すると、aから見ても変更後の値になります。

2-3. 値型と参照型のメモリ上のイメージ

値型と参照型の違いを、メモリのイメージで考えてみましょう。

値型の場合、変数が値そのものを持ちます。

a -> 10
b -> 10

abに代入すると、値がコピーされます。したがって、bを変更してもaには影響しません。

一方、参照型の場合、変数はオブジェクトへの参照を持ちます。

a -> 配列オブジェクト { 1, 2, 3 }
b ┘

abに代入すると、配列そのものがコピーされるのではなく、配列を指す参照がコピーされます。そのため、abは同じ配列オブジェクトを指します。

2-4. 代入したときの挙動の違い

値型と参照型では、代入時の挙動が大きく異なります。

値型の場合は、値がコピーされます。

C#
int x = 5;
int y = x;

y = 10;

Console.WriteLine(x);

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

5

参照型の場合は、参照がコピーされます。

C#
int[] x = { 5 };
int[] y = x;

y[0] = 10;

Console.WriteLine(x[0]);

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

10

この違いを理解することが、C#の参照を理解する第一歩です。

2-5. 値型と参照型の違いをコード例で確認

次のコードで、値型と参照型の違いをまとめて確認してみましょう。

C#
using System;

class Program
{
static void Main()
{
int a = 10;
int b = a;
b = 20;

Console.WriteLine("値型:");
Console.WriteLine(a);
Console.WriteLine(b);

int[] numbers1 = { 10 };
int[] numbers2 = numbers1;
numbers2[0] = 20;

Console.WriteLine("参照型:");
Console.WriteLine(numbers1[0]);
Console.WriteLine(numbers2[0]);
}
}

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

値型:
10
20
参照型:
20
20

値型では値がコピーされ、参照型では参照がコピーされることがわかります。

3. C#の値渡しとは?

3-1. 値渡しの基本:引数の値がコピーされる仕組み

C#のメソッド引数は、何も指定しなければ基本的に値渡しです。

値渡しとは、メソッドに引数を渡すときに、変数の値をコピーして渡す仕組みです。呼び出し先のメソッド内で引数を書き換えても、呼び出し元の変数そのものは変更されません。

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

このメソッドのxは、呼び出し元の変数そのものではありません。呼び出し元の値がコピーされて、メソッド内のxに入っているだけです。

3-2. 値型を値渡しした場合の挙動

値型を値渡しすると、値そのものがコピーされます。

C#
using System;

class Program
{
static void Change(int x)
{
x = 100;
}

static void Main()
{
int number = 10;
Change(number);

Console.WriteLine(number);
}
}

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

10

Changeメソッド内でx = 100;としても、呼び出し元のnumberは変わりません。これは、numberの値である10がコピーされて渡されているからです。

3-3. 参照型を値渡しした場合の挙動

参照型を値渡しした場合は少し注意が必要です。

参照型を値渡しすると、「オブジェクトへの参照」がコピーされて渡されます。つまり、参照のコピーがメソッドに渡されます。

C#
using System;

class Program
{
static void ChangeArray(int[] array)
{
array[0] = 100;
}

static void Main()
{
int[] numbers = { 10, 20, 30 };
ChangeArray(numbers);

Console.WriteLine(numbers[0]);
}
}

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

100

この結果を見ると、「参照渡しされているのでは?」と思うかもしれません。しかし、これは参照渡しではありません。参照型の値、つまり参照がコピーされて渡されているだけです。

メソッド内のarrayと呼び出し元のnumbersは別の変数ですが、同じ配列オブジェクトを指しています。そのため、配列の中身を変更すると、呼び出し元から見ても変更されたように見えます。

3-4. 「参照型を値渡し」が初心者に混乱されやすい理由

「参照型を値渡し」が混乱されやすい理由は、メソッド内でオブジェクトの中身を変更できるからです。

次のコードを見てください。

C#
static void ChangeArray(int[] array)
{
array[0] = 100;
}

この場合、array[0]を変更すると、呼び出し元の配列にも影響します。

しかし、次のようにメソッド内で別の配列を代入した場合はどうでしょうか。

C#
static void ReplaceArray(int[] array)
{
array = new int[] { 100, 200, 300 };
}

この場合、呼び出し元の変数が新しい配列を指すようになるわけではありません。

C#
using System;

class Program
{
static void ReplaceArray(int[] array)
{
array = new int[] { 100, 200, 300 };
}

static void Main()
{
int[] numbers = { 1, 2, 3 };
ReplaceArray(numbers);

Console.WriteLine(numbers[0]);
}
}

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

1

メソッド内のarrayに新しい配列を代入しても、呼び出し元のnumbersは元の配列を参照したままです。ここが、参照型の値渡しを理解するうえで重要なポイントです。

3-5. 値渡しのコード例と実行結果

値型と参照型の値渡しをまとめて確認しましょう。

C#
using System;

class Program
{
static void ChangeNumber(int x)
{
x = 100;
}

static void ChangeArrayElement(int[] array)
{
array[0] = 100;
}

static void ReplaceArray(int[] array)
{
array = new int[] { 999 };
}

static void Main()
{
int number = 10;
ChangeNumber(number);
Console.WriteLine(number);

int[] numbers = { 10 };
ChangeArrayElement(numbers);
Console.WriteLine(numbers[0]);

ReplaceArray(numbers);
Console.WriteLine(numbers[0]);
}
}

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

10
100
100

値型の値渡しでは元の変数は変わりません。参照型の値渡しでは、オブジェクトの中身は変更できますが、呼び出し元の変数が別のオブジェクトを指すようにはできません。

4. C#の参照渡しとは?

4-1. 参照渡しの基本:変数そのものをメソッドに渡す仕組み

参照渡しとは、メソッドに変数そのものを渡す仕組みです。

値渡しでは値がコピーされますが、参照渡しでは呼び出し元の変数に直接アクセスできるようになります。そのため、メソッド内で引数に別の値を代入すると、呼び出し元の変数も変更されます。

C#で参照渡しを行うには、主にrefキーワードを使います。

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

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

C#
int number = 10;
Change(ref number);

4-2. 参照渡しを使うと呼び出し元の変数を変更できる

参照渡しを使うと、メソッド内で呼び出し元の変数を書き換えることができます。

C#
using System;

class Program
{
static void Change(ref int x)
{
x = 100;
}

static void Main()
{
int number = 10;
Change(ref number);

Console.WriteLine(number);
}
}

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

100

値渡しの場合は10のままでしたが、refを使った参照渡しではnumber自体が変更されます。

4-3. 値型を参照渡しした場合の挙動

値型を参照渡しすると、メソッド内で呼び出し元の値型変数を直接変更できます。

C#
using System;

class Program
{
static void AddTen(ref int x)
{
x += 10;
}

static void Main()
{
int score = 50;
AddTen(ref score);

Console.WriteLine(score);
}
}

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

60

値型であっても、refを使えば呼び出し元の変数を変更できます。

4-4. 参照型を参照渡しした場合の挙動

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

C#
using System;

class Program
{
static void ReplaceArray(ref int[] array)
{
array = new int[] { 100, 200, 300 };
}

static void Main()
{
int[] numbers = { 1, 2, 3 };

ReplaceArray(ref numbers);

Console.WriteLine(numbers[0]);
}
}

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

100

refなしで配列を渡した場合、メソッド内で新しい配列を代入しても呼び出し元には影響しませんでした。しかし、refを付けると、呼び出し元のnumbersが新しい配列を参照するようになります。

4-5. 参照渡しのコード例と実行結果

参照渡しの挙動をまとめて確認しましょう。

C#
using System;

class Program
{
static void ChangeNumber(ref int x)
{
x = 100;
}

static void ReplaceArray(ref int[] array)
{
array = new int[] { 999 };
}

static void Main()
{
int number = 10;
ChangeNumber(ref number);
Console.WriteLine(number);

int[] numbers = { 10 };
ReplaceArray(ref numbers);
Console.WriteLine(numbers[0]);
}
}

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

100
999

参照渡しでは、値型であっても参照型であっても、呼び出し元の変数そのものを変更できます。

5. C#のrefとは?

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

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

通常、C#のメソッド引数は値渡しです。しかし、引数にrefを付けると、呼び出し元の変数そのものをメソッドに渡せます。

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

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

5-2. refを使ったメソッド引数の書き方

refを使う場合は、メソッド定義側と呼び出し側の両方にrefを書く必要があります。

C#
using System;

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

static void Main()
{
int value = 10;

DoubleValue(ref value);

Console.WriteLine(value);
}
}

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

20

メソッド定義側だけでなく、呼び出し側にもrefを書くことで、「このメソッドは変数を変更する可能性がある」と読み手に明示できます。

5-3. refを使うと呼び出し元の変数が変更される理由

refを使うと、メソッド内の引数が呼び出し元の変数の別名のように扱われます。

値渡しでは、メソッド内の引数はコピーです。

呼び出し元の変数 number
メソッド内の変数 x

この2つは別物です。

一方、refを使うと、メソッド内のxが呼び出し元のnumberと同じ変数を指すようになります。

number と x が同じ変数を指す

そのため、メソッド内でx = 100;とすると、呼び出し元のnumber100になります。

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

refを使う際には、いくつか注意点があります。

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

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

refは、呼び出し元の変数の現在の値をメソッドに渡し、さらにメソッド内で変更できる仕組みです。そのため、未初期化の変数は渡せません。

次に、呼び出し側にもrefを書く必要があります。

C#
Change(ref number);

これは面倒に見えるかもしれませんが、メソッドによって変数が変更される可能性を明確にするために重要です。

また、refを使いすぎると、コードの流れが追いにくくなります。どこで変数が変更されたのかがわかりづらくなるため、必要な場面に限定して使うことが大切です。

5-5. refのコード例と実行結果

refの基本的なコード例を確認しましょう。

C#
using System;

class Program
{
static void Swap(ref int a, ref int b)
{
int temp = a;
a = b;
b = temp;
}

static void Main()
{
int x = 10;
int y = 20;

Swap(ref x, ref y);

Console.WriteLine(x);
Console.WriteLine(y);
}
}

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

20
10

Swapメソッドでは、xyの値を入れ替えています。refを使うことで、呼び出し元の変数を直接変更できています。

6. 値渡し・参照渡し・refの違いを比較

6-1. 値渡しと参照渡しの違い

値渡しと参照渡しの違いは、メソッドに何を渡しているかです。

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

参照渡しでは、変数そのものを渡します。メソッド内で引数に別の値を代入すると、呼び出し元の変数も変更されます。

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

static void ByReference(ref int x)
{
x = 100;
}

呼び出し結果は次のように異なります。

C#
int a = 10;
ByValue(a);
Console.WriteLine(a); // 10

int b = 10;
ByReference(ref b);
Console.WriteLine(b); // 100

6-2. 参照型と参照渡しの違い

初心者が特に混同しやすいのが、「参照型」と「参照渡し」です。

参照型とは、型の分類です。classstring、配列、List<T>などが参照型です。

一方、参照渡しとは、メソッド引数の渡し方です。C#では、refを使うことで参照渡しができます。

つまり、「参照型」と「参照渡し」は別の概念です。

C#
int[] numbers = { 1, 2, 3 };
Change(numbers);

このように配列は参照型ですが、refを付けていないため、引数の渡し方は値渡しです。ただし、渡される値が「配列への参照」なので、メソッド内で配列の中身を変更できます。

6-3. refあり・refなしの違い

参照型を引数に渡す場合、refありとrefなしでは挙動が変わります。

refなしの場合、オブジェクトの中身は変更できますが、呼び出し元の変数が指す先は変更できません。

C#
static void Replace(int[] array)
{
array = new int[] { 100 };
}

refありの場合、呼び出し元の変数が指す先そのものを変更できます。

C#
static void Replace(ref int[] array)
{
array = new int[] { 100 };
}

この違いは非常に重要です。

6-4. 比較表で理解する値型・参照型・値渡し・参照渡し

パターンメソッド内で値を代入した場合オブジェクトの中身を変更した場合呼び出し元への影響
値型を値渡し変更されない該当なし影響なし
値型をrefで参照渡し変更される該当なし影響あり
参照型を値渡し参照先の変更は反映されない変更される中身の変更は影響あり
参照型をrefで参照渡し参照先の変更も反映される変更される影響あり

ポイントは、「参照型かどうか」と「参照渡しかどうか」を分けて考えることです。

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

初心者が混同しやすいポイントは、次の3つです。

1つ目は、「参照型を渡すこと」と「参照渡し」を同じだと思ってしまうことです。参照型を引数に渡しても、refがなければ通常は値渡しです。

2つ目は、参照型の値渡しでは、オブジェクトの中身は変更できるという点です。そのため、見た目だけでは参照渡しのように感じることがあります。

3つ目は、stringの挙動です。stringは参照型ですが、不変なので値型のように見えることがあります。

これらを整理すると、C#の参照に関する理解がかなり深まります。

7. ref・out・inの違い

7-1. outとは?メソッド内で値を必ず代入する引数

outは、メソッドから複数の値を返したい場合などに使われるキーワードです。

outで渡された引数は、メソッド内で必ず値を代入する必要があります。呼び出し元の変数は、事前に初期化されていなくても構いません。

C#
using System;

class Program
{
static void GetValue(out int x)
{
x = 100;
}

static void Main()
{
int number;
GetValue(out number);

Console.WriteLine(number);
}
}

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

100

outは、メソッドが値を設定して返すことを明確にしたい場合に使います。

7-2. inとは?読み取り専用で参照渡しする引数

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

inを使うと、引数をコピーせずに渡しつつ、メソッド内ではその値を変更できないようにできます。

C#
static void PrintValue(in int x)
{
Console.WriteLine(x);
// x = 100; // エラー
}

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

7-3. ref・out・inの使い分け

refoutinは似ていますが、目的が異なります。

キーワード目的呼び出し前の初期化メソッド内での代入
ref値を受け取り、変更して返す必要任意
outメソッド内で値を設定して返す不要必須
inコピーを避けつつ読み取り専用で渡す必要不可

refは、既存の値を変更したいときに使います。

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

inは、値を読み取るだけで変更したくないときに使います。

7-4. ref・out・inのコード例

次のコードで、refoutinの違いを確認しましょう。

C#
using System;

class Program
{
static void UseRef(ref int x)
{
x += 10;
}

static void UseOut(out int x)
{
x = 100;
}

static void UseIn(in int x)
{
Console.WriteLine(x);
}

static void Main()
{
int a = 10;
UseRef(ref a);
Console.WriteLine(a);

int b;
UseOut(out b);
Console.WriteLine(b);

int c = 30;
UseIn(in c);
}
}

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

20
100
30

それぞれのキーワードが、異なる目的で使われていることがわかります。

7-5. 実務でよく使われる場面

実務でよく見かけるのは、outを使ったTryParse系のメソッドです。

C#
string text = "123";

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

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

一方、refは値を入れ替える処理や、パフォーマンス上の理由でコピーを避けたい場合などに使われます。ただし、通常のアプリケーション開発では、戻り値やクラスのプロパティを使ったほうが読みやすいことも多いです。

inは、大きな構造体を扱う場面などで使われることがありますが、初心者のうちは「読み取り専用の参照渡し」と理解しておけば十分です。

8. C#の参照でよくある疑問とエラー

8-1. stringは参照型なのに値型のように見えるのはなぜ?

stringは参照型です。しかし、値型のように見えることがあります。

その理由は、stringが不変、つまり一度作られた文字列の中身を変更できない型だからです。

C#
string a = "Hello";
string b = a;

b = "World";

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

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

Hello
World

b = "World";と書くと、bが新しい文字列を参照するようになります。元の"Hello"の中身が書き換えられたわけではありません。

そのため、stringは参照型でありながら、値型に近い感覚で扱えるように見えます。

8-2. 配列やListをメソッドで変更すると元の値も変わるのはなぜ?

配列やList<T>は参照型です。そのため、メソッドに渡すと参照のコピーが渡されます。

C#
using System;
using System.Collections.Generic;

class Program
{
static void AddItem(List<int> list)
{
list.Add(100);
}

static void Main()
{
List<int> numbers = new List<int> { 1, 2, 3 };

AddItem(numbers);

Console.WriteLine(numbers.Count);
}
}

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

4

メソッド内のlistと呼び出し元のnumbersは、同じList<int>オブジェクトを参照しています。そのため、list.Add(100)を実行すると、呼び出し元から見ても要素が追加されています。

8-3. NullReferenceExceptionとは?

NullReferenceExceptionは、参照がnullであるにもかかわらず、その参照先のメンバーにアクセスしようとしたときに発生する例外です。

C#
Person person = null;

Console.WriteLine(person.Name);

このコードでは、personはどのオブジェクトも参照していません。その状態でperson.Nameにアクセスしようとしているため、NullReferenceExceptionが発生します。

対策としては、nullチェックを行います。

C#
if (person != null)
{
Console.WriteLine(person.Name);
}

また、null条件演算子を使うこともできます。

C#
Console.WriteLine(person?.Name);

C#の参照を理解すると、NullReferenceExceptionが「参照先がないのに使おうとしたエラー」だと理解しやすくなります。

8-4. 参照先が同じかどうかを確認する方法

2つの変数が同じオブジェクトを参照しているかどうかを確認するには、ReferenceEqualsを使います。

C#
using System;

class Program
{
static void Main()
{
int[] a = { 1, 2, 3 };
int[] b = a;
int[] c = { 1, 2, 3 };

Console.WriteLine(object.ReferenceEquals(a, b));
Console.WriteLine(object.ReferenceEquals(a, c));
}
}

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

True
False

abは同じ配列を参照しています。一方、acは中身が同じでも、別々に作られた配列なので参照先は異なります。

8-5. ==とEqualsの違い

==Equalsの挙動は、型によって異なります。

値型では、基本的に値が同じかどうかを比較します。

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

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

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

True
True

参照型の場合、==が参照先を比較することもあれば、型によっては値の内容を比較するように定義されていることもあります。

たとえばstringでは、==は文字列の内容を比較します。

C#
string s1 = "Hello";
string s2 = "Hello";

Console.WriteLine(s1 == s2);
Console.WriteLine(s1.Equals(s2));

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

True
True

一方、配列では中身が同じでも、別々の配列であれば==Falseになります。

C#
int[] a = { 1, 2, 3 };
int[] b = { 1, 2, 3 };

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

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

False
False

配列の中身を比較したい場合は、SequenceEqualなどを使います。

C#
using System.Linq;

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

9. C#の参照を理解するための実践コード

9-1. 値型の代入を試すサンプル

値型の代入では、値がコピーされます。

C#
using System;

class Program
{
static void Main()
{
int a = 10;
int b = a;

b = 20;

Console.WriteLine($"a = {a}");
Console.WriteLine($"b = {b}");
}
}

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

a = 10
b = 20

bを変更しても、aには影響しません。

9-2. 参照型の代入を試すサンプル

参照型の代入では、参照がコピーされます。

C#
using System;

class Person
{
public string Name { get; set; }
}

class Program
{
static void Main()
{
Person p1 = new Person { Name = "Alice" };
Person p2 = p1;

p2.Name = "Bob";

Console.WriteLine(p1.Name);
Console.WriteLine(p2.Name);
}
}

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

Bob
Bob

p1p2は同じPersonオブジェクトを参照しているため、p2.Nameを変更すると、p1.Nameも変更されたように見えます。

9-3. 値渡しを試すサンプル

値渡しでは、メソッドに値のコピーが渡されます。

C#
using System;

class Program
{
static void Change(int x)
{
x = 999;
}

static void Main()
{
int number = 10;

Change(number);

Console.WriteLine(number);
}
}

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

10

メソッド内でxを変更しても、呼び出し元のnumberは変わりません。

9-4. refによる参照渡しを試すサンプル

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

C#
using System;

class Program
{
static void Change(ref int x)
{
x = 999;
}

static void Main()
{
int number = 10;

Change(ref number);

Console.WriteLine(number);
}
}

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

999

refによって、メソッド内の変更が呼び出し元に反映されます。

9-5. クラス・配列・Listで挙動を比較するサンプル

最後に、クラス、配列、List<T>の挙動を比較してみましょう。

C#
using System;
using System.Collections.Generic;

class Person
{
public string Name { get; set; }
}

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

static void ChangeArray(int[] array)
{
array[0] = 100;
}

static void ChangeList(List<int> list)
{
list.Add(999);
}

static void Main()
{
Person p = new Person { Name = "Alice" };
ChangePerson(p);
Console.WriteLine(p.Name);

int[] numbers = { 1, 2, 3 };
ChangeArray(numbers);
Console.WriteLine(numbers[0]);

List<int> list = new List<int> { 1, 2, 3 };
ChangeList(list);
Console.WriteLine(list.Count);
}
}

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

Bob
100
4

クラス、配列、List<T>はいずれも参照型です。メソッドには参照のコピーが渡されますが、同じオブジェクトを指しているため、中身を変更すると呼び出し元にも影響します。

10. C#で参照渡しやrefを使うべき場面・使わないほうがよい場面

10-1. refを使うべき場面

refを使うべき場面は、呼び出し元の変数そのものをメソッド内で変更したい場合です。

代表的な例として、値の入れ替えがあります。

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

また、大きな構造体を扱う場合にコピーコストを避ける目的で使うこともあります。

ただし、初心者のうちは「どうしても呼び出し元の変数を書き換える必要があるときだけ使う」と考えるとよいでしょう。

10-2. refを使わないほうがよい場面

refを使わないほうがよい場面も多くあります。

たとえば、単純に計算結果を返したいだけなら、戻り値を使ったほうが自然です。

C#
static int DoubleValue(int x)
{
return x * 2;
}

次のようにrefを使うこともできますが、通常は戻り値のほうが読みやすいです。

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

refを多用すると、どのメソッドがどの変数を変更するのか追いにくくなります。結果として、バグの原因になることもあります。

10-3. 可読性を下げないための注意点

refを使うときは、コードを読む人が「このメソッドは引数を変更する」とすぐにわかるようにすることが大切です。

C#では呼び出し側にもrefを書く必要があるため、ある程度は意図が明確になります。

C#
UpdateValue(ref value);

しかし、それでもメソッド名が曖昧だと読みづらくなります。たとえば、Processのような名前では、引数が変更されるのかどうかがわかりにくいです。

C#
Process(ref value);

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

10-4. 戻り値で対応できる場合は戻り値を使う

C#では、戻り値で対応できる場合は、基本的に戻り値を使うほうがわかりやすいです。

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

呼び出し側もシンプルです。

C#
int price = 1000;
int taxIncludedPrice = AddTax(price);

このように書けば、元のpriceを変更せず、新しい値として結果を受け取れます。

refを使うと、元の変数が書き換わるため、意図しない変更につながることがあります。特別な理由がない限り、戻り値を使う設計を優先しましょう。

10-5. 初心者が覚えるべき使い分けの基準

初心者が覚えるべき基準は、次のとおりです。

値型は、代入すると値がコピーされます。

参照型は、代入すると参照がコピーされます。

メソッド引数は、何も付けなければ値渡しです。

参照型を値渡しした場合、オブジェクトの中身は変更できますが、呼び出し元の変数が別のオブジェクトを指すようにはできません。

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

outは、メソッド内で値を設定して返すときに使います。

inは、読み取り専用で参照渡ししたいときに使います。

まずはこの基準を押さえておけば、C#の参照に関する多くの疑問を解消できます。

まとめ

C#の参照を理解するうえで重要なのは、「値型と参照型の違い」と「値渡しと参照渡しの違い」を分けて考えることです。

値型は、変数が値そのものを保持する型です。intdoubleboolstructなどが代表例です。値型を別の変数に代入すると、値がコピーされます。

参照型は、変数がオブジェクトへの参照を保持する型です。classstring、配列、List<T>objectなどが代表例です。参照型を別の変数に代入すると、オブジェクトそのものではなく参照がコピーされます。

C#のメソッド引数は、通常は値渡しです。値型を値渡しすると値がコピーされ、参照型を値渡しすると参照がコピーされます。参照型を値渡しした場合、同じオブジェクトを指すため中身は変更できますが、呼び出し元の変数が指す参照先そのものは変更できません。

呼び出し元の変数そのものを変更したい場合は、refを使います。refを使うと、値型でも参照型でも、メソッド内の変更を呼び出し元に反映できます。

ただし、refは便利な一方で、使いすぎるとコードの可読性が下がります。戻り値で対応できる場合は戻り値を使い、どうしても呼び出し元の変数を変更する必要がある場合に限定して使うのがおすすめです。

C#の参照、値渡し、参照渡し、refの違いを理解できると、配列、クラス、List、メソッド引数、NullReferenceExceptionなどの挙動が一気にわかりやすくなります。初心者のうちは、まず「参照型」と「参照渡し」は別物であることをしっかり押さえておきましょう。