クラス

【C#】クラスをわかりやすく入門!基本構文からコンストラクター・継承まで

C#のクラスはオブジェクトの設計図で、関連するデータと動作を一つにまとめます。

classキーワードで定義し、フィールドで状態を、メソッドで振る舞いを持たせ、コンストラクターで初期化できます。

継承を使えば既存機能を再利用しつつ拡張でき、カプセル化によって保守性が高まります。

クラスを理解するとプログラム構造が整理され、再利用しやすいコードを書けます。

目次から探す
  1. クラスとは?オブジェクト指向の基礎
  2. クラスの基本構文
  3. コンストラクターの使い方
  4. プロパティとカプセル化
  5. フィールドと定数
  6. メソッドの設計ポイント
  7. staticメンバー活用
  8. 継承の基本
  9. 抽象クラスとインターフェース
  10. ポリモーフィズムの実践
  11. ジェネリッククラス
  12. 演算子オーバーロード
  13. レコード型との違い
  14. データクラスのToStringカスタマイズ
  15. IDisposableとusing
  16. イベントとデリゲート
  17. 部分クラスとネストクラス
  18. アクセス修飾子一覧
  19. テストしやすいクラス設計
  20. クラス設計のアンチパターン
  21. まとめ

クラスとは?オブジェクト指向の基礎

C#におけるクラスは、オブジェクト指向プログラミングの基本的な構造であり、データとそのデータを操作するメソッドを一つの単位としてまとめたものです。

クラスは「設計図」のような役割を果たし、実際に動作するオブジェクトを作成するための元となります。

ここでは、クラスとオブジェクトの違い、インスタンス化のイメージ、そしてメモリ上での配置についてわかりやすく解説します。

クラスとオブジェクトの違い

クラスは「設計図」、オブジェクトはその設計図から作られた「実体」と考えると理解しやすいです。

たとえば、家の設計図がクラスで、実際に建てられた家がオブジェクトです。

  • クラス

クラスは、データ(フィールドやプロパティ)と処理(メソッド)をまとめたものです。

クラス自体はメモリ上に存在せず、あくまでオブジェクトを作るためのテンプレートです。

  • オブジェクト(インスタンス)

クラスをもとに実際に作られた具体的なデータの塊です。

オブジェクトはメモリ上に存在し、クラスで定義されたフィールドやメソッドを持ちます。

たとえば、Carクラスがあった場合、Carは「車」という概念の設計図です。

そこからmyCarというオブジェクトを作ると、myCarは実際の車の一台を表します。

インスタンス化のイメージ

クラスからオブジェクトを作ることを「インスタンス化」と呼びます。

C#ではnewキーワードを使ってインスタンス化を行います。

public class Car
{
    public string Color;
    public int Year;
    public Car(string color, int year)
    {
        Color = color;
        Year = year;
    }
    public void Start()
    {
        Console.WriteLine("車が始動しました。");
    }
}
class Program
{
    static void Main()
    {
        // Carクラスのインスタンスを作成(インスタンス化)
        Car myCar = new Car("赤", 2020);
        myCar.Start();  // メソッドの呼び出し
    }
}
車が始動しました。

この例では、Carクラスの設計図をもとにmyCarというオブジェクトを作っています。

new Car("赤", 2020)がインスタンス化の部分で、myCarCarクラスのインスタンス(オブジェクト)です。

インスタンス化すると、myCarColorYearというデータを持ち、Startメソッドを使うことができます。

インスタンス化は、クラスの設計図から実際に使える「もの」を作る作業だとイメージしてください。

メモリ上の配置

クラスのインスタンスはメモリ上にどのように配置されているのかを理解すると、プログラムの動作がよりイメージしやすくなります。

C#はマネージド言語であり、メモリ管理は.NETランタイムが行いますが、基本的な配置の仕組みは以下のようになっています。

項目説明
ヒープ領域クラスのインスタンス(オブジェクト)が格納される場所。動的に確保され、ガベージコレクションで管理されます。
スタック領域メソッドの呼び出し時に使われる領域。ローカル変数やメソッドの引数がここに置かれる。
メソッド領域クラスのメソッドの実装コードや静的メンバーが格納される領域。

インスタンスの配置

  • クラスのインスタンスはヒープ領域に確保されます
  • インスタンスのフィールド(データ)はヒープ上に存在し、参照型の変数はヒープのアドレスを指します
  • 変数自体はスタックに置かれ、そこにヒープ上のインスタンスの参照が格納されます

具体例

class Person
{
    public string Name;
    public int Age;
}
class Program
{
    static void Main()
    {
        Person p = new Person();
        p.Name = "太郎";
        p.Age = 30;
        Console.WriteLine($"{p.Name}さんは{p.Age}歳です。");
    }
}
太郎さんは30歳です。

この場合、pという変数はスタックに置かれ、Personクラスのインスタンスはヒープに作られます。

pはヒープ上のインスタンスのアドレスを持っているため、p.Namep.Ageはヒープ上のデータにアクセスしています。

値型との違い

C#には値型intstructなど)と参照型(classがあります。

値型はスタックに直接データが置かれますが、参照型はスタックに参照(ポインタ)が置かれ、実際のデータはヒープにあります。

この違いを理解すると、メモリの効率的な使い方やパフォーマンスの最適化に役立ちます。

クラスはオブジェクト指向の中心的な概念であり、設計図としての役割を持ちます。

インスタンス化によって実際のオブジェクトが作られ、メモリ上ではヒープに配置されることが多いです。

これらの基本を押さえることで、C#のクラスを使ったプログラミングがスムーズに進められます。

クラスの基本構文

classキーワードと名前空間の関係

C#でクラスを定義する際は、classキーワードを使います。

クラスは名前空間namespaceの中に配置するのが一般的です。

名前空間はクラスやその他の型をグループ化し、同じ名前のクラスが衝突しないように管理する役割を持ちます。

namespace VehicleApp
{
    public class Car
    {
        public string Color;
        public int Year;
    }
}

この例では、CarクラスがVehicleAppという名前空間の中にあります。

名前空間を使うことで、同じ名前のCarクラスが他の名前空間にあっても区別できます。

名前空間を使わずにクラスを定義することも可能ですが、規模が大きくなると名前の衝突や管理が難しくなるため、名前空間は必ず使うことが推奨されます。

フィールドとプロパティの宣言

クラスの中でデータを保持するために使うのが「フィールド」と「プロパティ」です。

フィールド

フィールドはクラスの内部で使う変数のことです。

直接データを格納します。

public class Car
{
    public string Color;  // 車の色を表すフィールド
    private int year;     // 製造年を表すプライベートフィールド
}
  • publicは外部からアクセス可能
  • privateはクラス内部だけでアクセス可能

フィールドは直接アクセスできるため簡単ですが、外部からの不正な値の設定を防ぐために、通常はprivateにしてプロパティを使うことが多いです。

プロパティ

プロパティはフィールドの値を取得・設定するためのアクセサーgetとミューテーターsetを持つメンバーです。

カプセル化を実現し、値の検証や読み取り専用などの制御が可能です。

public class Car
{
    private int year;
    public int Year
    {
        get { return year; }
        set
        {
            if (value > 1885)  // 自動車の発明年以降のみ許可
            {
                year = value;
            }
        }
    }
}

このように、Yearプロパティを通じてyearフィールドにアクセスします。

setの中で値の検証を行うことで、不正な値の設定を防げます。

自動実装プロパティ

C#では、フィールドを明示的に書かずに自動的にバックフィールドを作成する「自動実装プロパティ」が使えます。

public class Car
{
    public string Color { get; set; }  // 自動実装プロパティ
}

この場合、Colorの値は自動的に内部で管理され、簡潔に書けます。

メソッドの定義

メソッドはクラスの中で動作や処理を定義する部分です。

メソッドは戻り値の型、メソッド名、引数リスト、そして処理内容の本体から構成されます。

public class Car
{
    public string Color { get; set; }
    public int Year { get; set; }
    // メソッドの定義
    public void Start()
    {
        Console.WriteLine("車が始動しました。");
    }
    // 引数と戻り値のあるメソッド
    public string GetDescription()
    {
        return $"この車は{Color}色で、製造年は{Year}年です。";
    }
}
  • voidは戻り値がないことを示します
  • 戻り値がある場合は型を指定し、return文で値を返します

メソッドはクラスの外部から呼び出して、オブジェクトの動作を実行できます。

アクセス修飾子の役割

アクセス修飾子はクラスやメンバー(フィールド、プロパティ、メソッドなど)のアクセス範囲を制御します。

主な修飾子は以下の通りです。

修飾子アクセス範囲説明
publicどこからでもアクセス可能最も制限が緩い。外部から自由にアクセスできます。
private定義されたクラス内のみ最も制限が厳しい。外部からはアクセス不可。
protected定義クラスと派生クラス内継承したクラスからアクセス可能です。
internal同一アセンブリ内(同じプロジェクト内)外部プロジェクトからはアクセス不可。
protected internal同一アセンブリ内または派生クラス内internalprotectedの両方の条件を満たす。
private protected同一アセンブリ内かつ派生クラス内privateよりは緩く、protected internalよりは厳しい。
public class Car
{
    public string Color;          // どこからでもアクセス可能
    private int year;             // クラス内のみアクセス可能
    protected int speed;          // 派生クラスからアクセス可能
    internal string modelName;    // 同一アセンブリ内でアクセス可能
    public void Start()
    {
        Console.WriteLine("車が始動しました。");
    }
}

アクセス修飾子を適切に使うことで、クラスの内部構造を隠蔽し、外部からの不正な操作を防ぐことができます。

これがオブジェクト指向の「カプセル化」の重要なポイントです。

これらの基本構文を理解すると、C#でクラスを定義し、データと動作をまとめて扱うことができるようになります。

