C#のオブジェクトとは?クラス・インスタンスとの違いと使い方を初心者向けに解説

はじめに

C#を学び始めると、「オブジェクト」「クラス」「インスタンス」といった言葉が頻繁に登場します。これらはオブジェクト指向プログラミングを理解するうえで欠かせない概念ですが、意味が似ているため、初心者には違いがわかりにくい部分です。

簡単に表すと、クラスは設計図、インスタンスは設計図から作られた実体、オブジェクトはプログラム内で扱うデータと処理をひとまとめにした存在です。

この記事では、C#のオブジェクトとは何かを、クラスやインスタンスとの違い、作成方法、object型、参照型、比較、継承、メモリ管理まで含めて解説します。サンプルコードを読みながら、C#でオブジェクトを扱う基本を身につけていきましょう。

1. C#のオブジェクトとは

1-1. オブジェクトを初心者向けにわかりやすく説明

C#におけるオブジェクトとは、データと、そのデータを利用する処理をひとまとめにしたものです。

たとえば、プログラム上で「人」を表現するとします。人には、名前や年齢などの情報があります。また、自己紹介をする、歩く、食事をするといった行動もあります。

C#では、次のように情報と処理をまとめて表現できます。

public class Person{public string Name { get; set; } = "";public int Age { get; set; }
public void Introduce(){Console.WriteLine($"私の名前は{Name}です。{Age}歳です。");}

}

このPersonクラスから作られた具体的な人がオブジェクトです。

Person person = new Person();

person.Name = "田中";person.Age = 25;person.Introduce();

実行結果は次のようになります。

私の名前は田中です。25歳です。

この例では、personPersonクラスから生成されたオブジェクトを参照しています。

1-2. C#におけるオブジェクトの役割

オブジェクトの主な役割は、関連するデータと処理をまとめ、プログラムを整理しやすくすることです。

オブジェクトを使わず、名前、年齢、住所などを別々の変数として管理すると、データ同士の関係がわかりにくくなります。

string personName = "田中";int personAge = 25;string personAddress = "東京都";

人数が増えると、さらに多くの変数が必要です。

string person1Name = "田中";int person1Age = 25;

string person2Name = "佐藤";int person2Age = 30;

クラスとオブジェクトを使えば、1人分の情報をひとまとまりとして扱えます。

Person person1 = new Person { Name = "田中", Age = 25 };Person person2 = new Person { Name = "佐藤", Age = 30 };

オブジェクトを利用することで、次のようなメリットを得られます。

  • 関連するデータをまとめられる

  • データに関連する処理も一緒に管理できる

  • 同じ構造のデータを何個でも作成できる

  • プログラムの変更箇所を整理しやすい

  • コードを再利用しやすい

1-3. オブジェクトが持つ「状態」と「振る舞い」

オブジェクトは、一般的に「状態」と「振る舞い」を持ちます。

状態とは、オブジェクトが現在持っているデータです。C#では、主にフィールドやプロパティで表現します。

振る舞いとは、オブジェクトが実行できる処理です。C#ではメソッドで表現します。

public class Car{// 状態public string Color { get; set; } = "";public int Speed { get; private set; }

// 振る舞いpublic void Accelerate(){Speed += 10;}public void Stop(){Speed = 0;}

}

ColorSpeedが状態、AccelerateStopが振る舞いです。

Car car = new Car{Color = "赤"};

car.Accelerate();Console.WriteLine(car.Speed); // 10

car.Stop();Console.WriteLine(car.Speed); // 0

オブジェクト自身にデータの変更方法を持たせることで、不正な値が設定されるのを防ぎやすくなります。

1-4. C#ではすべての型がobject型を継承する

C#では、object型が型システムの基礎となる型です。クラスは、明示的に別の基底クラスを指定していない場合、最終的にSystem.Objectを基底とします。

objectは、System.ObjectのC#における別名です。

object value1 = "こんにちは";object value2 = 100;object value3 = new Person();

文字列、整数、独自クラスのオブジェクトなどを、object型の変数に代入できます。

値型もobject型として扱えますが、整数などの値型をobject型へ代入するときには、後述するボックス化が発生します。

すべてのオブジェクトは、object型が提供する次のようなメソッドを利用できます。

ToString()Equals()GetHashCode()GetType()

例として、整数の型名と文字列表現を取得してみましょう。

int number = 100;

Console.WriteLine(number.ToString());Console.WriteLine(number.GetType().Name);

実行結果は次のとおりです。

100Int32

2. オブジェクト・クラス・インスタンスの違い

2-1. クラスはオブジェクトを作るための設計図

クラスは、オブジェクトがどのようなデータと処理を持つかを定義する設計図です。

たとえば、家を建てる場合、設計図には部屋数や窓の位置などが記載されています。しかし、設計図そのものは実際の家ではありません。

C#のクラスも同様です。

public class Product{public string Name { get; set; } = "";public decimal Price { get; set; }

public void ShowInfo(){Console.WriteLine($"{Name}: {Price}円");}

}

このコードは、商品が名前と価格を持ち、情報を表示できることを定義しています。ただし、クラスを定義しただけでは、具体的な商品はまだ作られていません。

2-2. インスタンスはクラスから生成された実体

