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

広告

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

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

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

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

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

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

分類 何についての話か 代表例
値型・参照型 データ型の分類 intboolstructclass など
値渡し・参照渡し メソッド引数の渡し方 通常の引数、refoutin

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

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

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

intdoubleboolchar、列挙型、構造体などは値型です。

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 インスタンスそのものではなく、インスタンスを参照するための値です。そのため、player1player2 のどちらからプロパティを変更しても、同じインスタンスに変更が反映されます。

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

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

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

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. outin も「参照による引数渡し」

ref 以外にも、C#には outin があります。

キーワード 主な用途 主なルール
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 を使うと、呼び出し元の変数そのものを操作できる
  • outin も参照による引数渡しだが、それぞれルールが異なる

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

訪問数 4 回, 今日の訪問数 4回

広告