フィールドやプロパティでデータを管理し、メソッドで処理を実装し、アクセス修飾子で安全に制御することが基本です。

コンストラクターの使い方

デフォルトコンストラクター

コンストラクターはクラスのインスタンスが生成されるときに自動的に呼び出される特別なメソッドで、オブジェクトの初期化を行います。

引数を持たないコンストラクターを「デフォルトコンストラクター」と呼びます。

public class Car
{
    public string Color;
    public int Year;
    // デフォルトコンストラクター
    public Car()
    {
        Color = "白";  // デフォルトの色を白に設定
        Year = 2020;   // デフォルトの製造年を2020年に設定
    }
    public void ShowInfo()
    {
        Console.WriteLine($"色: {Color}, 製造年: {Year}");
    }
}
class Program
{
    static void Main()
    {
        Car myCar = new Car();  // デフォルトコンストラクターが呼ばれる
        myCar.ShowInfo();
    }
}
色: 白, 製造年: 2020

この例では、Carクラスに引数なしのコンストラクターを定義しています。

new Car()でインスタンスを作成すると、このコンストラクターが呼ばれて、ColorYearに初期値が設定されます。

もしコンストラクターを自分で定義しなければ、C#コンパイラーが自動的に引数なしのデフォルトコンストラクターを生成します。

引数付きコンストラクター

引数付きコンストラクターは、インスタンス生成時に初期化したい値を外部から受け取るために使います。

これにより、柔軟にオブジェクトの状態を設定できます。

public class Car
{
    public string Color;
    public int Year;
    // 引数付きコンストラクター
    public Car(string color, int year)
    {
        Color = color;
        Year = year;
    }
    public void ShowInfo()
    {
        Console.WriteLine($"色: {Color}, 製造年: {Year}");
    }
}
class Program
{
    static void Main()
    {
        Car myCar = new Car("赤", 2021);  // 引数付きコンストラクターが呼ばれる
        myCar.ShowInfo();
    }
}
色: 赤, 製造年: 2021

この例では、Carクラスのコンストラクターがcoloryearの2つの引数を受け取り、それらをフィールドにセットしています。

インスタンス生成時に具体的な値を渡すことで、オブジェクトの初期状態を自由に決められます。

コンストラクターのオーバーロード

C#では、同じクラス内で複数のコンストラクターを定義できます。

これを「コンストラクターのオーバーロード」と呼び、引数の数や型が異なる複数のコンストラクターを用意して、様々な初期化パターンに対応できます。

public class Car
{
    public string Color;
    public int Year;
    // デフォルトコンストラクター
    public Car()
    {
        Color = "白";
        Year = 2020;
    }
    // 引数付きコンストラクター(色のみ)
    public Car(string color)
    {
        Color = color;
        Year = 2020;
    }
    // 引数付きコンストラクター(色と年)
    public Car(string color, int year)
    {
        Color = color;
        Year = year;
    }
    public void ShowInfo()
    {
        Console.WriteLine($"色: {Color}, 製造年: {Year}");
    }
}
class Program
{
    static void Main()
    {
        Car car1 = new Car();
        Car car2 = new Car("青");
        Car car3 = new Car("黒", 2019);
        car1.ShowInfo();
        car2.ShowInfo();
        car3.ShowInfo();
    }
}
色: 白, 製造年: 2020
色: 青, 製造年: 2020
色: 黒, 製造年: 2019

この例では、引数なし、引数1つ、引数2つの3種類のコンストラクターを用意しています。

呼び出し時の引数に応じて適切なコンストラクターが選ばれ、柔軟に初期化できます。

this()とbase()呼び出し

コンストラクター内で他のコンストラクターを呼び出すことができます。

これにより、重複した初期化コードをまとめて効率的に書けます。

  • this()は同じクラス内の別のコンストラクターを呼び出すときに使います
  • base()は基底クラス(親クラス)のコンストラクターを呼び出すときに使います

this()の例

public class Car
{
    public string Color;
    public int Year;
    public Car() : this("白", 2020)  // 引数付きコンストラクターを呼び出す
    {
    }
    public Car(string color, int year)
    {
        Color = color;
        Year = year;
    }
    public void ShowInfo()
    {
        Console.WriteLine($"色: {Color}, 製造年: {Year}");
    }
}
class Program
{
    static void Main()
    {
        Car myCar = new Car();  // デフォルトコンストラクターが呼ばれ、内部で引数付きコンストラクターを呼ぶ
        myCar.ShowInfo();
    }
}
色: 白, 製造年: 2020

この例では、引数なしのコンストラクターがthis("白", 2020)を使って、引数付きコンストラクターを呼び出しています。

これにより初期化コードの重複を防げます。

base()の例

public class Car
{
    public string Color;
    public int Year;
    public Car(string color, int year)
    {
        Color = color;
        Year = year;
    }
}
public class ElectricCar : Car
{
    public int BatteryLevel;
    public ElectricCar(string color, int year, int batteryLevel)
        : base(color, year)  // 基底クラスのコンストラクターを呼び出す
    {
        BatteryLevel = batteryLevel;
    }
    public void ShowInfo()
    {
        Console.WriteLine($"色: {Color}, 製造年: {Year}, バッテリー残量: {BatteryLevel}%");
    }
}
class Program
{
    static void Main()
    {
        ElectricCar myElectricCar = new ElectricCar("青", 2022, 85);
        myElectricCar.ShowInfo();
    }
}
色: 青, 製造年: 2022, バッテリー残量: 85%

ElectricCarクラスはCarクラスを継承しています。

ElectricCarのコンストラクターでbase(color, year)を使い、親クラスのコンストラクターを呼び出してColorYearを初期化しています。

これにより、親クラスの初期化処理を再利用できます。

コンストラクターはオブジェクトの初期化に欠かせない機能で、デフォルトや引数付き、複数のオーバーロードを使い分けることで柔軟な初期化が可能です。

this()base()を活用すると、コードの重複を減らし、継承関係でも効率的に初期化処理を行えます。

プロパティとカプセル化

プロパティの基本形

プロパティはクラスのフィールド(データ)に対して、外部からのアクセスを制御しつつ値の取得や設定を行うための仕組みです。

フィールドを直接公開するのではなく、プロパティを介してアクセスすることで、データの安全性や柔軟性を高められます。

プロパティはgetアクセサーとsetアクセサーを持ち、getは値の取得、setは値の設定を担当します。

基本的な構文は以下の通りです。

public class Person
{
    private string name;  // フィールド(データの実体)
    public string Name    // プロパティ
    {
        get
        {
            return name;  // 値を返す
        }
        set
        {
            name = value; // 値を設定する
        }
    }
}

この例では、nameというプライベートフィールドに対して、Nameプロパティを通じてアクセスしています。

外部からはNameを使って値を読み書きできますが、内部的にはnameフィールドが操作されます。

getアクセサーとsetアクセサー

getアクセサーはプロパティの値を取得するときに呼ばれ、setアクセサーは値を設定するときに呼ばれます。

setアクセサーの中では、特別なキーワードvalueが設定される値を表します。

public class Person
{
    private int age;
    public int Age
    {
        get
        {
            return age;
        }
        set
        {
            if (value >= 0)  // 年齢は0以上であることを保証
            {
                age = value;
            }
            else
            {
                Console.WriteLine("年齢は0以上でなければなりません。");
            }
        }
    }
}

この例では、Ageプロパティのsetアクセサーで値の検証を行い、不正な値が設定されるのを防いでいます。

getアクセサーは単純にフィールドの値を返しています。

自動実装プロパティ

C#では、フィールドを明示的に書かずにプロパティだけを定義できる「自動実装プロパティ」があります。

これにより、コードが簡潔になります。

public class Person
{
    public string Name { get; set; }  // 自動実装プロパティ
    public int Age { get; set; }
}

この場合、コンパイラーが自動的にバックフィールドを生成し、NameAgeの値を内部で管理します。

特に特別な処理が不要な場合は、自動実装プロパティを使うのが一般的です。

読み取り専用や書き込み専用のプロパティも自動実装で表現できます。

public class Person
{
    public string Name { get; }  // 読み取り専用プロパティ
    public Person(string name)
    {
        Name = name;  // コンストラクター内でのみ設定可能
    }
}

データ検証の実装例

プロパティのsetアクセサー内でデータ検証を行うことで、不正な値の設定を防ぎ、クラスの状態を常に正しく保てます。

以下は、年齢を表すAgeプロパティで負の値を拒否する例です。

public class Person
{
    private int age;
    public int Age
    {
        get { return age; }
        set
        {
            if (value < 0)
            {
                throw new ArgumentOutOfRangeException(nameof(value), "年齢は0以上でなければなりません。");
            }
            age = value;
        }
    }
}
class Program
{
    static void Main()
    {
        Person p = new Person();
        try
        {
            p.Age = 25;
            Console.WriteLine($"年齢: {p.Age}");
            p.Age = -5;  // 例外が発生する
        }
        catch (ArgumentOutOfRangeException ex)
        {
            Console.WriteLine($"エラー: {ex.Message}");
        }
    }
}
年齢: 25
エラー: 年齢は0以上でなければなりません。

この例では、Ageプロパティのsetアクセサーで負の値が設定された場合に例外を投げています。

これにより、誤ったデータがクラスに入るのを防ぎ、プログラムの信頼性を高めています。

プロパティを使うことで、フィールドの直接アクセスを避け、データの取得・設定に制御を加えられます。

特にgetsetアクセサーを活用したカプセル化は、オブジェクト指向プログラミングの重要な要素です。

自動実装プロパティを使えばコードがシンプルになり、必要に応じて検証ロジックを追加することも簡単です。

フィールドと定数

ローカル変数との違い