インスタンスとは、クラスをもとに生成された具体的な実体です。

Product product = new Product();

このコードでは、new Product()によってProductクラスのインスタンスを生成しています。

クラスからは複数のインスタンスを作成できます。

Product product1 = new Product{Name = "ノート",Price = 200};

Product product2 = new Product{Name = "ペン",Price = 100};

product1product2は、同じProductクラスから生成されていますが、それぞれ異なる状態を持つ別のインスタンスです。

2-3. オブジェクトとインスタンスは何が違うのか

実際のC#開発では、「オブジェクト」と「インスタンス」はほぼ同じ意味で使われることがあります。ただし、厳密には注目している点が異なります。

インスタンスは、特定のクラスから生成された実体であることを強調する言葉です。

一方、オブジェクトは、プログラム上でデータと処理を持つ操作対象であることを強調する言葉です。

たとえば、次のように表現できます。

Person person = new Person();

このnew Person()について、次の説明はどちらも成り立ちます。

  • Personクラスのインスタンスを生成した

  • Personオブジェクトを生成した

初心者のうちは、クラスが設計図、インスタンスまたはオブジェクトが設計図から作られた実体、と理解しておけば問題ありません。

2-4. クラス・オブジェクト・インスタンスの関係を具体例で理解する

クラス、オブジェクト、インスタンスの関係を、たい焼きに例えてみましょう。

  • クラス:たい焼きの型

  • インスタンス:型を使って作られた1個のたい焼き

  • オブジェクト:プログラム上で扱うたい焼きの実体

C#で表現すると次のようになります。

public class Taiyaki{public string Filling { get; set; } = "";

public void ShowFilling(){Console.WriteLine($"中身は{Filling}です。");}

}

このTaiyakiクラスを使って、異なる中身のたい焼きを生成できます。

Taiyaki ankoTaiyaki = new Taiyaki{Filling = "あんこ"};

Taiyaki creamTaiyaki = new Taiyaki{Filling = "クリーム"};

ankoTaiyaki.ShowFilling();creamTaiyaki.ShowFilling();

実行結果は次のとおりです。

中身はあんこです。中身はクリームです。

同じクラスから生成しても、各オブジェクトはそれぞれ独立した状態を持ちます。

3. C#でオブジェクトを作成する方法

3-1. クラスを定義する基本構文

C#で独自のオブジェクトを作成するには、最初にクラスを定義します。

基本構文は次のとおりです。

