1. はじめに
箱を持っていると想像してみて。もしその箱が中身そのもの(例えばリンゴが入ってる箱)なら、それは値型みたいなもの。データ自体がこの「箱」(変数)の中に直接入ってるイメージだね。
逆に、住所が書かれた名刺を持っていると想像してみて。名刺自体は家じゃなくて、どこに家があるかを示してるだけ。これが参照型のイメージ。変数はデータそのものじゃなくて、「名刺」=メモリ上のアドレスを持ってる感じ。
超シンプルな例:
- int x = 5; // 値型:変数xは5という数字を直接「持ってる」
- string name = "ヴァーシャ"; // 参照型:変数nameは「ヴァーシャ」という文字列がどこかのメモリにある場所を「指してる」だけ
この世界で誰がどっち?
分かりやすくするために、主なデータ型がどっちのカテゴリかざっくりリストアップするね:
値型 (Value Types):- プリミティブ型: int, double, float, bool, char, byte, short, long, decimal など
- 構造体 (struct): structキーワードで宣言した自作の構造体全部
- 列挙型 (enum): 名前付き定数のセットを定義できる型
- 文字列 (string): 文字列はちょっと特殊(イミュータブル)だけど、参照型だよ
- すべての配列: 例:int[], string[], YourCustomClass[]
- すべてのクラス (class): classキーワードで宣言した自作クラス全部
- デリゲート (delegate): メソッドへの参照を表す型
- インターフェース (interface): インターフェース自体はオブジェクトじゃないけど、インターフェース型の変数はそのインターフェースを実装したオブジェクトへの参照を持てる
- リスト・辞書・その他コレクション: 例:List<T>, Dictionary<TKey, TValue>
- とにかく、structやenumじゃないものは、C#ではデフォルトで参照型だよ
2. 変数のコピー
ここが実は一番大事な違い。変数を別の変数に代入したとき、実際に何がコピーされるの?
A) 値型 (Value Type) のコピー
値型をコピーすると、完全に独立したコピーが作られる。ドキュメントをコピー機で複製する感じ。どっちかを変えても、もう一方には影響なし。
int a = 10;
int b = a; // bも10だけど、これは「自分のコピー」
Console.WriteLine($"初期値: a = {a}, b = {b}"); // 初期値: a = 10, b = 10
b = 15; // bを変更
Console.WriteLine($"bを変更後: a = {a}, b = {b}"); // bを変更後: a = 10, b = 15
解説: 変数bは10の自分だけのコピーを持ってる。bを15に変えても、aは元の10のまま。完全に独立してるよ。
B) 参照型 (Reference Type) のコピー
参照型をコピーすると、参照(アドレス)だけがコピーされる。つまり、2つの変数が同じオブジェクトを指すことになる。2人に同じ名刺を渡すイメージ。どちらかが家の壁を塗り替えたら、もう一方もその変化を見ることになる。
配列で見てみよう(文字列はちょっと特殊だから配列が分かりやすい):
int[] arr1 = {1, 2, 3};
int[] arr2 = arr1; // arr2もarr1も同じ配列を指してる!
// 初期値: arr1[0] = 1, arr2[0] = 1
Console.WriteLine($"初期値: arr1[0] = {arr1[0]}, arr2[0] = {arr2[0]}");
arr2[0] = 42; // arr2経由で配列の要素を変更
// arr2[0]を変更後: arr1[0] = 42, arr2[0] = 42
Console.WriteLine($"arr2[0]を変更後: arr1[0] = {arr1[0]}, arr2[0] = {arr2[0]}");
解説: どちらの変数(arr1とarr2)も同じ配列を指してる。arr2[0]を変えると、実際はその配列自体が変わるから、arr1[0]も変わった値になる。
文字列(string)のちょっとしたクセ
C#の文字列は参照型だけど、イミュータブル(変更不可)だからちょっと挙動が違う。作った後は文字列自体を変更できない。何か「変更」する操作(連結やReplace()など)は、実は新しい文字列を作ってるだけ。
string str1 = "Hello";
string str2 = str1; // str2もstr1と同じ"Hello"を指してる
// str1 = "Hello", str2 = "Hello"
Console.WriteLine($"初期値: str1 = \"{str1}\", str2 = \"{str2}\"");
str2 = "Bye"; // ここで新しい"Bye"オブジェクトが作られ、str2はそっちを指す
// str1 = "Hello", str2 = "Bye"
Console.WriteLine($"str2を変更後: str1 = \"{str1}\", str2 = \"{str2}\"");
解説: 最初はstr1とstr2は同じ"Hello"を指してたけど、str2に"Bye"を代入した時、C#は既存の"Hello"を変えずに新しい"Bye"を作って、str2だけがそっちを指すようになる。str1はそのまま"Hello"。この違い、初心者はよく混乱するから注意!
3. 主な違いの表
| 特徴 | 値型 (例: struct, int) |
参照型 (例: class, string, 配列) |
|---|---|---|
| 何がコピーされる? | 値そのもの(データの「コピー」) | オブジェクトへの参照(メモリアドレス) |
| コピー同士の関係 | なし。完全に独立。片方を変えてももう片方は変わらない。 | あり。全部同じオブジェクトを指す。どれかでオブジェクトを変えると全部に反映される。 |
| nullになれる? | できない(Nullable型、例:int?は例外)。必ず値を持つ。 | できる。「何も指してない」(null)状態になれる。nullなオブジェクトにアクセスしようとするとNullReferenceExceptionになる。 |
| 宣言方法 | struct、プリミティブ型(int, boolなど)、enum | class, interface, delegate, array, string, object |
| 消去の仕組み | スコープを抜けると自動的にスタックから消える | 参照がなくなった時にガベージコレクタ(Garbage Collector)が消してくれる |
4. アプリでの例
例えば、ユーザーアンケートの簡単なコンソールアプリを作るとしよう。試験の点数用の構造体と、ユーザープロファイル用のクラスがあるとする。
// 値型:点数を保存する構造体
struct Score
{
public int Points;
public string Grade; // 分かりやすくするために追加
}
// 参照型:ユーザープロファイル用クラス
class User
{
public string Name;
public Score ExamScore; // 構造体を内包
}
構造体 (Score) のコピー
Score score1 = new Score { Points = 100, Grade = "A" };
Score score2 = score1; // score1の中身が全部score2にコピーされる
Console.WriteLine($"score1: Points={score1.Points}, Grade={score1.Grade}"); // score1: Points=100, Grade=A
Console.WriteLine($"score2: Points={score2.Points}, Grade={score2.Grade}"); // score2: Points=100, Grade=A
score2.Points = 88;
score2.Grade = "B";
Console.WriteLine("--- score2を変更後 ---");
Console.WriteLine($"score1: Points={score1.Points}, Grade={score1.Grade}"); // score1: Points=100, Grade=A(変わらない!)
Console.WriteLine($"score2: Points={score2.Points}, Grade={score2.Grade}"); // score2: Points=88, Grade=B
結果: score1はそのまま。Score score2 = score1;でscore1の中身(フィールド全部)がscore2にコピーされたから、両方独立したデータを持ってる。
クラス (User) のコピー
User u1 = new User
{
Name = "アンナ",
ExamScore = new Score { Points = 95, Grade = "A" }
};
User u2 = u1; // u2もu1も同じユーザーを指してる!
Console.WriteLine($"u1: Name={u1.Name}, Score={u1.ExamScore.Points}"); // u1: Name=アンナ, Score=95
Console.WriteLine($"u2: Name={u2.Name}, Score={u2.ExamScore.Points}"); // u2: Name=アンナ, Score=95
u2.Name = "イワン"; // u2経由で名前を変更
u2.ExamScore.Points = 60; // u2経由で点数を変更
u2.ExamScore.Grade = "C";
Console.WriteLine("--- u2を変更後 ---");
Console.WriteLine($"u1: Name={u1.Name}, Score={u1.ExamScore.Points}, Grade={u1.ExamScore.Grade}"); // u1: Name=イワン, Score=60, Grade=C
Console.WriteLine($"u2: Name={u2.Name}, Score={u2.ExamScore.Points}, Grade={u2.ExamScore.Grade}"); // u2: Name=イワン, Score=60, Grade=C
結果: u1も変わっちゃった!これはu1とu2が最初から同じUserオブジェクトを指してるから。u2.Name = "イワン";みたいにプロパティを変えると、実際のオブジェクト自体が変わるから、u1.Nameも変わる。内包してる構造体ExamScoreも同じ理由で変わるよ。
5. メソッドに渡すとどうなる?
型をメソッドに渡すときの挙動を理解するのは、プログラムの予測しやすさに超重要!
値型 (Value Type) をメソッドに渡す
値型をメソッドに渡すと、デフォルトで値渡しになる。つまり、メソッドはコピーを受け取る。メソッド内でそのコピーを変えても、元の変数には影響なし。
void AddTen(int x)
{
Console.WriteLine($"メソッド内(変更前): x = {x}"); // メソッド内(変更前): x = 5
x = x + 10; // xは15になるけど、これはローカルコピー
Console.WriteLine($"メソッド内(変更後): x = {x}"); // メソッド内(変更後): x = 15
// このローカルなxはメソッド終了後に消える
}
int num = 5;
Console.WriteLine($"メソッド呼び出し前: num = {num}"); // メソッド呼び出し前: num = 5
AddTen(num);
Console.WriteLine($"メソッド呼び出し後: num = {num}"); // メソッド呼び出し後: num = 5(変わらない!)
結果: 変数numはそのまま(5)。AddTen内のxはnumの値をコピーしただけの別物。
参照型 (Reference Type) をメソッドに渡す
参照型もデフォルトでは値渡しだけど、コピーされるのは参照(アドレス)。つまり、メソッド内でも元のオブジェクトを指してる。どっちの参照からも同じオブジェクトを操作できる。
void RenameUser(User u)
{
// メソッド内(変更前): u.Name = "オリガ"
Console.WriteLine($"メソッド内(変更前): u.Name = \"{u.Name}\"");
u.Name = "新しい名前"; // オブジェクトのプロパティを変更
// メソッド内(変更後): u.Name = "新しい名前"
Console.WriteLine($"メソッド内(変更後): u.Name = \"{u.Name}\"");
}
User user = new User { Name = "オリガ" };
// メソッド呼び出し前: user.Name = "オリガ"
Console.WriteLine($"メソッド呼び出し前: user.Name = \"{user.Name}\"");
RenameUser(user);
// メソッド呼び出し後: user.Name = "新しい名前"
Console.WriteLine($"メソッド呼び出し後: user.Name = \"{user.Name}\"");
結果: ユーザー名が「新しい名前」に変わった。RenameUserはuserの参照のコピーを受け取って、その参照経由でオブジェクトのプロパティを変えたから、元のuserも変わる。
重要な補足: もしメソッド内で参照型変数に新しいオブジェクトを代入したらどうなる?
void ReassignUser(User u)
{
u = new User { Name = "まったく新しいユーザー" }; // uは新しいオブジェクトを指すようになる
Console.WriteLine($"メソッド内(再代入後): u.Name = \"{u.Name}\"");
}
User originalUser = new User { Name = "オリジナルユーザー" };
Console.WriteLine($"ReassignUser呼び出し前: originalUser.Name = \"{originalUser.Name}\"");
ReassignUser(originalUser);
Console.WriteLine($"ReassignUser呼び出し後: originalUser.Name = \"{originalUser.Name}\""); // "オリジナルユーザー"のまま!
結果: originalUserは変わらない!ReassignUserは参照のコピーを受け取ってるだけなので、u = new User(...)でローカル変数uが新しいオブジェクトを指すだけ。元のoriginalUserはそのまま古いオブジェクトを指してる。これ、超大事!
6. 「初心者の涙」:よくあるミスとその理由
参照型と値型の違いは最初はややこしい。よくあるミスや勘違いをまとめてみたよ:
配列のコピーで混乱: 初心者はarr2 = arr1;で独立した配列ができると思いがち。でも実際は同じ配列を2つの変数が指してるだけ。2つのコントローラーで同じゲームを操作する感じ。独立した配列が欲しいなら、明示的にクローン(例:int[] arr2 = (int[])arr1.Clone();やCopyメソッド)を使おう。
文字列が「参照渡し」で変わると思い込む: stringは参照型だけど、イミュータブルだから、何か「変更」する操作は実は新しい文字列を作ってるだけ。ループで何度も文字列操作するならStringBuilderを使った方が効率的だよ。
nullを忘れる: 値型(nullable型int?やbool?以外)は必ず値を持ってて、nullにはなれない。参照型はnullになれる=何も指してない状態。nullなオブジェクトのメンバーにアクセスしようとするとNullReferenceExceptionが出る。参照型変数は使う前にnullチェックを忘れずに!
小さいデータにも全部クラスを使っちゃう: 何でもクラスで宣言しがちだけど、単純なデータ(例:点Point { X, Y }や色Color { R, G, B })は構造体の方が効率的な場合もある。構造体はスタックに保存されて値コピーされるから、ガベージコレクションの負担が減る。ただし、構造体はイミュータブルで小さく、参照型フィールドを持たない方がいいよ。
GO TO FULL VERSION