フィールドとローカル変数はどちらもデータを格納するための変数ですが、役割やスコープ(有効範囲)が大きく異なります。

項目フィールドローカル変数
定義場所クラスや構造体の内部メソッドやコンストラクターの内部
スコープクラス全体で有効定義されたメソッドやブロック内のみ
ライフタイムインスタンスのライフタイムに依存メソッドの実行中のみ有効
初期化明示的に初期化しないとデフォルト値が入る明示的に初期化しないと使用できない
アクセス修飾子付けられる(publicprivateなど)付けられない

フィールドの例

public class Car
{
    public string Color;  // フィールド(クラスのメンバー変数)
    public void ShowColor()
    {
        string message = "車の色は";  // ローカル変数(メソッド内の変数)
        Console.WriteLine(message + Color);
    }
}

この例では、Colorはクラスのフィールドで、ShowColorメソッドの外からもアクセス可能です。

一方、messageShowColorメソッド内のローカル変数で、メソッドの外からは見えません。

ライフタイムの違い

  • フィールドはオブジェクトのライフタイムにわたって存在し、オブジェクトが生きている限り値を保持します
  • ローカル変数はメソッドの実行中だけ存在し、メソッドが終了すると破棄されます

この違いにより、フィールドはオブジェクトの状態を保持するのに使い、ローカル変数は一時的な計算や処理に使います。

readonlyとconstキーワード

C#では、変更不可の値を表すためにreadonlyconstというキーワードがあります。

どちらも「定数」を表しますが、使い方や意味合いが異なります。

キーワード意味・特徴初期化タイミング変更可能か使用例
constコンパイル時に値が決まる定数。静的で、すべてのインスタンスで共有されます。宣言時に必ず初期化が必要変更不可const double Pi = 3.14159;
readonly実行時に一度だけ値を設定できる読み取り専用フィールド。インスタンスごとに異なる値を持てる。宣言時またはコンストラクター内で初期化可能初期化後は変更不可readonly int MaxSpeed;

constの例

public class MathConstants
{
    public const double Pi = 3.14159;  // コンパイル時定数
    public void ShowPi()
    {
        Console.WriteLine($"円周率: {Pi}");
    }
}

constはコンパイル時に値が決まるため、プログラム中で変更できません。

静的な性質を持つため、インスタンスを作らなくてもアクセス可能です。

readonlyの例

public class Car
{
    public readonly int MaxSpeed;
    public Car(int maxSpeed)
    {
        MaxSpeed = maxSpeed;  // コンストラクター内で初期化可能
    }
    public void ShowMaxSpeed()
    {
        Console.WriteLine($"最大速度: {MaxSpeed} km/h");
    }
}
class Program
{
    static void Main()
    {
        Car car = new Car(180);
        car.ShowMaxSpeed();
        // car.MaxSpeed = 200;  // コンパイルエラー: readonlyフィールドは変更不可
    }
}

readonlyは実行時に一度だけ値を設定でき、主にインスタンスごとに異なる定数的な値を持たせたい場合に使います。

コンストラクター内で初期化できるため、動的に値を決められます。

constは完全に不変の値を表し、readonlyは初期化後に変更できないフィールドを表します。

フィールドはクラスの状態を保持し、ローカル変数は一時的な処理に使うため、用途に応じて使い分けることが重要です。

メソッドの設計ポイント

戻り値と引数

メソッドは特定の処理をまとめたもので、呼び出し元に結果を返すこともできます。

戻り値はメソッドの処理結果を返すための型で、引数はメソッドに渡すデータです。

public class Calculator
{
    // 引数2つを受け取り、合計を返すメソッド
    public int Add(int a, int b)
    {
        return a + b;
    }
}

この例では、Addメソッドが2つの整数を引数として受け取り、合計をint型で返しています。

戻り値がない場合はvoidを使います。

public void PrintMessage(string message)
{
    Console.WriteLine(message);
}

引数は複数指定でき、型と名前をカンマで区切って並べます。

引数がないメソッドも定義可能です。

オーバーロードとオプション引数

オーバーロード

同じ名前のメソッドを引数の型や数を変えて複数定義することを「メソッドのオーバーロード」と言います。

これにより、異なるパターンの呼び出しに対応できます。

public class Calculator
{
    public int Add(int a, int b)
    {
        return a + b;
    }
    public double Add(double a, double b)
    {
        return a + b;
    }
    public int Add(int a, int b, int c)
    {
        return a + b + c;
    }
}
// 使い分け例
Calculator calc = new Calculator();
Console.WriteLine(calc.Add(1, 2));       // 3
Console.WriteLine(calc.Add(1.5, 2.5));   // 4.0
Console.WriteLine(calc.Add(1, 2, 3));    // 6

引数の型や数が異なれば、同じ名前のメソッドを複数持てるため、使いやすいAPI設計が可能です。

オプション引数

C#では引数にデフォルト値を設定でき、呼び出し時に省略可能な「オプション引数」を作れます。

public void Greet(string name, string greeting = "こんにちは")
{
    Console.WriteLine($"{greeting}, {name}さん!");
}
Greet("太郎");               // こんにちは, 太郎さん!
Greet("花子", "おはよう");   // おはよう, 花子さん!

オプション引数は後ろの方の引数に設定し、呼び出し時に省略するとデフォルト値が使われます。

これにより、メソッドの呼び出しが柔軟になります。

静的メソッドとインスタンスメソッド

インスタンスメソッド

インスタンスメソッドはクラスのオブジェクト(インスタンス)に属し、インスタンスの状態(フィールドやプロパティ)を操作できます。

呼び出すにはインスタンスを作成する必要があります。

public class Car
{
    public string Color;
    public void ShowColor()
    {
        Console.WriteLine($"車の色は{Color}です。");
    }
}
class Program
{
    static void Main()
    {
        Car myCar = new Car();
        myCar.Color = "赤";
        myCar.ShowColor();  // インスタンスメソッドの呼び出し
    }
}
車の色は赤です。

静的メソッド

静的メソッドはクラス自体に属し、インスタンスを作らずに呼び出せます。

インスタンスの状態にはアクセスできませんが、共通の処理やユーティリティ関数に適しています。

public class MathUtil
{
    public static int Square(int x)
    {
        return x * x;
    }
}
class Program
{
    static void Main()
    {
        int result = MathUtil.Square(5);  // 静的メソッドの呼び出し
        Console.WriteLine(result);
    }
}
25

静的メソッドはstaticキーワードで宣言し、クラス名を使って呼び出します。

インスタンスメソッドと静的メソッドは使い分けが重要で、状態を持つ処理はインスタンスメソッド、状態を持たない共通処理は静的メソッドにするのが一般的です。

staticメンバー活用

staticフィールドとstaticプロパティ

staticフィールドやstaticプロパティは、クラス自体に属するメンバーであり、インスタンスを生成しなくてもアクセスできます。

これにより、全インスタンスで共有されるデータや共通の設定値を管理できます。

public class Counter
{
    // staticフィールド:全インスタンスで共有されるカウント値
    private static int count = 0;
    // staticプロパティ:外部からアクセス可能なカウント値
    public static int Count
    {
        get { return count; }
    }
    public Counter()
    {
        count++;  // インスタンスが作られるたびにカウントアップ
    }
}
class Program
{
    static void Main()
    {
        Console.WriteLine($"初期カウント: {Counter.Count}");  // 0
        Counter c1 = new Counter();
        Counter c2 = new Counter();
        Console.WriteLine($"カウント: {Counter.Count}");  // 2
    }
}
初期カウント: 0
カウント: 2

この例では、countstaticフィールドなので、Counterクラスのすべてのインスタンスで共有されます。

Countプロパティもstaticで、インスタンスを作らずにCounter.Countでアクセス可能です。

インスタンスが生成されるたびにcountが増え、全体の生成数を管理しています。

staticコンストラクター

staticコンストラクターは、クラスのstaticメンバーの初期化を行う特別なコンストラクターです。

クラスが初めて使われるタイミングで一度だけ自動的に呼び出されます。

引数を持たず、アクセス修飾子も指定できません。

public class Logger
{
    public static readonly DateTime StartTime;
    // staticコンストラクター
    static Logger()
    {
        StartTime = DateTime.Now;
        Console.WriteLine("Loggerのstaticコンストラクターが呼ばれました。");
    }
    public static void Log(string message)
    {
        Console.WriteLine($"[{DateTime.Now}] {message}");
    }
}
class Program
{
    static void Main()
    {
        Console.WriteLine($"開始時刻: {Logger.StartTime}");
        Logger.Log("処理を開始します。");
    }
}
Loggerのstaticコンストラクターが呼ばれました。
開始時刻: 2024/06/XX XX:XX:XX
[2024/06/XX XX:XX:XX] 処理を開始します。

staticコンストラクターは、LoggerクラスのstaticメンバーStartTimeを初期化しています。

プログラム内でLoggerクラスのメンバーに初めてアクセスしたときに一度だけ呼ばれ、以降は呼ばれません。

シングルトンパターンへの応用

シングルトンパターンは、クラスのインスタンスを1つだけ生成し、それを共有するデザインパターンです。

staticメンバーとstaticコンストラクターを活用して実装できます。

public class Singleton
{
    // 唯一のインスタンスを保持するstaticフィールド
    private static readonly Singleton instance;
    // staticコンストラクターでインスタンスを初期化
    static Singleton()
    {
        instance = new Singleton();
    }
    // コンストラクターはprivateにして外部からの生成を禁止
    private Singleton()
    {
    }
    // インスタンスを取得するためのpublicなstaticプロパティ
    public static Singleton Instance
    {
        get { return instance; }
    }
    public void ShowMessage()
    {
        Console.WriteLine("シングルトンのインスタンスが呼ばれました。");
    }
}
class Program
{
    static void Main()
    {
        Singleton s1 = Singleton.Instance;
        Singleton s2 = Singleton.Instance;
        s1.ShowMessage();
        Console.WriteLine($"s1とs2は同じインスタンスですか? {ReferenceEquals(s1, s2)}");
    }
}
シングルトンのインスタンスが呼ばれました。
s1とs2は同じインスタンスですか? True