アクセス修飾子 class クラス名{// フィールド// プロパティ// メソッド}

実際のクラス定義を見てみましょう。

public class Book{public string Title { get; set; } = "";public string Author { get; set; } = "";public int Price { get; set; }

public void ShowInfo(){Console.WriteLine($"{Title} / {Author} / {Price}円");}

}

クラス名には、一般的に役割がわかる名詞を使用します。C#では、クラス名の先頭を大文字にするPascalCaseが標準的です。

3-2. new演算子でインスタンスを生成する

定義したクラスからインスタンスを生成するには、通常はnew演算子を使います。

Book book = new Book();

左側のBookは変数の型、bookは変数名です。右側のnew Book()が新しいBookオブジェクトを生成する処理です。

C#では、型が明らかな場合に右辺の型名を省略する書き方もできます。

Book book = new();

どちらも基本的に同じ意味です。初心者は、最初にnew Book()と明記する書き方を覚えると理解しやすいでしょう。

3-3. フィールドとプロパティに値を設定する

オブジェクトにデータを持たせる方法には、フィールドとプロパティがあります。

フィールドは、クラス内で値を直接保持する変数です。

public class Sample{public int Number;}

プロパティは、値を取得または設定するための仕組みです。

public class Sample{public int Number { get; set; }}

プロパティには、値の設定を制限する処理も記述できます。

public class Person{private int _age;

public int Age{get{return _age;}set{if (value < 0){throw new ArgumentOutOfRangeException(nameof(value),"年齢は0以上で指定してください。");}_age = value;}}

}

オブジェクトを生成した後は、ドット演算子を使ってプロパティへ値を設定します。

Person person = new Person();person.Age = 25;

Console.WriteLine(person.Age);

3-4. メソッドを呼び出してオブジェクトを操作する

メソッドは、オブジェクトの処理や振る舞いを表します。

public class Counter{public int Value { get; private set; }

public void Increment(){Value++;}

}

生成したオブジェクトのメソッドは、ドット演算子で呼び出します。

Counter counter = new Counter();

counter.Increment();counter.Increment();

Console.WriteLine(counter.Value); // 2

Valuesetprivateにしているため、クラスの外部からは自由に変更できません。

// コンパイルエラー// counter.Value = 100;

値を変更する方法をIncrementメソッドに限定することで、オブジェクトの状態を安全に管理できます。

3-5. オブジェクト初期化子を使って簡潔に記述する

プロパティへ値を設定する場合、オブジェクト初期化子を使うと簡潔に記述できます。

通常の書き方は次のとおりです。

Book book = new Book();

book.Title = "C#入門";book.Author = "山田太郎";book.Price = 2_500;

オブジェクト初期化子を使うと、次のようにまとめられます。

Book book = new Book{Title = "C#入門",Author = "山田太郎",Price = 2_500};

生成時に必要な値がひと目でわかるため、読みやすいコードになります。

4. C#のオブジェクトを使ったサンプルコード

4-1. Personクラスを作成する

ここでは、名前、年齢、自己紹介機能を持つPersonクラスを作成します。

public class Person{public string Name { get; set; } = "";public int Age { get; set; }

public void Introduce(){Console.WriteLine($"はじめまして。{Name}です。{Age}歳です。");}public void HaveBirthday(){Age++;Console.WriteLine($"{Name}は誕生日を迎え、{Age}歳になりました。");}

}

NameAgeが状態、IntroduceHaveBirthdayが振る舞いです。

4-2. Personオブジェクトを生成する

Personクラスからオブジェクトを生成します。

Person person = new Person();

生成時に値を設定する場合は、オブジェクト初期化子を使えます。

Person person = new Person{Name = "鈴木",Age = 20};

これで、名前が「鈴木」、年齢が20歳のPersonオブジェクトが作成されました。

4-3. プロパティの値を取得・変更する

プロパティの値は、ドット演算子を使って取得できます。

Console.WriteLine(person.Name);Console.WriteLine(person.Age);

値を変更する場合も、同じようにプロパティへ代入します。

person.Name = "鈴木一郎";person.Age = 21;

変更後の値を確認します。

Console.WriteLine(person.Name); // 鈴木一郎Console.WriteLine(person.Age);  // 21

4-4. メソッドを実行する

PersonオブジェクトのIntroduceメソッドを実行します。

person.Introduce();

実行結果は次のようになります。

はじめまして。鈴木一郎です。21歳です。

HaveBirthdayメソッドを呼び出すと、年齢が1増えます。

person.HaveBirthday();

実行結果は次のとおりです。

鈴木一郎は誕生日を迎え、22歳になりました。

メソッド内でプロパティを変更できることからも、オブジェクトが状態と振る舞いをひとまとめにした存在であることがわかります。

4-5. 複数のオブジェクトを生成して使い分ける

1つのクラスから、複数のオブジェクトを生成できます。

Person person1 = new Person{Name = "田中",Age = 25};

Person person2 = new Person{Name = "佐藤",Age = 30};

person1.Introduce();person2.Introduce();

実行結果は次のとおりです。

はじめまして。田中です。25歳です。はじめまして。佐藤です。30歳です。

person1の年齢を変更しても、person2には影響しません。

person1.Age = 26;

Console.WriteLine(person1.Age); // 26Console.WriteLine(person2.Age); // 30

それぞれが別のオブジェクトだからです。

複数のオブジェクトは、配列やリストにまとめることもできます。

List<Person> people = new List<Person>{person1,person2,new Person { Name = "高橋", Age = 28 }};

foreach (Person currentPerson in people){currentPerson.Introduce();}

5. object型の特徴と使い方

5-1. object型とは

object型は、C#の型システムにおける基本となる型です。正式な型名はSystem.Objectであり、次の2つは同じ意味です。

object value1 = "C#";System.Object value2 = "C#";

独自クラスのオブジェクトもobject型として扱えます。

Person person = new Person{Name = "田中",Age = 25};

object value = person;

ただし、変数をobject型として扱うと、元の型に固有のメンバーを直接呼び出せません。

// コンパイルエラー// value.Introduce();

valuePerson型へ戻せば、Introduceを呼び出せます。

5-2. object型の変数にさまざまな値を代入する

object型の変数には、さまざまな型の値を代入できます。

object value;

value = 100;Console.WriteLine(value);

value = "こんにちは";Console.WriteLine(value);

value = 3.14;Console.WriteLine(value);

value = new Person{Name = "山田",Age = 35};

Console.WriteLine(value);

1つの変数に異なる型の値を代入できる点は柔軟ですが、値を利用するときには実際の型を確認する必要があります。

一般的なアプリケーションでは、特別な理由がない限り、具体的な型やジェネリック型を使用したほうが安全です。

5-3. object型から元の型へキャストする

object型の値を元の型として利用するには、キャストが必要になる場合があります。

object value = "C#入門";

string text = (string)value;

Console.WriteLine(text.Length);

ただし、実際の型と異なる型へキャストすると、InvalidCastExceptionが発生します。

object value = "C#入門";

// 実行時にInvalidCastExceptionが発生するint number = (int)value;

明示的なキャストを行う場合は、その値が本当に目的の型であることを確認しましょう。

5-4. is演算子とas演算子で型を確認・変換する

is演算子を使うと、オブジェクトが特定の型であるかを確認できます。

object value = "C#入門";

if (value is string){Console.WriteLine("string型です。");}

パターンマッチングを利用すると、型の確認と変換を同時に行えます。

object value = "C#入門";

if (value is string text){Console.WriteLine(text.Length);}

この書き方では、valuestring型だった場合に、変換済みの値をtext変数として利用できます。

as演算子を使う方法もあります。

object value = "C#入門";

string? text = value as string;

if (text is not null){Console.WriteLine(text.Length);}

asによる変換に失敗すると、例外ではなくnullが返されます。asは主に参照型やnull許容値型への変換で使用します。

現在のC#では、isを利用したパターンマッチングが読みやすく、安全な場面が多くあります。

5-5. ボックス化とボックス化解除の仕組み

整数や真偽値などの値型をobject型へ変換すると、ボックス化が発生します。

int number = 100;object boxedValue = number;

このとき、numberの値がobjectとして扱える形に包まれます。

object型から元の値型へ戻す処理を、ボックス化解除と呼びます。

int restoredNumber = (int)boxedValue;

Console.WriteLine(restoredNumber);

ボックス化解除では、実際にボックス化された型と同じ型を指定する必要があります。

int number = 100;object boxedValue = number;

// intとして格納されているため、longへ直接解除できない// long result = (long)boxedValue;

longに変換したい場合は、一度intとしてボックス化解除してから変換します。

long result = (long)(int)boxedValue;

ボックス化には値のコピーや割り当てが伴うため、大量に繰り返す処理では性能に影響する可能性があります。

5-6. object型を使う際の注意点

object型にはどのような値でも格納できますが、使いすぎると次の問題が起こります。

  • コンパイル時に型の誤りを見つけにくくなる

  • キャスト処理が増える

  • キャスト失敗による例外が発生しやすくなる

  • 値型ではボックス化が発生する場合がある

  • コードから値の種類を判断しにくくなる

たとえば、複数の整数を管理するなら、List<object>よりList<int>が適切です。

List<int> numbers = new List<int>{10,20,30};

List<object>では文字列なども追加できるため、型の一貫性を保ちにくくなります。

List<object> values = new List<object>{10,"20",30.0};

扱う型が決まっている場合は、具体的な型やジェネリック型を使用しましょう。

6. オブジェクトと値型・参照型の違い

6-1. 値型と参照型の基本

C#の型は、大きく値型と参照型に分けられます。

代表的な値型には、次のものがあります。

intdoubleboolchardecimalstructenum

代表的な参照型には、次のものがあります。

classstringarraydelegateinterface

値型の変数は、基本的に値そのものを保持します。

int number = 100;

参照型の変数は、オブジェクトそのものではなく、そのオブジェクトを参照するための情報を保持します。

Person person = new Person();

なお、「値型は必ずスタック、参照型は必ずヒープ」と単純に決まるわけではありません。実際の配置は、変数の宣言場所や実行環境の最適化などによって異なります。初心者の段階では、値型は値をコピーし、参照型は参照をコピーすることが基本的な違いだと理解するとよいでしょう。

6-2. オブジェクト変数には参照が格納される

クラスは参照型です。クラス型の変数には、生成されたオブジェクトそのものではなく、オブジェクトへの参照が格納されます。

Person person = new Person{Name = "田中",Age = 25};

personを通じて、生成されたPersonオブジェクトのプロパティやメソッドへアクセスできます。

Console.WriteLine(person.Name);person.Introduce();

変数はオブジェクトを操作するための窓口だと考えると理解しやすいでしょう。

6-3. 代入したときの動作の違い

値型を別の変数へ代入すると、値がコピーされます。

int number1 = 10;int number2 = number1;

number2 = 20;

Console.WriteLine(number1); // 10Console.WriteLine(number2); // 20

number2を変更しても、number1には影響しません。

一方、参照型を別の変数へ代入すると、オブジェクトへの参照がコピーされます。

Person person1 = new Person{Name = "田中",Age = 25};

Person person2 = person1;

person2.Age = 30;

Console.WriteLine(person1.Age); // 30Console.WriteLine(person2.Age); // 30

person1person2が同じオブジェクトを参照しているため、どちらから見ても年齢は30になります。

6-4. 同じオブジェクトを複数の変数から参照する例

次のコードでは、1つのPersonオブジェクトを2つの変数から参照しています。

Person original = new Person{Name = "佐藤",Age = 20};

Person another = original;

another.Name = "佐藤花子";

Console.WriteLine(original.Name);

実行結果は次のとおりです。

佐藤花子

anotherへ代入したのはオブジェクトの複製ではなく、参照のコピーです。

別のオブジェクトを作成したい場合は、新しくインスタンスを生成して値をコピーします。

Person original = new Person{Name = "佐藤",Age = 20};

Person copied = new Person{Name = original.Name,Age = original.Age};

copied.Name = "佐藤花子";

Console.WriteLine(original.Name); // 佐藤Console.WriteLine(copied.Name); // 佐藤花子

6-5. nullが表す意味とNullReferenceExceptionへの対策

参照型の変数には、nullが格納されることがあります。nullは、変数がどのオブジェクトも参照していない状態を表します。

Person? person = null;

nullの変数からプロパティやメソッドへアクセスすると、NullReferenceExceptionが発生します。

Person? person = null;

// 実行時エラー// Console.WriteLine(person.Name);

対策として、使用前にnullでないことを確認します。

if (person is not null){Console.WriteLine(person.Name);}

null条件演算子?.も利用できます。

person?.Introduce();

personnullなら、Introduceは呼び出されません。

null合体演算子??を使うと、値がnullの場合の代替値を指定できます。

string name = person?.Name ?? "名前未設定";

Console.WriteLine(name);

メソッドの引数がnullであってはならない場合は、早い段階で例外を発生させる方法もあります。

public static void PrintPerson(Person person){ArgumentNullException.ThrowIfNull(person);

Console.WriteLine(person.Name);

}

7. オブジェクトの比較方法

7-1. ==演算子による比較

==演算子の動作は、比較する型によって異なります。

独自クラスで==演算子を定義していない場合、通常は2つの変数が同じオブジェクトを参照しているかを比較します。

Person person1 = new Person{Name = "田中",Age = 25};

Person person2 = person1;

Console.WriteLine(person1 == person2); // True

別々に生成したオブジェクトは、プロパティの値が同じでも、通常は同じ参照ではありません。

Person person1 = new Person{Name = "田中",Age = 25};

Person person2 = new Person{Name = "田中",Age = 25};

Console.WriteLine(person1 == person2); // False

ただし、stringなど一部の型では、==演算子が値を比較するように定義されています。

string text1 = new string(new[] { 'C', '#' });string text2 = "C#";

Console.WriteLine(text1 == text2); // True

そのため、==が参照比較になるか値比較になるかは、型の実装を確認する必要があります。

7-2. Equalsメソッドによる比較

Equalsメソッドは、2つのオブジェクトが等しいかを判定するために使用します。

Console.WriteLine(person1.Equals(person2));

独自クラスでEqualsをオーバーライドしていない場合、基本的には参照が同じかどうかが基準になります。

値の内容で比較したい場合は、クラス側でEqualsを適切に実装します。

public class Product{public int Id { get; set; }public string Name { get; set; } = "";

public override bool Equals(object? obj){if (obj is not Product other){return false;}return Id == other.Id &amp;&amp;Name == other.Name;}public override int GetHashCode(){return HashCode.Combine(Id, Name);}

}

このクラスでは、IdNameが同じなら同じ値を持つ商品と判断します。

7-3. ReferenceEqualsメソッドによる参照比較

ReferenceEqualsメソッドを使うと、2つの変数がまったく同じオブジェクトを参照しているかを確認できます。

Person person1 = new Person{Name = "田中",Age = 25};

Person person2 = person1;

Console.WriteLine(object.ReferenceEquals(person1, person2)); // True

別々に生成した場合は、値が同じでもfalseです。

Person person1 = new Person{Name = "田中",Age = 25};

Person person2 = new Person{Name = "田中",Age = 25};

Console.WriteLine(object.ReferenceEquals(person1, person2)); // False

ReferenceEqualsでは、オーバーライドされたEquals==演算子の影響を受けず、参照そのものを比較できます。

7-4. 値が同じオブジェクトを比較する際の注意点

見た目の値が同じでも、別々に生成したクラスのインスタンスは、既定では同じオブジェクトとは判断されません。

Person person1 = new Person{Name = "山田",Age = 30};

Person person2 = new Person{Name = "山田",Age = 30};

Console.WriteLine(person1 == person2); // FalseConsole.WriteLine(person1.Equals(person2)); // False

値による比較が必要な場合は、次の方法が考えられます。

  • EqualsGetHashCodeを適切にオーバーライドする

  • IEquatable<T>を実装する

  • 比較用のメソッドを作る

  • 値を中心に表現するrecordを使う

たとえば、recordでは、主要な値が同じオブジェクト同士を値ベースで比較できます。

public record PersonRecord(string Name, int Age);

PersonRecord person1 = new PersonRecord("山田", 30);PersonRecord person2 = new PersonRecord("山田", 30);

Console.WriteLine(person1 == person2); // True

値そのものを表現するデータでは、通常のクラスよりrecordが適している場合があります。

7-5. EqualsとGetHashCodeをオーバーライドする方法

独自クラスを値で比較する場合、EqualsだけでなくGetHashCodeも一緒にオーバーライドすることが重要です。

public class Employee : IEquatable<Employee>{public int Id { get; }public string Name { get; }

public Employee(int id, string name){Id = id;Name = name;}public bool Equals(Employee? other){if (other is null){return false;}if (ReferenceEquals(this, other)){return true;}return Id == other.Id &amp;&amp;Name == other.Name;}public override bool Equals(object? obj){return Equals(obj as Employee);}public override int GetHashCode(){return HashCode.Combine(Id, Name);}

}

比較に使用する値が同じオブジェクトでは、同じハッシュコードが返されるようにします。

これは、Dictionary<TKey, TValue>HashSet<T>など、ハッシュコードを利用するコレクションで正しく動作させるために必要です。

8. オブジェクトの継承とポリモーフィズム

8-1. 継承によって既存クラスを再利用する

継承とは、既存クラスの機能を引き継いで新しいクラスを作る仕組みです。

継承元を基底クラス、継承して作るクラスを派生クラスと呼びます。

public class Animal{public string Name { get; set; } = "";

public void Eat(){Console.WriteLine($"{Name}が食事をします。");}

}

public class Dog : Animal{public void Bark(){Console.WriteLine($"{Name}がワンと鳴きます。");}}

DogクラスはAnimalクラスを継承しているため、NameプロパティとEatメソッドを利用できます。

Dog dog = new Dog{Name = "ポチ"};

dog.Eat();dog.Bark();

8-2. 基底クラス型の変数で派生クラスのオブジェクトを扱う

派生クラスのオブジェクトは、基底クラス型の変数に代入できます。

Animal animal = new Dog{Name = "ポチ"};

animalが実際に参照しているのはDogオブジェクトですが、変数の型はAnimalです。

そのため、Animalクラスに定義されているメンバーは呼び出せます。

animal.Eat();

一方、Dogクラスだけに定義されたBarkは、Animal型の変数から直接呼び出せません。

// コンパイルエラー// animal.Bark();

型を確認して変換すれば呼び出せます。

if (animal is Dog dog){dog.Bark();}

8-3. virtualとoverrideでメソッドを上書きする

基底クラスのメソッドにvirtualを付けると、派生クラス側で処理を上書きできます。

public class Animal{public string Name { get; set; } = "";

public virtual void Speak(){Console.WriteLine($"{Name}が鳴きます。");}

}

public class Dog : Animal{public override void Speak(){Console.WriteLine($"{Name}がワンと鳴きます。");}}

public class Cat : Animal{public override void Speak(){Console.WriteLine($"{Name}がニャーと鳴きます。");}}

overrideは、基底クラスの仮想メソッドを派生クラス独自の処理へ置き換えるために使います。

8-4. ポリモーフィズムを利用したサンプルコード

ポリモーフィズムとは、同じ型や同じメソッド呼び出しを通して、実際のオブジェクトに応じた異なる処理を実行できる仕組みです。

List<Animal> animals = new List<Animal>{new Dog { Name = "ポチ" },new Cat { Name = "タマ" }};

foreach (Animal animal in animals){animal.Speak();}

実行結果は次のとおりです。

ポチがワンと鳴きます。タマがニャーと鳴きます。

変数の型はどちらもAnimalですが、実際のオブジェクトがDogならDog.SpeakCatならCat.Speakが実行されます。

ポリモーフィズムを利用すると、派生クラスの種類ごとに大量の条件分岐を書く必要がなくなります。

8-5. 抽象クラスやインターフェースとの関係

抽象クラスは、共通する状態や処理を持たせながら、一部の処理を派生クラスに実装させたい場合に使用します。

public abstract class Shape{public abstract double GetArea();}

抽象クラスは直接インスタンス化できません。

// コンパイルエラー// Shape shape = new Shape();

派生クラスで抽象メソッドを実装します。

public class Circle : Shape{public double Radius { get; set; }

public override double GetArea(){return Math.PI * Radius * Radius;}

}

インターフェースは、クラスが備えるべき機能の契約を表します。

public interface IPrintable{void Print();}

クラスはインターフェースを実装できます。

public class Report : IPrintable{public void Print(){Console.WriteLine("レポートを印刷します。");}}

インターフェース型の変数を使えば、異なるクラスのオブジェクトを同じ方法で扱えます。

IPrintable printable = new Report();printable.Print();

9. オブジェクトのライフサイクルとメモリ管理

9-1. オブジェクトが生成されてから不要になるまで

オブジェクトには、生成されてから不要になるまでの流れがあります。

一般的なライフサイクルは次のとおりです。

  1. クラスをもとにオブジェクトが生成される

  2. 変数やコレクションから参照される

  3. プロパティやメソッドを通じて利用される

  4. どこからも参照されなくなる

  5. ガベージコレクションの対象になる

例を見てみましょう。

static void CreatePerson(){Person person = new Person{Name = "田中",Age = 25};

person.Introduce();

}

CreatePersonメソッドの処理が終了すると、通常はローカル変数personを通じてオブジェクトへアクセスできなくなります。他の場所からも参照されていなければ、そのオブジェクトは将来的にガベージコレクションの対象になります。

9-2. スタックとヒープの基本的な違い

メモリ管理を理解するときには、スタックとマネージドヒープという言葉が登場します。

スタックは、主にメソッド呼び出しに関する情報やローカル変数などを効率よく管理する領域です。メソッドの処理が終了すると、それに対応する領域は解放されます。

マネージドヒープは、主に参照型のオブジェクトが管理される領域です。不要になったオブジェクトは、ガベージコレクターによって回収されます。

ただし、値型が常にスタックに置かれるわけではありません。値型がクラスのフィールドであれば、その値はクラスのオブジェクトの一部として管理されます。また、値型をボックス化すると、objectとしてヒープ上に割り当てられます。

スタックとヒープの説明は、C#の動作を理解するための基本モデルとして捉えましょう。

9-3. ガベージコレクションの仕組み

C#の実行環境には、ガベージコレクターと呼ばれるメモリ管理機能があります。

ガベージコレクターは、アプリケーションから到達できなくなったオブジェクトを見つけ、使用していたメモリを再利用できるようにします。

そのため、一般的なC#コードでは、確保したメモリを開発者が手動で解放する必要はありません。

Person person = new Person();

// 参照を外すperson = null!;

ただし、変数へnullを代入した直後にオブジェクトが必ず回収されるわけではありません。実際にガベージコレクションが実行されるタイミングは、実行環境が判断します。

通常のアプリケーションコードでは、GC.Collect()を手動で呼び出す必要はほとんどありません。むやみに実行すると、かえって性能が低下する可能性があります。

9-4. IDisposableとusing文が必要なケース

ガベージコレクションは、主にマネージドメモリを管理します。一方、ファイル、データベース接続、ネットワーク接続などの外部リソースは、使い終わった時点で明示的に解放すべき場合があります。

このようなリソースを扱う型は、IDisposableインターフェースを実装していることがあります。

using文を使うと、処理の終了時にDisposeメソッドが自動的に呼び出されます。

using StreamWriter writer = new StreamWriter("sample.txt");

writer.WriteLine("C#のオブジェクトについて学習中です。");

ブロック形式でも記述できます。

using (StreamWriter writer = new StreamWriter("sample.txt")){writer.WriteLine("ファイルへ書き込みます。");}

処理中に例外が発生しても、基本的にはDisposeが実行されるため、安全にリソースを解放できます。

9-5. 不要なオブジェクトを保持し続ける際の注意点

オブジェクトが不要になっても、どこかから参照され続けているとガベージコレクションの対象になりません。

たとえば、静的なリストへオブジェクトを追加し続けると、メモリ使用量が増え続ける可能性があります。

public static class PersonStore{public static List<Person> People { get; } = new();}

不要になったオブジェクトは、適切なタイミングでコレクションから削除します。

PersonStore.People.Remove(person);

特に次のような参照には注意が必要です。

  • 静的フィールドや静的コレクション

  • 長期間保持されるキャッシュ

  • 解除していないイベントハンドラー

  • 終了していないタイマー

  • 不要なオブジェクトを含むコレクション

C#ではメモリを自動管理してくれますが、不要な参照まで自動的に削除してくれるわけではありません。

10. C#のオブジェクトで初心者が間違えやすいポイント

10-1. クラスを定義しただけではオブジェクトは生成されない

クラスは設計図であり、クラスを定義しただけではオブジェクトは生成されません。

public class User{public string Name { get; set; } = "";}

この時点では、具体的なユーザーは存在していません。

通常はnewを使ってインスタンスを生成します。

User user = new User{Name = "田中"};

クラスの定義とオブジェクトの生成を区別しましょう。

10-2. nullのオブジェクトにアクセスしてしまう

参照型の変数がnullの状態でプロパティやメソッドへアクセスすると、NullReferenceExceptionが発生します。

User? user = null;

// NullReferenceException// Console.WriteLine(user.Name);

使用前にnullチェックを行います。

if (user is not null){Console.WriteLine(user.Name);}

値を必須にできる設計であれば、コンストラクターで受け取る方法も有効です。

public class User{public string Name { get; }

public User(string name){Name = name ??throw new ArgumentNullException(nameof(name));}

}

10-3. 参照のコピーとオブジェクトのコピーを混同する

次の代入では、オブジェクト自体はコピーされません。

Person person2 = person1;

コピーされるのは、オブジェクトへの参照です。そのため、person2を通じた変更は、person1から見ても反映されます。

別のオブジェクトが必要なら、新しく生成します。

Person person2 = new Person{Name = person1.Name,Age = person1.Age};

オブジェクトに別の参照型プロパティが含まれている場合、内部のオブジェクトまで複製するかどうかも考える必要があります。表面の値だけをコピーする方法はシャローコピー、内部のオブジェクトまで複製する方法はディープコピーと呼ばれます。

10-4. object型を多用して型安全性を失う

object型は柔軟ですが、型の誤りをコンパイル時に発見しにくくなります。

object age = "25";

この値を整数として扱おうとすると、キャストに失敗します。

// InvalidCastException// int number = (int)age;

年齢を整数として扱うことが決まっているなら、最初からintを使用すべきです。

int age = 25;

複数の型へ対応する処理では、objectではなくジェネリック型を利用できないか検討しましょう。

public static void PrintValue<T>(T value){Console.WriteLine(value);}

10-5. publicフィールドを直接公開する

次のようにpublicフィールドを直接公開すると、外部から自由に値を変更できます。

public class BankAccount{public decimal Balance;}

その結果、残高に負の値を直接代入できてしまいます。

BankAccount account = new BankAccount();account.Balance = -1_000_000;

プロパティやメソッドを使って変更方法を制御するほうが安全です。

public class BankAccount{public decimal Balance { get; private set; }

public void Deposit(decimal amount){if (amount &lt;= 0){throw new ArgumentOutOfRangeException(nameof(amount),"入金額は0より大きい値を指定してください。");}Balance += amount;}

}

10-6. 1つのクラスに多くの役割を持たせすぎる

1つのクラスに、ユーザー情報の管理、データベース保存、メール送信、画面表示などをすべて持たせると、クラスが複雑になります。

public class User{public void SaveToDatabase(){}

public void SendEmail(){}public void ExportCsv(){}public void ShowWindow(){}

}

このようなクラスは変更の影響範囲が大きく、テストや再利用も難しくなります。

役割ごとにクラスを分けると管理しやすくなります。

public class User{public string Name { get; set; } = "";public string Email { get; set; } = "";}

public class UserRepository{public void Save(User user){// 保存処理}}

public class EmailService{public void SendWelcomeEmail(User user){// メール送信処理}}

クラスを設計するときは、そのクラスが何を担当するのかを明確にしましょう。

11. C#のオブジェクトに関するよくある質問

11-1. オブジェクトとインスタンスは同じ意味ですか

日常的なC#開発では、オブジェクトとインスタンスはほぼ同じ意味で使われることがあります。

ただし、インスタンスは「特定のクラスから生成された実体」という点を強調し、オブジェクトは「プログラム上で操作するデータと処理のまとまり」という点を強調します。

初心者のうちは、どちらもクラスから生成された実体を指すことが多いと理解して問題ありません。

11-2. newを使わずにオブジェクトを作成できますか

通常のクラスでは、コンストラクターを呼び出して新しいインスタンスを作成する際にnewを使用します。

Person person = new Person();

ただし、コード上で自分がnewを書かなくても、ライブラリやフレームワークが内部でオブジェクトを生成する場合があります。

string text = string.Empty;DateTime now = DateTime.Now;

また、次のような仕組みを通じてオブジェクトを取得することもあります。

  • ファクトリーメソッド

  • 依存性注入

  • デシリアライズ

  • リフレクション

  • LINQなどが返すオブジェクト

  • フレームワークによる自動生成

ファクトリーメソッドの例は次のとおりです。

public class User{public string Name { get; private set; } = "";

public static User Create(string name){return new User{Name = name};}

}

呼び出し側ではnewを書かずにオブジェクトを取得できます。

User user = User.Create("田中");

ただし、内部ではnew Userによって生成されています。

11-3. object型とvarの違いは何ですか

objectは実際の型です。一方、varはコンパイラーに変数の型を推論させるためのキーワードです。

object value1 = "C#";var value2 = "C#";

value1のコンパイル時の型はobjectです。そのため、string固有のLengthプロパティへ直接アクセスできません。

// コンパイルエラー// Console.WriteLine(value1.Length);

value2の型は、右辺からstringと推論されます。

Console.WriteLine(value2.Length);

varを使っても、変数が型を持たなくなるわけではありません。コンパイル時に具体的な型が決定されます。

また、varで宣言したローカル変数へ、後から異なる型の値を代入することはできません。

var value = "C#";

// コンパイルエラー// value = 100;

11-4. オブジェクトをコピーするにはどうすればよいですか

クラス型の変数を単純に代入しても、オブジェクトはコピーされません。

Person copied = original;

これは参照のコピーです。

別のオブジェクトとして複製したい場合は、新しいインスタンスを生成して値を設定します。

Person copied = new Person{Name = original.Name,Age = original.Age};

コピー処理をメソッドにまとめる方法もあります。

public class Person{public string Name { get; set; } = "";public int Age { get; set; }

public Person Copy(){return new Person{Name = Name,Age = Age};}

}

recordを使っている場合は、with式によるコピーも可能です。

public record UserRecord(string Name, int Age);

UserRecord user1 = new UserRecord("田中", 25);UserRecord user2 = user1 with { Age = 26 };

参照型のプロパティを含む複雑なオブジェクトでは、シャローコピーでよいのか、ディープコピーが必要なのかを検討してください。

11-5. オブジェクトがnullかどうかを確認する方法は何ですか

基本的には、is nullまたはis not nullを使用できます。

if (person is null){Console.WriteLine("personはnullです。");}

nullでないことを確認する場合は次のとおりです。

if (person is not null){person.Introduce();}

== nullでも確認できます。

if (person == null){Console.WriteLine("personはnullです。");}

ただし、独自クラスが==演算子をオーバーロードしている可能性を考慮すると、参照がnullかを明確に確認したい場面ではis nullがわかりやすい書き方です。

null条件演算子を使えば、安全にメンバーへアクセスできます。

person?.Introduce();

11-6. staticクラスはインスタンス化できますか

staticクラスはインスタンス化できません。

public static class Calculator{public static int Add(int left, int right){return left + right;}}

次のようにnewを使うことはできません。

// コンパイルエラー// Calculator calculator = new Calculator();

静的クラスのメンバーは、クラス名から直接呼び出します。

int result = Calculator.Add(10, 20);

Console.WriteLine(result); // 30

静的クラスは、オブジェクトごとの状態を持つ必要がなく、アプリケーション全体で共通して使う処理をまとめる場合に適しています。

まとめ

C#のオブジェクトとは、状態を表すデータと、振る舞いを表す処理をひとまとめにしたものです。クラスという設計図をもとにインスタンスを生成し、プロパティやメソッドを通じて操作します。

クラス、オブジェクト、インスタンスの関係は、次のように整理できます。

  • クラスはオブジェクトを作るための設計図

  • インスタンスはクラスから生成された具体的な実体

  • オブジェクトはプログラム上で扱うデータと処理のまとまり

クラス型の変数にはオブジェクトそのものではなく参照が格納されるため、変数を代入しただけではオブジェクトは複製されません。また、nullへのアクセス、object型の多用、値比較と参照比較の混同にも注意が必要です。

C#では、継承、ポリモーフィズム、インターフェース、ガベージコレクションなど、多くの機能がオブジェクトを中心に構成されています。まずは小さなクラスを定義し、複数のインスタンスを生成して、プロパティの変更やメソッドの呼び出しを試すことが理解への近道です。