動くコード図鑑技術記事現場の渡り方キャリア論すべての記事About
技術記事

C# の値型と参照型の違い(引数を書き換えたのに反映されない/される、の正体)

バイブス父さん
現役の業務SE
2026年8月22日16 min read広告 (PR) を含む場合があります
C# の値型と参照型の違い(引数を書き換えたのに反映されない/される、の正体)

みなさんこんにちは!ヒロポンです!

メソッドに引数を渡して、中で書き換えた。なのに呼び出し元にはまったく反映されない。

かと思えば、別のメソッドでは書き換えたつもりがバッチリ返ってきてる。「あれ、同じ引数渡しなのに、なんで挙動が違うん??」って詰まったこと、ないですか?

この差の正体が、C# の値型と参照型の違いです。

intstruct は値型、class や配列は参照型。この2つは「変数が何を持ってるか」がそもそも違う。

ここを一回きちんと腹落ちさせておくと、引数まわりの「なんで反映されないの?」系のバグがごっそり減ります。

今回は、代入や引数渡しでのコピーの挙動から ref / outstructclass の使い分けまで。スタックとヒープの図とコピペで動くコードで、順番に積み上げていきます。

忙しい人向けに最初にまとめ

  • 値型int / DateTime / struct)は、変数が値そのものを持つ。代入や引数渡しで中身がまるごとコピーされる。
  • 参照型class / 配列 / List<T> / string)は、変数が実体の置き場所(参照)を持つ。コピーされるのは参照だけで、実体は共有される。
  • だから、引数を値渡しすると値型は呼び出し元に返らない。参照型はフィールドの書き換えは返るが、new での差し替えは返らない。
  • 呼び出し元に返したいときは ref / out を使う。
  • structclass は「小さい・不変・単一の値なら struct、それ以外は class」が基本の判断軸。

そもそも値型と参照型は何が違うのか

まずは言葉から整理します。

値型とは、変数がその値そのものを保持する型。参照型とは、変数が実体の置き場所(参照)を保持する型です。

たった一行の違い。でも、ここから後半のややこしい挙動が全部説明できます。

C# の型は、この2つのどちらかに必ず分類されます。ざっくり分けると次の通り。

分類 代表的な型 変数が持つもの
値型 int double bool DateTime decimal enum struct 値そのもの
参照型 class string 配列 List<T> object 実体への参照

なぜこう分かれてるのか。置き場所が違うからです。

値型は基本的にスタック(メソッドのローカル変数が積まれる高速な領域)に置かれる。参照型の実体はヒープ(もう少し寿命の長いデータを置く領域)に置かれて、変数はそこへの参照を持つ、という形になります。

値型はb=aで値がまるごとコピーされ、参照型はy=xで参照だけコピーされて同じ実体を共有する図

図の上段が値型、下段が参照型です。

値型は b = a した時点で、中身が完全に別物としてコピーされる。参照型は y = x しても、コピーされるのは「実体はこっちにあるよ」という参照だけ。実体そのものは1個をみんなで指す、という形になります。

ここで押さえておきたいのは「値型は値をコピー、参照型は住所をコピー」の一点だけ。これさえ握っておけば大丈夫です。

代入で「値がコピーされる」を確かめる

概念だけだとピンと来ないので、実際に代入で動かして確かめます。まずは値型から。

// 値型 (int) の代入
int a = 5;
int b = a;   // 値がまるごとコピーされる
b = 99;
Console.WriteLine($"a={a}, b={b}");   // a=5, b=99

b を 99 に変えても、a は 5 のまま。b = a の時点で中身が別物としてコピーされてるので、片方をいじってももう片方には一切影響しません。struct も同じ挙動になります。

// 値型 (struct) の代入も中身がまるごとコピー
struct Point { public int X; public int Y; }

Point p1 = new Point { X = 1, Y = 2 };
Point p2 = p1;   // X も Y も全部コピー
p2.X = 99;
Console.WriteLine($"p1.X={p1.X}, p2.X={p2.X}");   // p1.X=1, p2.X=99

一方、参照型はこうなります。