この例では、Singletonクラスのインスタンスはstaticフィールドinstanceで一度だけ生成されます。

privateコンストラクターにより外部からのインスタンス生成を防ぎ、Instanceプロパティを通じて唯一のインスタンスにアクセスします。

ReferenceEqualsで2つの変数が同じインスタンスを指していることを確認できます。

staticメンバーはクラス全体で共有されるため、共通の状態管理やユーティリティ的な機能に適しています。

staticコンストラクターは初期化処理を一度だけ行うのに便利で、シングルトンパターンのような設計にも活用されます。

これらを理解し使いこなすことで、効率的で安全なクラス設計が可能になります。

継承の基本

基底クラスと派生クラス

継承は、既存のクラス(基底クラス、スーパークラス)をもとに新しいクラス(派生クラス、サブクラス)を作成し、基底クラスのメンバー(フィールドやメソッド)を引き継ぐ仕組みです。

これによりコードの再利用性が高まり、共通の機能をまとめて管理できます。

// 基底クラス
public class Animal
{
    public string Name;
    public void Eat()
    {
        Console.WriteLine($"{Name}は食事をしています。");
    }
}
// 派生クラス
public class Dog : Animal
{
    public void Bark()
    {
        Console.WriteLine($"{Name}は吠えています。");
    }
}
class Program
{
    static void Main()
    {
        Dog dog = new Dog();
        dog.Name = "ポチ";
        dog.Eat();   // 基底クラスのメソッドを呼び出し
        dog.Bark();  // 派生クラス独自のメソッドを呼び出し
    }
}
ポチは食事をしています。
ポチは吠えています。

この例では、DogクラスがAnimalクラスを継承しています。

DogAnimalNameフィールドやEatメソッドをそのまま使え、さらにBarkメソッドを追加しています。

メソッドのオーバーライド

基底クラスのメソッドを派生クラスで再定義し、振る舞いを変更することを「オーバーライド」と言います。

基底クラスのメソッドにはvirtualキーワードを付け、派生クラスのオーバーライドするメソッドにはoverrideキーワードを付けます。

public class Animal
{
    public string Name;
    public virtual void Speak()
    {
        Console.WriteLine($"{Name}は何かを話しています。");
    }
}
public class Dog : Animal
{
    public override void Speak()
    {
        Console.WriteLine($"{Name}はワンワンと吠えています。");
    }
}
class Program
{
    static void Main()
    {
        Animal animal = new Animal { Name = "動物" };
        animal.Speak();
        Dog dog = new Dog { Name = "ポチ" };
        dog.Speak();
    }
}
動物は何かを話しています。
ポチはワンワンと吠えています。

virtualメソッドは派生クラスで上書き可能で、overrideで新しい実装を提供します。

これにより、同じメソッド名でもクラスごとに異なる動作を実現できます。

sealedキーワードで継承制限

sealedキーワードは、クラスやメソッドの継承やオーバーライドを禁止するために使います。

これにより、設計上の意図を明確にし、不適切な継承を防げます。

クラスに対するsealed

public sealed class FinalClass
{
    public void Show()
    {
        Console.WriteLine("このクラスは継承できません。");
    }
}
// 以下はコンパイルエラーになる
// public class DerivedClass : FinalClass { }

sealedクラスは継承できません。

上記のようにFinalClassを継承しようとするとコンパイルエラーになります。

メソッドに対するsealed

派生クラスでオーバーライドしたメソッドをさらに派生クラスでオーバーライドできないようにする場合、sealedを使います。

sealedoverrideと組み合わせて使います。

public class BaseClass
{
    public virtual void Display()
    {
        Console.WriteLine("BaseClassのDisplay");
    }
}
public class DerivedClass : BaseClass
{
    public sealed override void Display()
    {
        Console.WriteLine("DerivedClassのDisplay(これ以上オーバーライド不可)");
    }
}
public class FurtherDerivedClass : DerivedClass
{
    // 以下はコンパイルエラーになる
    // public override void Display() { }
}

DerivedClassDisplayメソッドはsealedなので、FurtherDerivedClassでのオーバーライドはできません。

is演算子とas演算子

継承関係にあるオブジェクトの型チェックやキャストに便利な演算子がisasです。

is演算子

isはオブジェクトが特定の型かどうかを判定し、bool値を返します。

Animal animal = new Dog();
if (animal is Dog)
{
    Console.WriteLine("animalはDog型です。");
}
else
{
    Console.WriteLine("animalはDog型ではありません。");
}
animalはDog型です。

as演算子

asはオブジェクトを指定した型に安全にキャストし、失敗した場合はnullを返します。

例外は発生しません。

Animal animal = new Dog();
Dog dog = animal as Dog;
if (dog != null)
{
    dog.Bark();
}
else
{
    Console.WriteLine("キャストに失敗しました。");
}
ポチは吠えています。

asを使うと、キャスト失敗時に例外が発生しないため安全に型変換できます。

isと組み合わせて使うことも多いです。

継承はコードの再利用や多態性を実現する重要な機能です。

基底クラスと派生クラスの関係を理解し、メソッドのオーバーライドや継承制限を適切に使い分けることで、堅牢で拡張性の高い設計が可能になります。

isas演算子は型チェックや安全なキャストに役立ちます。

抽象クラスとインターフェース

abstractクラスの用途

abstractクラスは、共通の機能や設計をまとめつつ、直接インスタンス化できないクラスです。

基本的な動作や共通のフィールド・メソッドを持ちつつ、派生クラスで具体的な実装を強制したい場合に使います。

public abstract class Animal
{
    public string Name { get; set; }
    // 共通のメソッド(実装あり)
    public void Eat()
    {
        Console.WriteLine($"{Name}は食事をしています。");
    }
    // 抽象メソッド(実装なし、派生クラスで必須実装)
    public abstract void Speak();
}

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

Animalクラスは共通のEatメソッドを持ちつつ、Speakメソッドは派生クラスで必ず実装しなければなりません。

抽象メソッドの実装義務

抽象メソッドはabstractキーワードを付けて宣言し、実装を持ちません。

派生クラスは必ずこのメソッドをオーバーライドして具体的な処理を実装する必要があります。

public class Dog : Animal
{
    public override void Speak()
    {
        Console.WriteLine($"{Name}はワンワンと吠えています。");
    }
}
class Program
{
    static void Main()
    {
        Dog dog = new Dog { Name = "ポチ" };
        dog.Eat();    // 基底クラスの共通メソッド
        dog.Speak();  // 派生クラスで実装した抽象メソッド
    }
}
ポチは食事をしています。
ポチはワンワンと吠えています。

抽象メソッドを実装しない派生クラスはコンパイルエラーになります。

これにより、設計上必要なメソッドの実装を強制できます。

interfaceの定義方法

interfaceはメソッドやプロパティのシグネチャ(宣言)だけを持ち、実装は持ちません。

複数のクラスで共通の契約(インターフェース)を定めるために使います。

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

public interface IFlyable
{
    void Fly();
}
public interface ISwimmable
{
    void Swim();
}
public class Bird : IFlyable
{
    public void Fly()
    {
        Console.WriteLine("鳥が飛んでいます。");
    }
}
public class Duck : IFlyable, ISwimmable
{
    public void Fly()
    {
        Console.WriteLine("アヒルが飛んでいます。");
    }
    public void Swim()
    {
        Console.WriteLine("アヒルが泳いでいます。");
    }
}

インターフェースは実装を持たないため、クラスは必ずすべてのメンバーを実装しなければなりません。

多重実装と名前の衝突回避

C#では複数のインターフェースを同時に実装できますが、同じ名前のメソッドが複数のインターフェースに存在すると名前の衝突が起こります。

これを回避するために「明示的インターフェース実装」を使います。

public interface IPrinter
{
    void Print();
}
public interface IScanner
{
    void Print();
}
public class MultiFunctionDevice : IPrinter, IScanner
{
    // IPrinterのPrint実装
    void IPrinter.Print()
    {
        Console.WriteLine("プリンターで印刷しています。");
    }
    // IScannerのPrint実装
    void IScanner.Print()
    {
        Console.WriteLine("スキャナーでスキャンしています。");
    }
    // クラス独自のメソッド
    public void Print()
    {
        Console.WriteLine("MultiFunctionDeviceの通常のPrintメソッド");
    }
}
class Program
{
    static void Main()
    {
        MultiFunctionDevice device = new MultiFunctionDevice();
        // クラスのメソッド呼び出し
        device.Print();
        // 明示的インターフェース実装の呼び出し
        ((IPrinter)device).Print();
        ((IScanner)device).Print();
    }
}
MultiFunctionDeviceの通常のPrintメソッド
プリンターで印刷しています。
スキャナーでスキャンしています。

明示的インターフェース実装は、インターフェース名を付けてメソッドを実装し、通常のメソッドとは別に管理します。

呼び出す際はインターフェースにキャストしてアクセスします。

抽象クラスは共通の機能を持ちつつ必須実装を強制し、インターフェースは実装の契約を定める役割を持ちます。

多重実装が可能なインターフェースでは名前の衝突を明示的実装で回避し、柔軟で安全な設計を実現できます。

ポリモーフィズムの実践

仮想メソッドとvirtualキーワード

ポリモーフィズム(多態性)は、同じメソッド呼び出しが異なるクラスで異なる動作をする仕組みです。

C#では、基底クラスのメソッドにvirtualキーワードを付けることで、そのメソッドを派生クラスでオーバーライド可能にします。

