みなさんこんにちは!ヒロポンです!
メソッドに引数を渡して、中で書き換えた。なのに呼び出し元にはまったく反映されない。
かと思えば、別のメソッドでは書き換えたつもりがバッチリ返ってきてる。「あれ、同じ引数渡しなのに、なんで挙動が違うん??」って詰まったこと、ないですか?
この差の正体が、C# の値型と参照型の違いです。
int や struct は値型、class や配列は参照型。この2つは「変数が何を持ってるか」がそもそも違う。
ここを一回きちんと腹落ちさせておくと、引数まわりの「なんで反映されないの?」系のバグがごっそり減ります。
今回は、代入や引数渡しでのコピーの挙動から ref / out、struct と class の使い分けまで。スタックとヒープの図とコピペで動くコードで、順番に積み上げていきます。
忙しい人向けに最初にまとめ
- 値型(
int/DateTime/struct)は、変数が値そのものを持つ。代入や引数渡しで中身がまるごとコピーされる。 - 参照型(
class/ 配列 /List<T>/string)は、変数が実体の置き場所(参照)を持つ。コピーされるのは参照だけで、実体は共有される。 - だから、引数を値渡しすると値型は呼び出し元に返らない。参照型はフィールドの書き換えは返るが、
newでの差し替えは返らない。 - 呼び出し元に返したいときは
ref/outを使う。 structとclassは「小さい・不変・単一の値なら struct、それ以外は class」が基本の判断軸。
そもそも値型と参照型は何が違うのか
まずは言葉から整理します。
値型とは、変数がその値そのものを保持する型。参照型とは、変数が実体の置き場所(参照)を保持する型です。
たった一行の違い。でも、ここから後半のややこしい挙動が全部説明できます。
C# の型は、この2つのどちらかに必ず分類されます。ざっくり分けると次の通り。
| 分類 | 代表的な型 | 変数が持つもの |
|---|---|---|
| 値型 | int double bool DateTime decimal enum struct |
値そのもの |
| 参照型 | class string 配列 List<T> object |
実体への参照 |
なぜこう分かれてるのか。置き場所が違うからです。
値型は基本的にスタック(メソッドのローカル変数が積まれる高速な領域)に置かれる。参照型の実体はヒープ(もう少し寿命の長いデータを置く領域)に置かれて、変数はそこへの参照を持つ、という形になります。

図の上段が値型、下段が参照型です。
値型は 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 も変わる!!
b の Name を書き換えただけなのに、a.Name まで「花子」になる。
b = a でコピーされたのは参照だけで、a も b も同じ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 のまま渡したい。そういうときは、さっきの in を付けてコピーを避ける手もあります。いずれにせよ「大きい=class 寄り」で考えておくと安全です。
ここまでで、struct と class の使い分けの軸と、「大きい 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 は参照型。「struct か class か」で切ると覚えやすいです。組み込み型の分類は 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 で経歴詳細を見る