// 参照型 (class) は参照だけコピー → 同じ実体を共有
class Person { public string Name; }

Person a = new Person { Name = "太郎" };
Person b = a;    // 「実体の住所」だけコピー
b.Name = "花子";
Console.WriteLine($"a.Name={a.Name}");   // 花子  ← a も変わる!!

bName を書き換えただけなのに、a.Name まで「花子」になる。

b = a でコピーされたのは参照だけで、ab同じ1個の実体を指してるからです。これがいい感じに事故る典型パターンなんですよね。参照型の代入、ほんま気をつけてください!!

ここまでで分かったのは、代入時に「値型は中身が別れる/参照型は同じ実体を指し続ける」ということ。次はこの性質が引数渡しでどう出るかを見ます。

引数を書き換えても反映されない/される、の正体

冒頭の「引数を書き換えたのに反映されない」問題に戻ります。C# の引数渡しは、指定しない限り値渡し(コピーを渡す)が基本です。

値型を値渡しすると、メソッドの中で書き換えても、それはコピーをいじってるだけ。呼び出し元には返りません。

void AddOne(int n) { n = n + 1; }   // コピーをいじってるだけ

int x = 10;
AddOne(x);
Console.WriteLine(x);   // 10  ← 変わらない

では参照型を渡すとどうか。ここが一番混乱するポイントなので、2つに分けます。

class Person { public string Name; }

// ケース1: フィールドを書き換える → 反映される
void Rename(Person p) { p.Name = "変更後"; }

var person = new Person { Name = "元" };
Rename(person);
Console.WriteLine(person.Name);   // 変更後  ← 反映される
// ケース2: 引数そのものを new で差し替える → 反映されない
void Replace(Person p) { p = new Person { Name = "別人" }; }

var person = new Person { Name = "元" };
Replace(person);
Console.WriteLine(person.Name);   // 元  ← 反映されない

同じ参照型なのに、ケース1は反映されてケース2は反映されない。

ぶっちゃけ、私も昔ここで「え、参照型って渡したら中で書き換えたのが返るんちゃうの??」って混乱しました。

正体はこうです。メソッドに渡されるのは「参照のコピー」。

だからケース1のように、コピーした参照を辿って同じ実体のフィールドを書き換えると呼び出し元に返る。でもケース2のように、参照の変数自体を new で差し替えると、それはコピーした側の変数を別の実体に向けただけ。呼び出し元の変数は元の実体を指したままなんですよね。

ハマりポイント①:「参照型を渡せば中の変更は全部返る」と思ってると、ケース2の new 差し替えで返らずにハマります。返るのは実体への操作であって、参照の差し替えではない、と切り分けて覚えると事故りません。

ここまでで、「値型は返らない/参照型は実体への操作なら返る・参照の差し替えは返らない」が整理できました。次は、値型でも呼び出し元に返す方法を見ます。

ref と out で値型も参照渡しにする

値型を値渡しすると返らない、と書きました。でも「メソッドの中で計算した結果を呼び出し元の変数に返したい」場面はあります。

そういうときは ref を使って参照渡しにします。

void AddOneRef(ref int n) { n = n + 1; }

int x = 10;
AddOneRef(ref x);   // 呼び出し側にも ref を付ける
Console.WriteLine(x);   // 11  ← 反映される!!

ref を付けると、値型でも「その変数そのもの」への参照が渡る。だから中での書き換えが呼び出し元にちゃんと返ります!!呼び出し側でも ref を明示するのがルールになってます。

似たものに out があります。ref は「呼び出す前から値が入ってる変数」を渡すのに対し、out は「メソッドの中で必ず値を入れて返す(呼び出し前は未初期化でOK)」用途。おなじみの int.TryParse がこれです。

if (int.TryParse("123", out int result))
    Console.WriteLine(result);   // 123