これを「仮想メソッド」と呼びます。

public class Animal
{
    public virtual void Speak()
    {
        Console.WriteLine("動物が鳴いています。");
    }
}
public class Dog : Animal
{
    public override void Speak()
    {
        Console.WriteLine("ワンワン");
    }
}
public class Cat : Animal
{
    public override void Speak()
    {
        Console.WriteLine("ニャー");
    }
}
class Program
{
    static void Main()
    {
        Animal animal1 = new Dog();
        Animal animal2 = new Cat();
        animal1.Speak();  // DogのSpeakが呼ばれる
        animal2.Speak();  // CatのSpeakが呼ばれる
    }
}
ワンワン
ニャー

この例では、AnimalクラスのSpeakメソッドがvirtualで宣言されているため、DogCatoverrideして独自の実装が可能です。

基底クラスの型であっても、実際のオブジェクトの型に応じたメソッドが呼ばれます。

overrideとnewの違い

派生クラスで基底クラスのメソッドを再定義する際、overridenewの2つのキーワードがありますが、動作が異なります。

  • override

基底クラスのvirtualメソッドをオーバーライドし、ポリモーフィズムを実現します。

基底クラスの参照を通じて呼び出しても派生クラスのメソッドが実行されます。

  • new

基底クラスのメソッドを隠蔽(シャドウイング)します。

基底クラスの参照を通じて呼び出すと基底クラスのメソッドが実行され、派生クラスの参照を通じて呼び出すと派生クラスのメソッドが実行されます。

public class BaseClass
{
    public virtual void Show()
    {
        Console.WriteLine("BaseClassのShow");
    }
}
public class DerivedOverride : BaseClass
{
    public override void Show()
    {
        Console.WriteLine("DerivedOverrideのShow");
    }
}
public class DerivedNew : BaseClass
{
    public new void Show()
    {
        Console.WriteLine("DerivedNewのShow");
    }
}
class Program
{
    static void Main()
    {
        BaseClass baseRef1 = new DerivedOverride();
        BaseClass baseRef2 = new DerivedNew();
        baseRef1.Show();  // DerivedOverrideのShowが呼ばれる
        baseRef2.Show();  // BaseClassのShowが呼ばれる
        DerivedNew derivedNew = new DerivedNew();
        derivedNew.Show();  // DerivedNewのShowが呼ばれる
    }
}
DerivedOverrideのShow
BaseClassのShow
DerivedNewのShow

この例からわかるように、overrideは基底クラスのメソッドを置き換え、ポリモーフィズムを実現します。

一方、newは基底クラスのメソッドを隠すだけで、基底クラス型の変数からは元のメソッドが呼ばれます。

ダックタイピング的な利用パターン

C#は静的型付け言語ですが、dynamic型やインターフェースを使うことで、ダックタイピング的な柔軟なコードを書くことができます。

ダックタイピングとは「もしそれがアヒルのように歩き、アヒルのように鳴くなら、それはアヒルである」という考え方で、オブジェクトの型ではなく振る舞いに注目します。

dynamicを使った例

public class Duck
{
    public void Quack()
    {
        Console.WriteLine("ガーガー");
    }
}
public class Person
{
    public void Quack()
    {
        Console.WriteLine("私はアヒルの真似をします。");
    }
}
class Program
{
    static void MakeQuack(dynamic obj)
    {
        obj.Quack();
    }
    static void Main()
    {
        Duck duck = new Duck();
        Person person = new Person();
        MakeQuack(duck);    // ガーガー
        MakeQuack(person);  // 私はアヒルの真似をします。
    }
}
ガーガー
私はアヒルの真似をします。

dynamic型はコンパイル時に型チェックを行わず、実行時にメソッドの存在を確認します。

これにより、異なる型でも同じメソッド名があれば呼び出せます。

インターフェースを使った例

public interface IQuackable
{
    void Quack();
}
public class Duck : IQuackable
{
    public void Quack()
    {
        Console.WriteLine("ガーガー");
    }
}
public class Person : IQuackable
{
    public void Quack()
    {
        Console.WriteLine("私はアヒルの真似をします。");
    }
}
class Program
{
    static void MakeQuack(IQuackable obj)
    {
        obj.Quack();
    }
    static void Main()
    {
        Duck duck = new Duck();
        Person person = new Person();
        MakeQuack(duck);    // ガーガー
        MakeQuack(person);  // 私はアヒルの真似をします。
    }
}
ガーガー
私はアヒルの真似をします。

インターフェースを使う方法は静的型付けのまま多態性を実現し、安全にメソッド呼び出しができます。

ポリモーフィズムはオブジェクト指向の重要な特徴で、virtualoverrideを使った仮想メソッドが基本です。

newキーワードはメソッドの隠蔽に使い、動作が異なるため注意が必要です。

dynamicやインターフェースを活用すると、柔軟で拡張性の高いコードが書けます。

ジェネリッククラス

型引数の宣言

ジェネリッククラスは、クラスの定義時に型をパラメーターとして受け取り、柔軟に様々な型に対応できるクラスです。

型引数を使うことで、型安全かつ再利用性の高いコードを書けます。

型引数はクラス名の後に<T>のように角括弧で指定します。

Tは型パラメーターの名前で、慣例的にTTKeyTValueなどが使われます。

public class Box<T>
{
    private T content;
    public void SetContent(T value)
    {
        content = value;
    }
    public T GetContent()
    {
        return content;
    }
}
class Program
{
    static void Main()
    {
        Box<int> intBox = new Box<int>();
        intBox.SetContent(123);
        Console.WriteLine(intBox.GetContent());  // 123
        Box<string> strBox = new Box<string>();
        strBox.SetContent("こんにちは");
        Console.WriteLine(strBox.GetContent());  // こんにちは
    }
}
123
こんにちは

この例では、Box<T>というジェネリッククラスを定義し、T型のデータを格納・取得できるようにしています。

intstringなど、任意の型でインスタンス化可能です。

制約(Constraints)の指定

型引数には制約(Constraints)を付けて、受け入れる型を限定できます。

これにより、型引数に特定のメソッドやプロパティが存在することを保証し、安全に操作できます。

主な制約の種類は以下の通りです。

制約キーワード説明
where T : class参照型(クラス)に限定
where T : struct値型(構造体)に限定
where T : new()引数なしコンストラクターを持つ型に限定
where T : 基底クラス名指定したクラスまたはその派生クラスに限定
where T : インターフェース名指定したインターフェースを実装する型に限定
public class Repository<T> where T : class, new()
{
    public T CreateInstance()
    {
        return new T();  // new()制約があるため引数なしコンストラクターが使える
    }
}
class Program
{
    static void Main()
    {
        Repository<StringBuilder> repo = new Repository<StringBuilder>();
        StringBuilder sb = repo.CreateInstance();
        sb.Append("ジェネリック制約の例");
        Console.WriteLine(sb.ToString());
    }
}
ジェネリック制約の例

この例では、Tclassnew()の制約を付けています。

これにより、Tは参照型で引数なしコンストラクターを持つ型に限定され、new T()が安全に使えます。

複数の制約はカンマで区切って指定可能です。

コレクションライブラリとの相性

.NETのコレクションライブラリはジェネリックを活用しており、型安全で効率的なデータ操作が可能です。

代表的なジェネリックコレクションには以下があります。

コレクション名説明
List<T>可変長のリスト。配列のように使えます。
Dictionary<TKey, TValue>キーと値のペアを管理する連想配列。
Queue<T>先入れ先出し(FIFO)のキュー。
Stack<T>後入れ先出し(LIFO)のスタック。
class Program
{
    static void Main()
    {
        List<int> numbers = new List<int> { 1, 2, 3 };
        numbers.Add(4);
        foreach (int num in numbers)
        {
            Console.WriteLine(num);
        }
        Dictionary<string, int> ages = new Dictionary<string, int>();
        ages["太郎"] = 30;
        ages["花子"] = 25;
        Console.WriteLine($"太郎の年齢は{ages["太郎"]}歳です。");
    }
}
1
2
3
4
太郎の年齢は30歳です。

ジェネリックコレクションは型安全なので、誤った型のデータを格納するミスを防げます。

また、ボクシングやアンボクシングのオーバーヘッドがなく、高速に動作します。

ジェネリッククラスは型引数を使って柔軟に設計でき、制約を付けることで安全性を高められます。

.NETのコレクションライブラリはジェネリックを活用しており、日常的に使うことで効率的なプログラミングが可能です。

演算子オーバーロード

代表的な演算子の書き方

C#では、クラスや構造体に対して演算子をオーバーロード(再定義)することができます。

これにより、独自の型でも+-==などの演算子を使って直感的に操作できるようになります。

演算子オーバーロードはoperatorキーワードを使い、public staticメソッドとして定義します。

以下は代表的な演算子のオーバーロード例です。

