C#では、データの扱い方が「値型」と「参照型」で大きく異なります。
どちらも日常的に使いますが、代入やメソッド呼び出し、nullの扱いなどで混乱しやすいポイントが多いです。
本記事では、初心者の方でも直感的に理解できるよう、具体例とコードを使って値型と参照型の違いをやさしく解説します。
C#の値型と参照型とは
C#の型は大きく分けて2種類あります。
値型(value type)と参照型(reference type)です。
違いは「データそのものを持つか」「データの場所(参照)を持つか」にあります。
値型(value type)の基本ポイント
値型は値そのものを入れる箱のイメージです。
変数に代入するときは、その時点の値がまるごとコピーされます。
基本的な数値型やbool、構造体(struct)が該当します。
実行時には多くの場合、値はその場に直接格納されます(スタック上、またはオブジェクトの内部など)。
初心者の方は「コピーされる」という性質をまず押さえると理解が進みます。
参照型(reference type)の基本ポイント
参照型はデータ本体がある場所(アドレス)を指し示すイメージです。
変数が持つのは参照であり、同じ参照を複数の変数が共有できます。
class、string、配列、List<T>などが該当します。
実行時には実体(オブジェクト)はヒープに置かれ、変数はその参照を持つのが一般的です。
同じ参照を共有していると、片方の変更がもう片方にも影響します。
型の例一覧と見分け方
代表的な型を一覧で把握すると見分けやすくなります。
値型の代表例(int, double, bool, struct, enum)
値型の代表的な例としては次のようなものがあります。
C#の多くの基本型は値型です。
- 数値型: int, long, float, double, decimal
- 論理値: bool
- 文字: char
- 列挙: enum
- 構造体: struct(独自定義の値型)
参照型の代表例(class, string, array, List<T>)
参照型の代表的な例は次の通りです。
- クラス: class(独自定義の参照型)
- 文字列: string
- 配列: T[]
- コレクション: List<T>, Dictionary<TKey, TValue> など
stringは参照型で特別(不変)
stringは参照型ですが不変(immutable)です。
文字列の結合などで内容が変わるように見えても、実際は新しいstringインスタンスが作られます。
stringは参照型だが、内容は変更されないという点が他の参照型と異なるため、文字列操作では新しいインスタンスが増えがちです。
代入と変更の伝わり方の違い
ここからは実際のコードで、代入や変更の挙動を確認します。
値型は値をコピーして代入
値型は変数への代入時に値がコピーされます。
コピー先を変更しても元には影響しません。
using System;
// 値型の構造体。プロパティで座標を持ちます。
public struct PointValue
{
public int X { get; set; }
public int Y { get; set; }
}
class Program
{
static void Main()
{
var a = new PointValue { X = 1, Y = 2 };
var b = a; // 値のコピーが発生
b.X = 99; // bだけが変わる
Console.WriteLine($"a: X={a.X}, Y={a.Y}");
Console.WriteLine($"b: X={b.X}, Y={b.Y}");
}
}
a: X=1, Y=2
b: X=99, Y=2
参照型は同じデータを共有して代入
参照型の代入は参照(アドレス)のコピーです。
2つの変数が同じ実体を共有するため、片方の変更がもう片方にも見えます。
using System;
// 参照型のクラス。プロパティで座標を持ちます。
public class PointRef
{
// 自動実装プロパティ。{ get; set; }で簡潔に定義できます。
public int X { get; set; }
public int Y { get; set; }
// コンストラクタで初期化できます。
public PointRef(int x, int y)
{
X = x;
Y = y;
}
}
class Program
{
static void Main()
{
var c = new PointRef(1, 2);
var d = c; // 参照のコピー(同じオブジェクトを指す)
d.X = 99; // cもdも同じ実体のXが変わる
Console.WriteLine($"c: X={c.X}, Y={c.Y}");
Console.WriteLine($"d: X={d.X}, Y={d.Y}");
}
}
c: X=99, Y=2
d: X=99, Y=2
メソッド引数に渡したときの違い(やさしいイメージ)
C#のメソッド引数は既定では値渡しです。
値型を渡すと値(中身)のコピーが渡されるため、メソッド内で変更しても呼び出し元には影響しません。
参照型を渡すと参照のコピーが渡されます。同じ実体を指すため、メソッド内でオブジェクトのプロパティを変更すると呼び出し元からも見えます。
ただし、メソッド内で新しいオブジェクトを代入しても参照の再代入は呼び出し元には伝わりません(ref指定は別機能のためここでは扱いません)。
using System;
public struct PointValue
{
public int X { get; set; }
}
public class PointRef
{
public int X { get; set; }
}
class Program
{
static void BumpValue(PointValue p)
{
p.X += 10; // 値のコピーを変更。呼び出し元には影響しない
}
static void BumpRef(PointRef p)
{
p.X += 10; // 同じ実体のプロパティを変更。呼び出し元からも見える
}
static void ReplaceRef(PointRef p)
{
p = new PointRef { X = 999 }; // 参照の再代入はローカルだけに有効
}
static void Main()
{
var pv = new PointValue { X = 1 };
var pr = new PointRef { X = 1 };
BumpValue(pv);
BumpRef(pr);
Console.WriteLine($"pv.X = {pv.X}"); // 1のまま
Console.WriteLine($"pr.X = {pr.X}"); // 11に増える
ReplaceRef(pr);
Console.WriteLine($"pr.X(after replace) = {pr.X}"); // 11のまま(置き換わらない)
}
}
pv.X = 1
pr.X = 11
pr.X(after replace) = 11
nullの扱いとNullable(int?)
参照型はnullを取り得るため、未初期化や見つからない状態を表現できます。
一方、値型は通常nullを取れませんが、int?
のようにNullable構文を使えば値型にもnullを許可できます。
using System;
class Program
{
static void Main()
{
string name = null; // 参照型はnull可能
int age = 0; // 値型は通常null不可
int? score = null; // Nullable<int>。nullを扱える値型
Console.WriteLine(name == null ? "name is null" : name);
Console.WriteLine(score.HasValue ? score.Value.ToString() : "score is null");
// null合体演算子で既定値を用意
int finalScore = score ?? -1;
Console.WriteLine($"finalScore: {finalScore}");
// null条件演算子で安全にアクセス
Console.WriteLine($"name length: {name?.Length ?? 0}");
}
}
name is null
score is null
finalScore: -1
name length: 0
配列やList<T>を渡したときの注意
配列やList<T>は参照型です。
メソッドに渡して要素を変更すると呼び出し元からも変更が見えます。
意図せぬ共有に注意してください。
using System;
using System.Collections.Generic;
class Program
{
static void TouchArray(int[] arr)
{
arr[0] = 42; // 要素の変更は呼び出し元に影響
arr = new int[] { 9, 9, 9 }; // 参照の再代入は呼び出し元に伝わらない
}
static void TouchList(List<string> list)
{
list.Add("added"); // 内容の変更は共有される
list = new List<string> { "replaced" }; // 参照再代入はローカルのみ
}
static void Main()
{
var a = new[] { 1, 2, 3 };
var l = new List<string> { "a", "b" };
TouchArray(a);
TouchList(l);
Console.WriteLine($"array[0] = {a[0]}"); // 42
Console.WriteLine($"list: {string.Join(",", l)}"); // a,b,added
}
}
array[0] = 42
list: a,b,added
初心者が知っておく落とし穴
日常的に遭遇しやすいポイントを、挙動の違いとともに確認します。
==の比較は型で意味が変わる
値型の多くは==が値の等価性を比較します。
参照型の==は原則「同じ実体を指すか」(参照の同一性)を比較しますが、stringは内容の等価比較にオーバーライドされています。
using System;
public class Box
{
public int Value { get; set; }
}
class Program
{
static void Main()
{
int x = 5, y = 5;
Console.WriteLine(x == y); // 値の等価比較 → True
var b1 = new Box { Value = 10 };
var b2 = new Box { Value = 10 };
Console.WriteLine(b1 == b2); // 参照の同一性 → False (別インスタンス)
// stringは内容で等価比較される
var s1 = new string('a', 3); // "aaa"
var s2 = new string('a', 3); // "aaa"
Console.WriteLine(s1 == s2); // True (内容が同じ)
Console.WriteLine(object.ReferenceEquals(s1, s2)); // False (参照は別)
}
}
True
False
True
False
「==が常に内容比較」とは限らない点に注意しましょう。
参照型で内容の等価を扱いたい場合は、型がEqualsや==を適切に実装しているか確認します。
null参照例外(NullReferenceException)を避けるコツ
参照型変数がnullのままメンバーにアクセスすると例外になります。
以下の予防策が有効です。
- 使う前に必ずnullチェックを行う。
?.
や??
などの演算子を活用する。- 生成時にコンストラクタで必要なプロパティを初期化しておく。
- (上級) プロジェクトでNullable参照型を有効にし、コンパイラの警告で早期に気付けるようにする。
using System;
public class Person
{
public string Name { get; set; } // 参照型。nullの可能性に注意
public Person(string name)
{
// コンストラクタで初期化してnullを避ける
Name = name ?? throw new ArgumentNullException(nameof(name));
}
}
class Program
{
static void Main()
{
Person p = new Person("Alice");
Console.WriteLine(p.Name.Length); // 安全
string s = null;
// Console.WriteLine(s.Length); // 実行するとNullReferenceException
Console.WriteLine(s?.Length ?? 0); // 安全に0を表示
}
}
5
0
値型の既定値(default)と初期化
値型の既定値(default)は型ごとに決まっています。
intは0、boolはfalse、DateTimeは最小値などです。
参照型の既定値はnullです。
ローカル変数は明示的に代入しないと使えない点にも注意します。
using System;
class Program
{
static void Main()
{
int di = default; // 0
bool db = default; // false
DateTime dt = default; // 0001-01-01 00:00:00
string ds = default; // null
Console.WriteLine($"int: {di}, bool: {db}");
Console.WriteLine($"DateTime: {dt:yyyy-MM-dd HH:mm:ss}");
Console.WriteLine(ds == null ? "string is null" : ds);
}
}
int: 0, bool: False
DateTime: 0001-01-01 00:00:00
string is null
参考の早見表
下表は、初心者が混同しやすいポイントを短く比較したものです。
実装や最適化によって詳細は異なる場合がありますが、まずは次の感覚を目安にしてください。
観点 | 値型(value type) | 参照型(reference type) |
---|---|---|
代入 | 値そのものをコピー | 参照(アドレス)をコピー |
メソッド引数 | 値のコピーが渡る | 参照のコピーが渡る(内容変更は共有) |
既定値 | 型ごとに0やfalseなど | null |
null可 | 通常不可(Nullableで可) | 可 |
代表例 | int, double, bool, struct, enum | class, string, array, List<T> |
等価比較(==) | 値の等価が多い | 参照同一が既定(stringは内容比較) |
まとめ
値型は「値の箱」、参照型は「場所のしるし」と捉えると、代入や引数渡し、比較やnullの扱いが理解しやすくなります。
特に、参照型は同じ実体を共有しやすいため、メソッド内での変更や配列・List<T>の操作が呼び出し元に影響することを常に意識しましょう。
stringが参照型でありながら不変である点、==の意味が型によって異なる点、Nullableで値型にnullを許せる点も重要です。
まずは本記事のコードを手元で実行し、挙動を自分の目で確認してください。
少しずつ体感しながら、「コピーされる場面」と「共有される場面」を見分けられるようになると、C#のオブジェクト操作が格段に扱いやすくなります。