もう1つ、読み取り専用で参照だけ渡したいときの in があります(in は C# 7.2 から。VS2019 の C# 7.3 環境でも使えます)。大きい struct をコピーせず、でも書き換えはさせたくない、という場面で効きます。この話は後半の struct の使い分けでもう一度出てきます。

修飾子 呼び出し前の値 メソッド内で 主な用途
なし(値渡し) 必要 コピーをいじる 通常の引数
ref 必要 読み書きして返す 呼び出し元の変数を更新
out 不要 必ず代入して返す TryParse 系・複数の戻り値
in 必要 読み取り専用 大きい struct をコピーせず渡す

ここまでで、値型でも ref / out を使えば呼び出し元に返せる、という手段が揃いました。次は、参照型なのに値型っぽく見える string の話です。

string が「参照型なのに値型っぽい」理由

string は参照型です。なのに、代入や比較の挙動が値型っぽく見える。これがまた混乱の種になります。

理由は2つ。不変(immutable)であることと、== が値比較にオーバーロードされてることです。

string s1 = "abc";
string s2 = s1;
s2 = s2 + "d";   // 書き換えではなく「新しい実体」が作られる
Console.WriteLine($"s1={s1}, s2={s2}");   // s1=abc, s2=abcd

さっきの class の例だと、参照をコピーした b をいじると a も変わりました。でも string は不変。+ "d" した瞬間にまったく別の新しい string 実体が作られて、s2 はそっちを指します。

だから s1 は巻き込まれず「abc」のまま。書き換えてるように見えて、実際は毎回作り直してるわけです。

比較も独特です。

string x = "hello";
string y = "hello";
Console.WriteLine(x == y);   // True  ← 中身で比較される

普通の参照型は == が「同じ実体か(参照が一致するか)」を見ます。でも string== が中身の比較にオーバーロードされてる。だから別々に作った "hello" 同士でも True になります。

ハマりポイント②:「string は参照型」と暗記だけしてると、この不変&値比較の挙動で「あれ、参照型のはずやのに値型みたいに動く??」と混乱します。string参照型だけど不変、と2語セットで覚えるのが正解。ちなみに == の挙動を厳密に扱いたいときは StringComparison を指定する話とセットになるので、そこは別記事に譲ります。

ここで分かったのは、string の値型っぽさは「不変+== オーバーロード」で説明できる、ということ。分類上はあくまで参照型です。

struct と class をどう使い分けるか

値型と参照型の性質が分かると、自作の型を struct(値型)にするか class(参照型)にするか、の判断軸も見えてきます。.NET の設計ガイドライン(Microsoft Learn「クラスと構造体の選択」)をかみ砕くと、こんな感じです。

観点 struct(値型)向き class(参照型)向き
サイズ 小さい(目安16バイト以下) 大きい
変わるか 不変(作ったら変えない) 状態が変わる
意味 単一の値(座標・金額・期間) 実体・エンティティ(顧客・注文)
コピー頻度 頻繁にコピーしても軽い コピーは参照ぶんだけで済ませたい

ざっくり言うと、座標や金額みたいな「小さくて不変な単一の値」は struct、顧客や注文みたいな「状態を持つ実体」は class。業務系のDTOやエンティティは、ほぼ class で問題ないです。

ここで気をつけたいのが、struct を安易に大きくすると逆に遅くなること。

値型は引数渡しで毎回まるごとコピーされます。だからフィールドを増やして大きくすると、渡すたびのコピーコストが効いてきます。

大きなstructを値渡ししてコピーが重い状態を、classにするか in で読み取り専用参照渡しに直すdiff

読み取り専用で、どうしても struct のまま渡したい。そういうときは、さっきの in を付けてコピーを避ける手もあります。いずれにせよ「大きい=class 寄り」で考えておくと安全です。

ここまでで、structclass の使い分けの軸と、「大きい struct のコピーコスト」という落とし穴が押さえられました。

私の現場ではこう考えてる

正直、業務系の保守をやってると、自分で struct を切る場面ってそんなに多くないです。DTOもエンティティもほぼ class で足りる。私も基本は「迷ったら class」で組んでます。

struct を意識したのは、集計処理で1行を小さな値の塊として大量に回したときでした。

DataTable から読んだ行を「軽そうだから」と struct に詰めて、List に何万件も入れてループを回した。したら、想定より重い。プロファイラで追ったら、foreach のたびに struct がコピーされてて、そのコピーが積み上がってた、というオチでした。

「値型は軽い」って思い込みが逆に効いてた形で、けっこう時間を溶かしました。

そこで学んだのが、「小さくて不変なら struct が効く、でも大きくして大量に回すなら class か in」という、まさに設計ガイドライン通りの結論。教科書に書いてある話を、こんな感じで遠回りして体で覚えたわけです。

あと現場で地味に効くのが、冒頭の引数の話。

「この引数、値型やから中で書き換えても返らへんよ」とか「これ参照型やから、渡した先でフィールド触られたら元も変わるで」を、コードレビューでサッと指摘できる。これができると、原因不明の「なんか値が書き換わってる/書き換わってない」系バグの発生源を、かなり潰せます。

まとめ

C# の値型と参照型の違いは、突き詰めると一行です。

値型は値そのものを持つ(コピーで中身が別れる)。参照型は実体への参照を持つ(コピーしても実体は共有)。

この一点さえ握れば、

  • 引数を値渡しした値型が返らないのも、
  • 参照型のフィールド書き換えが返って、new 差し替えが返らないのも、
  • string が参照型なのに値型っぽく振る舞うのも、

全部同じ理屈で説明がつきます。

ref / out は「値型でも返したいとき」、in は「大きい値型をコピーせず読み取りで渡したいとき」の道具。そう位置づけて覚えておけばOKです。

引数まわりの「なんで反映されるんだっけ/されないんだっけ」を毎回悩まなくて済むようになると、地味ですが日々のデバッグ時間がちゃんと減ります。型の分類を一度腹落ちさせておく価値は、じゅうぶんありますよ。

よくある質問

値型と参照型は、どうやって見分ければいいですか?

struct / enum と、int double bool DateTime decimal などの組み込み型は値型です。class と、string・配列・List<T>object は参照型。「structclass か」で切ると覚えやすいです。組み込み型の分類は Microsoft Learn の言語リファレンスで確認できます。

List に入れた要素は値型ですか参照型ですか?

List<T> 自体は参照型ですが、中の要素が値型か参照型かは T によります。List<int> の要素は値型(int)、List<Person>(class)の要素は参照型です。List<構造体> の場合、インデクサ経由で取り出した要素はコピーになる点に注意すると事故りません。

引数に out を付けるとき、呼び出し前に変数を初期化しなくていいのはなぜですか?

out は「メソッドの中で必ず値を入れて返す」約束の修飾子だからです。呼び出し前は未初期化でよく、逆にメソッド側は全ての out 引数に代入しないとコンパイルエラーになります。int.TryParse が代表例です。

struct を使うと本当に速くなりますか?

小さく・不変で・短命な値なら、ヒープ確保やGCの負担を減らせて効くことがあります。ただし大きい struct は引数渡しや代入のたびにコピーが発生するので、大量に回すと逆に遅くなることも。「小さいなら効く、大きいなら class か in」で判断するのが安全です。

struct を class に変えるだけで既存コードは動きますか?

コンパイルは通ることが多いですが、挙動が変わる場合があります。値型は代入でコピーされ、参照型は実体を共有するため、「代入した先を書き換えたら元も変わる」ようになります。値型前提で組まれたコードを class に変えるときは、共有による意図しない書き換えがないかを確認してください。

次に読むべき記事

以上!


執筆者

バイブス父さん — 業務 SE 7 年 (SIer 正社員 2 / フリーランス 5)。 現職は SEO 直轄部の AI アドバイザー兼 PL、 副業で中小 SIer の CTO。 SIer の正社員からフリーランスに転じ、 複数のエージェント経由で案件を回してきた経験ベースで「業務 SE 視点」 の技術 + キャリア記事を書いています。

🐦 X: @hiro_progra0524 (日々の現場メモ更新中)
📝 About Me で経歴詳細を見る


この記事のコードと手順は ぜんぶ動作検証済み。 安心して現場で試してくれ。
バイブス父さん

現役の業務SE。C# / SQL Server 保守の現場から、コードも人もキャリアも全部書く。 実体験ベース。

運営者について