public class Point
{
    public int X { get; }
    public int Y { get; }
    public Point(int x, int y)
    {
        X = x;
        Y = y;
    }
    // + 演算子のオーバーロード
    public static Point operator +(Point a, Point b)
    {
        return new Point(a.X + b.X, a.Y + b.Y);
    }
    // - 演算子のオーバーロード
    public static Point operator -(Point a, Point b)
    {
        return new Point(a.X - b.X, a.Y - b.Y);
    }
    // == 演算子のオーバーロード
    public static bool operator ==(Point a, Point b)
    {
        if (ReferenceEquals(a, b)) return true;
        if (a is null || b is null) return false;
        return a.X == b.X && a.Y == b.Y;
    }
    // != 演算子のオーバーロード
    public static bool operator !=(Point a, Point b)
    {
        return !(a == b);
    }
    // Equalsメソッドのオーバーライド(後述)
    public override bool Equals(object obj)
    {
        if (obj is Point p)
        {
            return this == p;
        }
        return false;
    }
    // GetHashCodeメソッドのオーバーライド(後述)
    public override int GetHashCode()
    {
        return HashCode.Combine(X, Y);
    }
}
class Program
{
    static void Main()
    {
        Point p1 = new Point(2, 3);
        Point p2 = new Point(4, 5);
        Point p3 = p1 + p2;
        Point p4 = p2 - p1;
        Console.WriteLine($"p3: ({p3.X}, {p3.Y})");  // (6, 8)
        Console.WriteLine($"p4: ({p4.X}, {p4.Y})");  // (2, 2)
        Console.WriteLine($"p1 == p2? {p1 == p2}");  // False
        Console.WriteLine($"p1 != p2? {p1 != p2}");  // True
        Point p5 = new Point(2, 3);
        Console.WriteLine($"p1 == p5? {p1 == p5}");  // True
    }
}
p3: (6, 8)
p4: (2, 2)
p1 == p2? False
p1 != p2? True
p1 == p5? True

この例では、Pointクラスに+-==!=演算子をオーバーロードしています。

==!=はペアで実装し、EqualsGetHashCodeも整合性を保つためにオーバーライドしています。

Equalsと==の整合性

Equalsメソッドはオブジェクトの等価性を判定する標準的なメソッドで、==演算子は比較演算子です。

クラスで==をオーバーロードする場合、Equalsメソッドもオーバーライドして整合性を保つ必要があります。

  • Equalsは参照型の等価性を判定するために使われ、object型の引数を受け取ります
  • ==は演算子として使われ、オーバーロード可能です

Equals==の実装が異なると、同じオブジェクトでも比較結果が異なることがあり、バグの原因になります。

public override bool Equals(object obj)
{
    if (obj is Point p)
    {
        return this.X == p.X && this.Y == p.Y;
    }
    return false;
}
public static bool operator ==(Point a, Point b)
{
    if (ReferenceEquals(a, b)) return true;
    if (a is null || b is null) return false;
    return a.Equals(b);
}
public static bool operator !=(Point a, Point b)
{
    return !(a == b);
}

このように==演算子はEqualsを呼び出す形にすると、両者の判定基準が一致しやすくなります。

GetHashCodeの実装ガイドライン

GetHashCodeはオブジェクトのハッシュコードを返すメソッドで、ハッシュベースのコレクション(DictionaryHashSetなど)で使われます。

Equalsをオーバーライドした場合は、GetHashCodeも必ずオーバーライドし、等価なオブジェクトは同じハッシュコードを返すように実装しなければなりません。

public override int GetHashCode()
{
    return HashCode.Combine(X, Y);
}
  • HashCode.Combineは複数のフィールドを組み合わせてハッシュコードを生成する便利なメソッドです
  • フィールドの値が同じなら同じハッシュコードを返すことが重要です
  • ハッシュコードが異なってもEqualstrueなら問題ですが、逆は問題になります

不適切なGetHashCodeの実装は、コレクションの動作不良やパフォーマンス低下を招くため注意が必要です。

演算子オーバーロードはクラスの使いやすさを向上させますが、EqualsGetHashCodeとの整合性を保つことが重要です。

代表的な演算子は+-==!=で、これらを適切に実装することで直感的で安全な比較や演算が可能になります。

レコード型との違い

クラスとrecordの設計基準

C# 9.0から導入されたrecord型は、主にデータの保持と比較に特化した参照型で、クラスと似ていますが設計思想や用途に違いがあります。

クラスは状態と振る舞いを持つオブジェクトの設計に適しているのに対し、recordは不変のデータを表現しやすく、値の比較を簡単に行うために設計されています。

クラスの特徴

  • 状態(フィールドやプロパティ)と振る舞い(メソッド)を持つ
  • 可変(ミュータブル)なデータを扱うことが多い
  • 等価性は参照の等価性がデフォルト(Equals==はオーバーライドしない限り参照比較)
  • 継承や複雑な振る舞いの実装に向いている

レコードの特徴

  • 主にデータの保持に特化した型
  • デフォルトでイミュータブル(読み取り専用)なプロパティを持つことが多い
  • 値の等価性(プロパティの値が同じなら等しい)を自動的にサポート
  • with式による簡単なコピーと変更が可能
  • データ転送オブジェクト(DTO)や設定値の表現に適している
// クラスの例
public class PersonClass
{
    public string Name { get; set; }
    public int Age { get; set; }
}
// レコードの例
public record PersonRecord(string Name, int Age);

クラスはプロパティの値が同じでも異なるインスタンスは等しくありませんが、レコードは値の等価性を持つため、以下のように比較できます。

PersonClass c1 = new PersonClass { Name = "太郎", Age = 30 };
PersonClass c2 = new PersonClass { Name = "太郎", Age = 30 };
Console.WriteLine(c1 == c2);  // False(参照比較)
PersonRecord r1 = new PersonRecord("太郎", 30);
PersonRecord r2 = new PersonRecord("太郎", 30);
Console.WriteLine(r1 == r2);  // True(値比較)

イミュータブルデータの扱い

レコードはイミュータブル(不変)なデータを扱うのに適しています。

デフォルトでは、レコードのプロパティはinitアクセサーを持ち、オブジェクト生成時にのみ値を設定でき、その後は変更できません。

public record PersonRecord
{
    public string Name { get; init; }
    public int Age { get; init; }
}
class Program
{
    static void Main()
    {
        var person = new PersonRecord { Name = "花子", Age = 25 };
        Console.WriteLine($"{person.Name}{person.Age} 歳です。");
        // person.Age = 26;  // コンパイルエラー:init-onlyプロパティは変更不可
    }
}

イミュータブルなデータはスレッドセーフであり、状態の変更によるバグを防ぎやすいメリットがあります。

with式によるコピーと変更

レコードはwith式を使って既存のインスタンスを元に一部のプロパティだけ変更した新しいインスタンスを簡単に作成できます。

var person1 = new PersonRecord("太郎", 30);
var person2 = person1 with { Age = 31 };
Console.WriteLine(person1);  // PersonRecord { Name = 太郎, Age = 30 }
Console.WriteLine(person2);  // PersonRecord { Name = 太郎, Age = 31 }

この機能により、イミュータブルなまま柔軟にデータを更新できます。

クラスは振る舞いを持つオブジェクト設計に適し、レコードは値の等価性を持つイミュータブルなデータ表現に適しています。

用途に応じて使い分けることで、より安全で効率的なコード設計が可能です。

データクラスのToStringカスタマイズ

文字列表現の標準化

C#のすべてのクラスはobjectクラスを継承しており、ToStringメソッドを持っています。

ToStringはオブジェクトの文字列表現を返すメソッドで、デフォルトではクラス名が返されます。

しかし、データクラスの場合は、オブジェクトの状態をわかりやすく表現するためにToStringをオーバーライドしてカスタマイズすることが一般的です。

public class Person
{
    public string Name { get; set; }
    public int Age { get; set; }
    // ToStringのオーバーライドで文字列表現をカスタマイズ
    public override string ToString()
    {
        return $"名前: {Name}, 年齢: {Age}歳";
    }
}
class Program
{
    static void Main()
    {
        Person person = new Person { Name = "太郎", Age = 30 };
        Console.WriteLine(person.ToString());
    }
}
名前: 太郎, 年齢: 30歳

このようにToStringをオーバーライドすることで、デバッグ時やログ出力、UI表示などでオブジェクトの内容を簡単に確認できるようになります。

標準化された文字列表現は、コードの可読性や保守性を高める効果もあります。

文字列補間の活用

C# 6.0以降で使える文字列補間(interpolated strings)は、ToStringのカスタマイズに非常に便利です。

$記号を文字列の前に付けて、波括弧{}内に変数や式を直接埋め込めます。

public class Product
{
    public string Name { get; set; }
    public decimal Price { get; set; }
    public override string ToString()
    {
        // 文字列補間を使って見やすく整形
        return $"商品名: {Name}, 価格: {Price:C}";
    }
}
class Program
{
    static void Main()
    {
        Product product = new Product { Name = "ノートパソコン", Price = 123456.78m };
        Console.WriteLine(product.ToString());
    }
}
商品名: ノートパソコン, 価格: ¥123,456.78

この例では、{Price:C}のように書くことで通貨形式にフォーマットされます。

文字列補間は複雑な文字列を簡潔に書けるため、ToStringの実装がシンプルで読みやすくなります。

複数行の文字列補間

複数行の文字列を返したい場合も、文字列補間と@記号を組み合わせて使えます。

public override string ToString()
{
    return $@"商品情報:
  名前: {Name}
  価格: {Price:C}";
}

これにより、改行やインデントを含む整形された文字列を簡単に作成できます。

ToStringのカスタマイズはデータクラスの状態をわかりやすく表現するために重要です。

文字列補間を活用すると、コードが簡潔で読みやすくなり、メンテナンス性も向上します。

適切な文字列表現を標準化しておくことで、デバッグやログ出力の効率が大幅にアップします。

IDisposableとusing

リソース解放のパターン

C#では、ファイル操作やデータベース接続、ネットワーク通信などの外部リソースを扱う際に、使用後に必ずリソースを解放する必要があります。

これを適切に行わないと、メモリリークやリソース不足の原因になります。

IDisposableインターフェースは、リソース解放のための標準的なパターンを提供します。

IDisposableを実装したクラスは、Disposeメソッドを持ち、ここでリソースの解放処理を記述します。

