C#の「値型・参照型」と「値渡し・参照渡し」は別の話

広告

C#を学んでいると、「値型」「参照型」「値渡し」「参照渡し」という似た言葉が登場します。ここでよくある疑問が、「参照型をメソッドに渡すと、参照渡しになるのでは?」というものです。

結論から言うと、値型・参照型と、値渡し・参照渡しは別の軸です。

  • 値型・参照型:データ型の分類
  • 値渡し・参照渡し:メソッド引数の渡し方

まずは、全体像を4象限で確認しましょう。

C#の値型・参照型と値渡し・参照渡しを別の軸として整理した4象限図
値型・参照型はデータ型の分類、値渡し・参照渡しはメソッド引数の渡し方

最初に結論:2つの言葉は見ている対象が違う

分類 何についての話か 代表例
値型・参照型 データ型の分類 int、bool、struct、class など
値渡し・参照渡し メソッド引数の渡し方 通常の引数、ref、out、in

C#では、引数に ref、out、in を付けない通常の渡し方は、型が値型でも参照型でも値渡しです。

1. 値型と参照型は「データ型の分類」

値型では値そのものがコピーされる

int、double、bool、char、列挙型、構造体などは値型です。

int a = 10;
int b = a;

b = 20;

Console.WriteLine(a); // 10
Console.WriteLine(b); // 20

b = a では、a が持つ値が b にコピーされます。その後で b を変更しても、a は変わりません。

参照型では参照の値がコピーされる

クラス、配列、文字列、デリゲートなどは参照型です。次の例では、2つの変数が同じ Player インスタンスを参照します。

class Player
{
    public int Hp { get; set; }
}

var player1 = new Player { Hp = 100 };
var player2 = player1;

player2.Hp = 50;

Console.WriteLine(player1.Hp); // 50

player2 = player1 でコピーされるのは、Player インスタンスそのものではなく、インスタンスを参照するための値です。そのため、player1 と player2 のどちらからプロパティを変更しても、同じインスタンスに変更が反映されます。

注意:値型は必ずスタック、参照型は必ずヒープ、という単純な整理は正確ではありません。この記事では格納場所ではなく、代入や引数渡しで「何がコピーされるか」に注目します。

2. 通常の引数は型に関係なく「値渡し」

メソッドの仮引数に ref、out、in が付いていなければ、引数は値渡しです。

static void A(int x)        // 値型を値渡し
{
}

static void B(Player p)     // 参照型を値渡し
{
}

値渡しとは、呼び出し時に変数が持っている値を仮引数へコピーすることです。

  • int なら、数値がコピーされる
  • Player なら、参照の値がコピーされる

3. 参照型を値渡しすると何が起きる?

ここが最も混乱しやすい部分です。参照型を値渡しすると、呼び出し元とメソッド内の変数は別の変数ですが、コピーされた参照の値は同じインスタンスを指します。

インスタンスの内容は変更できる

static void Damage(Player p)
{
    p.Hp = 50;
}

var player = new Player { Hp = 100 };
Damage(player);

Console.WriteLine(player.Hp); // 50

p.Hp の変更が呼び出し元からも見えるのは、参照渡しだからではありません。値渡しでコピーされた2つの参照が、同じインスタンスを指しているからです。

仮引数を別のインスタンスへ差し替えても、呼び出し元の変数は変わらない

static void Replace(Player p)
{
    p = new Player { Hp = 999 };
}

var player = new Player { Hp = 100 };
Replace(player);

Console.WriteLine(player.Hp); // 100

p は参照の値を受け取った別の変数です。メソッド内で p を差し替えても、呼び出し元の player には影響しません。

4. 値型でも ref を使えば参照渡しにできる

ref を付けると、メソッドはコピーではなく、呼び出し元の変数そのものを参照して操作します。

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

int number = 5;
AddTen(ref number);

Console.WriteLine(number); // 15

int は値型ですが、この引数の渡し方は参照渡しです。つまり、値型だから必ず値渡し、というわけではありません。

5. 参照型を ref で渡すと、呼び出し元の参照変数を差し替えられる

static void Replace(ref Player p)
{
    p = new Player { Hp = 999 };
}

var player = new Player { Hp = 100 };
Replace(ref player);

Console.WriteLine(player.Hp); // 999

この例では、メソッド内の p が呼び出し元の player そのものを参照します。そのため、別の Player インスタンスを代入すると、呼び出し元の変数も差し替わります。

6. out と in も「参照による引数渡し」

ref 以外にも、C#には out と in があります。

キーワード 主な用途 主なルール
ref 値を受け取り、必要に応じて書き換える 呼び出し前に初期化が必要
out メソッドから値を返す 呼び出し前の初期化は不要。メソッド内で代入が必要
in 読み取り専用の参照として受け取る 仮引数そのものへ再代入できない。参照先のオブジェクトが不変になるわけではない
static void CreatePlayer(out Player p)
{
    p = new Player { Hp = 100 };
}

CreatePlayer(out Player player);
Console.WriteLine(player.Hp); // 100

out は「出力用の参照渡し」と説明されることが多いものの、ref とは初期化や代入に関するルールが異なります。

4象限で整理

データ型の分類 引数の渡し方 例 コピー・操作の対象
値型 値渡し void A(int x) 値そのものをコピー
値型 参照渡し void B(ref int x) 呼び出し元の変数を操作
参照型 値渡し void C(Player p) 参照の値をコピー
参照型 参照渡し void D(ref Player p) 呼び出し元の参照変数を操作

よくある誤解

誤解1:参照型を渡したら参照渡し

いいえ。void M(Player p) は、参照型を値渡ししています。コピーされる値が参照である、という意味です。

誤解2:メソッド内でオブジェクトが変わったから参照渡し

いいえ。参照型の値渡しでも、呼び出し元と仮引数が同じインスタンスを指していれば、そのインスタンスの変更は双方から見えます。

誤解3:ref は参照型のための機能

いいえ。ref int のように値型にも使えます。ref が変えるのはデータ型の分類ではなく、引数の渡し方です。

まとめ

  • 値型・参照型は、データ型の分類
  • 値渡し・参照渡しは、メソッド引数の渡し方
  • 通常の引数は、値型でも参照型でも値渡し
  • 参照型の値渡しでは、参照の値がコピーされる
  • ref を使うと、呼び出し元の変数そのものを操作できる
  • out と in も参照による引数渡しだが、それぞれルールが異なる

迷ったときは、「型の分類の話か」「引数の渡し方の話か」を先に切り分けてください。この2つを別の軸として考えると、C#の引数渡しをすっきり理解できます。

訪問数 27 回, 今日の訪問数 1回

広告