public class ResourceHolder : IDisposable
{
    private bool disposed = false;
    // 外部リソースの例(ファイルやネットワークなど)
    // ここでは簡略化のため省略
    public void UseResource()
    {
        if (disposed)
            throw new ObjectDisposedException("ResourceHolder");
        Console.WriteLine("リソースを使用中...");
    }
    // IDisposableの実装
    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this);  // ガベージコレクターにファイナライザーの呼び出し不要を通知
    }
    protected virtual void Dispose(bool disposing)
    {
        if (!disposed)
        {
            if (disposing)
            {
                // マネージリソースの解放(例:他のIDisposableオブジェクトのDispose呼び出し)
                Console.WriteLine("マネージリソースを解放しています。");
            }
            // アンマネージリソースの解放(例:ファイルハンドルなど)
            Console.WriteLine("アンマネージリソースを解放しています。");
            disposed = true;
        }
    }
    ~ResourceHolder()
    {
        Dispose(false);
    }
}
class Program
{
    static void Main()
    {
        using (var resource = new ResourceHolder())
        {
            resource.UseResource();
        }  // usingブロック終了時にDisposeが自動的に呼ばれる
    }
}
リソースを使用中...
マネージリソースを解放しています。
アンマネージリソースを解放しています。

この例では、ResourceHolderクラスがIDisposableを実装し、Disposeメソッドでリソース解放処理を行っています。

using文を使うことで、スコープを抜けると自動的にDisposeが呼ばれ、安全にリソースを解放できます。

デストラクターとの比較

デストラクター(ファイナライザー)は、オブジェクトがガベージコレクションで回収される際に呼ばれるメソッドで、アンマネージリソースの解放に使われます。

ただし、デストラクターは呼ばれるタイミングが不確定であり、即時のリソース解放には向いていません。

public class SampleClass
{
    ~SampleClass()
    {
        Console.WriteLine("デストラクターが呼ばれました。");
    }
}

デストラクターの特徴:

  • ガベージコレクションがオブジェクトを回収する際に呼ばれる
  • 呼ばれるタイミングは予測できず、遅延する可能性がある
  • リソース解放の保証が弱い
  • パフォーマンスに影響を与えることがある

一方、IDisposableDisposeメソッドは明示的に呼び出すか、using文で自動的に呼び出されるため、リソースを確実かつ即時に解放できます。

まとめると

項目IDisposable/Disposeデストラクター(ファイナライザー)
呼び出しタイミング明示的に呼び出すかusingで自動呼び出しガベージコレクションによる回収時
解放の即時性即時にリソース解放可能不確定で遅延する可能性あり
使用目的マネージ・アンマネージリソースの解放主にアンマネージリソースのバックアップ的解放
パフォーマンス適切に使えば効率的過剰に使うとパフォーマンス低下の原因になる

IDisposableusingはリソース管理の基本パターンであり、確実かつ効率的にリソースを解放できます。

デストラクターは補助的な役割で使い、即時解放が必要な場合はDisposeを使うことが推奨されます。

これにより、メモリリークやリソース不足を防ぎ、安定したアプリケーションを作成できます。

イベントとデリゲート

イベントフィールドの宣言

C#のイベントは、あるオブジェクトが発生させる「通知」を他のオブジェクトが受け取る仕組みです。

イベントはデリゲートを使って実装され、イベントフィールドとしてクラス内に宣言します。

イベントフィールドはeventキーワードを使い、デリゲート型で宣言します。

一般的にはEventHandlerEventHandler<TEventArgs>が使われますが、独自のデリゲート型も定義可能です。

public class Button
{
    // イベントフィールドの宣言(標準のEventHandlerを使用)
    public event EventHandler Clicked;
    // イベントを発生させるメソッド
    public void OnClick()
    {
        // イベントが登録されていれば呼び出す
        Clicked?.Invoke(this, EventArgs.Empty);
    }
}

この例では、ButtonクラスにClickedというイベントを宣言しています。

ClickedEventHandler型のデリゲートで、イベントが発生すると登録されたメソッドが呼ばれます。

Publisher -Subscriberモデル

イベントは「Publisher -Subscriber(発行者 -購読者)」モデルに基づいています。

Publisher(発行者)がイベントを発生させ、Subscriber(購読者)がそのイベントに反応します。

class Program
{
    static void Main()
    {
        Button button = new Button();
        // イベント購読(イベントハンドラーの登録)
        button.Clicked += Button_Clicked;
        // ボタンをクリック(イベント発生)
        button.OnClick();
    }
    // イベントハンドラー(イベント発生時に呼ばれるメソッド)
    private static void Button_Clicked(object sender, EventArgs e)
    {
        Console.WriteLine("ボタンがクリックされました。");
    }
}
ボタンがクリックされました。

この例では、button.ClickedイベントにButton_Clickedメソッドを登録しています。

OnClickメソッドが呼ばれるとイベントが発生し、登録されたメソッドが実行されます。

これにより、発行者と購読者が疎結合で連携できます。

ActionとFuncで簡潔に記述

C#には汎用的なデリゲート型としてActionFuncが用意されており、イベントやデリゲートの宣言を簡潔に記述できます。

  • Actionは戻り値なしのメソッドを表し、引数の型を指定できます
  • Funcは戻り値ありのメソッドを表し、最後の型パラメーターが戻り値の型です

Actionを使ったイベントの例

public class Timer
{
    // 引数なしのActionデリゲートをイベントとして宣言
    public event Action Tick;
    public void OnTick()
    {
        Tick?.Invoke();
    }
}
class Program
{
    static void Main()
    {
        Timer timer = new Timer();
        // ラムダ式でイベント購読
        timer.Tick += () => Console.WriteLine("タイマーが動作しました。");
        timer.OnTick();
    }
}
タイマーが動作しました。

Funcを使った例

Funcは戻り値がある場合に使います。

例えば、計算結果を返すデリゲートをイベントのように使うことも可能です。

public class Calculator
{
    public event Func<int, int, int> Calculate;
    public int OnCalculate(int a, int b)
    {
        if (Calculate != null)
        {
            return Calculate.Invoke(a, b);
        }
        return 0;
    }
}
class Program
{
    static void Main()
    {
        Calculator calc = new Calculator();
        // 足し算の処理を登録
        calc.Calculate += (x, y) => x + y;
        int result = calc.OnCalculate(3, 4);
        Console.WriteLine($"計算結果: {result}");
    }
}
計算結果: 7

イベントとデリゲートはC#の強力な機能で、eventキーワードでイベントフィールドを宣言し、Publisher -Subscriberモデルで疎結合な通知機構を実現します。

ActionFuncを使うと、より簡潔で柔軟なイベント設計が可能です。

部分クラスとネストクラス

partial classの用途

partial classは、1つのクラスの定義を複数のファイルや複数の場所に分割して記述できる機能です。

大規模なクラスや自動生成コードと手動で書くコードを分けたい場合に便利です。

コンパイラーはビルド時に分割された部分をまとめて1つのクラスとして扱います。

// ファイル1: Person.Part1.cs
public partial class Person
{
    public string Name { get; set; }
    public void SayHello()
    {
        Console.WriteLine($"こんにちは、{Name}です。");
    }
}
// ファイル2: Person.Part2.cs
public partial class Person
{
    public int Age { get; set; }
    public void ShowAge()
    {
        Console.WriteLine($"{Name}の年齢は{Age}歳です。");
    }
}
class Program
{
    static void Main()
    {
        Person p = new Person { Name = "太郎", Age = 30 };
        p.SayHello();
        p.ShowAge();
    }
}
こんにちは、太郎です。
太郎の年齢は30歳です。

この例では、Personクラスの定義を2つのファイルに分けていますが、1つのクラスとして機能します。

partialを使うことで、チーム開発での分担や自動生成コードの管理がしやすくなります。

Nested classでスコープ管理

ネストクラス(入れ子クラス)は、あるクラスの内部に定義されたクラスのことです。

ネストクラスは外部クラスのメンバーにアクセスでき、外部クラスのスコープ内でのみ使いたい補助的なクラスを定義するのに適しています。

public class OuterClass
{
    private int outerValue = 10;
    // ネストクラスの定義
    public class NestedClass
    {
        public void ShowMessage()
        {
            Console.WriteLine("ネストクラスのメソッドです。");
            // outerValueには直接アクセスできない(非staticの場合)
        }
    }
    // ネストクラスのインスタンスを使うメソッド
    public void UseNested()
    {
        NestedClass nested = new NestedClass();
        nested.ShowMessage();
    }
}
class Program
{
    static void Main()
    {
        OuterClass outer = new OuterClass();
        outer.UseNested();
        // 外部からネストクラスのインスタンスを作成することも可能
        OuterClass.NestedClass nested = new OuterClass.NestedClass();
        nested.ShowMessage();
    }
}
ネストクラスのメソッドです。
ネストクラスのメソッドです。

ネストクラスは外部クラスのprivateメンバーにはアクセスできませんが、staticでない場合は外部クラスのインスタンスを通じてアクセス可能です。

ネストクラスを使うことで、クラスのスコープを整理し、関連するクラスをまとめて管理できます。

partial classはクラス定義を分割して管理しやすくするために使い、nested classはクラスの内部に補助的なクラスを定義してスコープを限定するために使います。

どちらもコードの可読性や保守性を高める便利な機能です。

アクセス修飾子一覧

public・private・protected

C#のアクセス修飾子は、クラスやメンバー(フィールド、メソッド、プロパティなど)へのアクセス範囲を制御します。

まずは基本的な3つの修飾子から説明します。

  • public

どこからでもアクセス可能です。

クラスの外部や他のアセンブリからも自由に利用できます。

APIの公開部分や外部から利用されるメンバーに使います。

  • private

定義されたクラスの内部からのみアクセス可能です。

外部からは見えず、カプセル化の基本となる修飾子です。

クラスの内部実装を隠すために使います。

  • protected

定義されたクラスと、そのクラスを継承した派生クラスからアクセス可能です。

外部の非派生クラスからはアクセスできません。

継承関係でのアクセス制御に使います。

public class BaseClass
{
    public int PublicValue = 1;
    private int PrivateValue = 2;
    protected int ProtectedValue = 3;
    public void ShowValues()
    {
        Console.WriteLine($"Public: {PublicValue}, Private: {PrivateValue}, Protected: {ProtectedValue}");
    }
}
public class DerivedClass : BaseClass
{
    public void ShowProtected()
    {
        // PublicValueはアクセス可能
        Console.WriteLine($"PublicValue: {PublicValue}");
        // PrivateValueはアクセス不可(コンパイルエラー)
        // Console.WriteLine($"PrivateValue: {PrivateValue}");
        // ProtectedValueはアクセス可能
        Console.WriteLine($"ProtectedValue: {ProtectedValue}");
    }
}
class Program
{
    static void Main()
    {
        BaseClass baseObj = new BaseClass();
        Console.WriteLine(baseObj.PublicValue);  // OK
        // Console.WriteLine(baseObj.PrivateValue);  // エラー
        // Console.WriteLine(baseObj.ProtectedValue);  // エラー
        DerivedClass derivedObj = new DerivedClass();
        derivedObj.ShowProtected();
    }
}
Public: 1, Private: 2, Protected: 3
PublicValue: 1
ProtectedValue: 3

internalとprotected internal

  • internal

同一アセンブリ(同じプロジェクトやDLL)内からアクセス可能で、外部アセンブリからはアクセスできません。

ライブラリ内部でのみ使いたいメンバーに適しています。

  • protected internal

同一アセンブリ内からはアクセス可能で、さらに他のアセンブリでも派生クラスからアクセス可能です。

protectedinternalの両方の条件を満たすアクセス範囲です。

public class Sample
{
    internal int InternalValue = 10;
    protected internal int ProtectedInternalValue = 20;
}
class Program
{
    static void Main()
    {
        Sample sample = new Sample();
        Console.WriteLine(sample.InternalValue);           // 同一アセンブリ内なのでアクセス可能
        Console.WriteLine(sample.ProtectedInternalValue);  // 同一アセンブリ内なのでアクセス可能
    }
}

protected internalは、アクセス範囲が広く柔軟ですが、設計上の意図を明確にするために使いどころを考慮する必要があります。

private protectedの使いどころ

  • private protected

C# 7.2で導入された修飾子で、同一アセンブリ内かつ派生クラスからのみアクセス可能です。

つまり、privateprotected internalの中間的なアクセス制御です。

public class BaseClass
{
    private protected int PrivateProtectedValue = 100;
}
public class DerivedClass : BaseClass
{
    public void ShowValue()
    {
        Console.WriteLine($"PrivateProtectedValue: {PrivateProtectedValue}");  // アクセス可能
    }
}
class Program
{
    static void Main()
    {
        DerivedClass derived = new DerivedClass();
        derived.ShowValue();
        BaseClass baseObj = new BaseClass();
        // Console.WriteLine(baseObj.PrivateProtectedValue);  // エラー:同一アセンブリ内でも非派生クラスからは不可
    }
}

private protectedは、同じアセンブリ内で継承関係にあるクラスにだけアクセスを許可したい場合に使います。

外部からのアクセスを厳しく制限しつつ、継承したクラスには必要な情報を渡したいときに便利です。

アクセス修飾子はクラス設計の重要な要素で、適切に使い分けることでカプセル化や安全性を高められます。

publicは公開、privateは隠蔽、protectedは継承用、internalは同一アセンブリ限定、protected internalprivate protectedはより細かいアクセス制御を実現します。

テストしやすいクラス設計

依存性注入の基本

テストしやすいクラス設計の重要なポイントの一つが「依存性注入(Dependency Injection、DI)」です。

依存性注入とは、クラスが必要とする外部のオブジェクト(依存オブジェクト)を自分で生成せず、外部から渡してもらう設計手法です。

これにより、クラスの結合度が下がり、テスト時に依存オブジェクトを差し替えやすくなります。

依存性注入の例

// 依存するインターフェース
public interface IMessageService
{
    void SendMessage(string message);
}
// 依存オブジェクトの実装
public class EmailService : IMessageService
{
    public void SendMessage(string message)
    {
        Console.WriteLine($"メール送信: {message}");
    }
}
// 依存性注入を受けるクラス
public class Notification
{
    private readonly IMessageService _messageService;
    // コンストラクターで依存オブジェクトを注入
    public Notification(IMessageService messageService)
    {
        _messageService = messageService;
    }
    public void Notify(string message)
    {
        _messageService.SendMessage(message);
    }
}
class Program
{
    static void Main()
    {
        IMessageService emailService = new EmailService();
        Notification notification = new Notification(emailService);
        notification.Notify("テストメッセージ");
    }
}
メール送信: テストメッセージ

この例では、NotificationクラスはIMessageServiceに依存していますが、自分でEmailServiceを生成せず、外部から注入しています。

これにより、テスト時に別の実装を渡すことが容易になります。

インターフェースでモック化

テスト時には、実際の依存オブジェクトの代わりに「モック(Mock)」と呼ばれるテスト用の代替オブジェクトを使います。

モックはインターフェースを実装し、テストに必要な動作だけを模倣します。

これにより、外部環境に依存しない単体テストが可能になります。

モックを使ったテスト例

// テスト用モック実装
public class MockMessageService : IMessageService
{
    public string LastMessage { get; private set; }
    public void SendMessage(string message)
    {
        LastMessage = message;  // メッセージを記録するだけ
    }
}
class Program
{
    static void Main()
    {
        var mockService = new MockMessageService();
        var notification = new Notification(mockService);
        notification.Notify("テストメッセージ");
        // モックに記録されたメッセージを検証
        Console.WriteLine(mockService.LastMessage == "テストメッセージ"
            ? "テスト成功"
            : "テスト失敗");
    }
}
テスト成功

この例では、MockMessageServiceIMessageServiceを実装し、実際の送信処理は行わずにメッセージを記録しています。

テストコードはモックの状態を検証することで、Notificationクラスの動作を確認できます。

依存性注入を使うことでクラスの結合度を下げ、外部依存を切り離せます。

インターフェースを活用してモックを作成すれば、外部環境に依存しない単体テストが簡単に実現できます。

これらの設計はテストの自動化や品質向上に欠かせない重要な技術です。

クラス設計のアンチパターン

巨大クラスの問題点

巨大クラス(God ClassやBlob Classとも呼ばれる)は、1つのクラスに過剰な責任や機能が集中してしまった状態のことを指します。

こうしたクラスは以下のような問題を引き起こします。

  • 可読性の低下

クラスのコード量が膨大になるため、全体像を把握しづらくなり、理解や修正が困難になります。

  • 保守性の悪化

変更が他の機能に影響を与えやすく、バグの温床になりやすいです。

修正や拡張が難しくなります。

  • 再利用性の低下

多くの機能が詰め込まれているため、部分的に使いたい場合でも切り出しにくく、再利用が難しくなります。

  • テスト困難

巨大クラスは多くの依存関係や状態を持つため、単体テストが複雑になり、テストの網羅性が低下します。

// 巨大クラスの例(問題点のイメージ)
public class OrderManager
{
    public void CreateOrder() { /* 注文作成処理 */ }
    public void ValidateOrder() { /* 注文検証処理 */ }
    public void CalculateDiscount() { /* 割引計算処理 */ }
    public void ProcessPayment() { /* 支払い処理 */ }
    public void SendConfirmationEmail() { /* 確認メール送信 */ }
    public void GenerateReport() { /* レポート生成 */ }
    // ... 多数の責任が混在
}

このように1つのクラスに多くの責任が集中すると、設計の原則である単一責任原則(SRP)に反し、コードの品質が低下します。

神クラスを避けるリファクタリング

巨大クラスを改善するためには、責任を適切に分割し、役割ごとにクラスを分けるリファクタリングが必要です。

以下の手法が有効です。

単一責任原則(SRP)に基づく分割

クラスは「1つの責任だけを持つ」ように設計します。

例えば、上記のOrderManagerを以下のように分割します。

public class OrderCreator
{
    public void CreateOrder() { /* 注文作成処理 */ }
    public void ValidateOrder() { /* 注文検証処理 */ }
}
public class PaymentProcessor
{
    public void ProcessPayment() { /* 支払い処理 */ }
}
public class NotificationService
{
    public void SendConfirmationEmail() { /* 確認メール送信 */ }
}
public class ReportGenerator
{
    public void GenerateReport() { /* レポート生成 */ }
}

これにより、各クラスは明確な役割を持ち、理解しやすく、テストもしやすくなります。

クラス間の依存関係を整理

分割したクラス同士の依存関係は、依存性注入などを使って疎結合に保つことが望ましいです。

これにより、変更の影響範囲を限定できます。

適切な名前付け

クラス名はその責任を明確に表す名前にし、役割が一目でわかるようにします。

これも可読性向上に寄与します。

小さなメソッドに分割

クラス内のメソッドも小さく分割し、1つのメソッドが1つの処理だけを行うようにします。

これにより、テストやデバッグが容易になります。

巨大クラスは開発初期には便利に見えることもありますが、長期的にはコードの複雑化やバグの温床になります。

リファクタリングで責任を分割し、単一責任原則を守ることで、保守性・拡張性の高いクラス設計が実現できます。

まとめ

この記事では、C#のクラス設計に関する基本から応用まで幅広く解説しました。

クラスの構文や継承、プロパティ、コンストラクターの使い方、ポリモーフィズムやジェネリック、演算子オーバーロードなどの重要な概念を理解できます。

また、テストしやすい設計やアンチパターンの回避方法も紹介し、実践的なクラス設計のポイントが身につきます。

これらを活用することで、保守性や再利用性の高い効率的なC#プログラムが書けるようになります。

関連記事

Back to top button
目次へ