【C#】クラスとオブジェクト完全ガイド|プロパティ・コンストラクタ・this・ToString/Equals・sealed/partialまで

C# はオブジェクト指向言語であり、クラスはそのもっとも基本的な構成要素です。クラスを正しく設計できると、保守性・再利用性・可読性の高いコードが書けるようになります。

本記事では「クラスとは何か」から始まり、プロパティの種類・コンストラクタ・オブジェクト初期化子・this キーワード・ToString/Equals のオーバーライド・sealed/partial クラスまで、クラス設計の基礎を体系的に解説します。

スポンサーリンク

クラスとオブジェクトの概念

クラスは「設計図」であり、オブジェクト(インスタンス)は「その設計図から作られた実体」です。1つのクラスから何個でもオブジェクトを生成できます。

たとえば「Person(人)クラス」は「名前と年齢を持ち、あいさつができる」という設計図です。その設計図から「Taro(25歳)」「Hanako(30歳)」という具体的な人物オブジェクトを作れます。

クラスとオブジェクトの基本
// クラス(設計図)
class Person
{
    public string Name { get; set; } = "";
    public int    Age  { get; set; }

    public void Greet()
        => Console.WriteLine($"こんにちは、{Name}({Age}歳)です。");
}

// オブジェクトの生成と使用
var taro   = new Person { Name = "Taro",   Age = 25 };
var hanako = new Person { Name = "Hanako", Age = 30 };

taro.Greet();   // こんにちは、Taro(25歳)です。
hanako.Greet(); // こんにちは、Hanako(30歳)です。
クラスの構成要素
フィールド — クラス内部に持つデータ(通常 private)
プロパティ — フィールドへの安全なアクセス口(外部から get/set)
コンストラクタ — オブジェクト生成時に呼ばれる初期化メソッド
メソッド — オブジェクトが実行できる処理
イベント — 外部に通知を送る仕組み

フィールドの宣言

フィールドはクラスが内部的に持つデータ変数です。外部から直接アクセスされないよう private にするのが基本です。

フィールドの宣言パターン
class BankAccount
{
    // インスタンスフィールド(private が原則)
    private decimal _balance;
    private string  _owner;

    // 読み取り専用フィールド(コンストラクタで設定・以降は変更不可)
    private readonly string _accountId;

    // 定数フィールド
    private const decimal MinBalance = 0m;

    // 静的フィールド(全インスタンスで共有)
    private static int _accountCount = 0;
}
フィールドを public にするとクラス外から直接変更できてしまい、不正な値が入るリスクがあります。データを外部に公開したい場合は プロパティ を使い、必要に応じてバリデーションを設けましょう。

プロパティの書き方

プロパティはフィールドへの制御されたアクセス窓口です。C# では複数の書き方があります。

自動実装プロパティ(Auto-implemented Property)

バッキングフィールド(内部で値を保持する private フィールド)をコンパイラが自動生成する、もっともシンプルな書き方です。

自動実装プロパティ
class Product
{
    // get と set を持つ通常の自動実装プロパティ
    public string Name  { get; set; } = "";
    public decimal Price { get; set; }

    // get のみ(読み取り専用・コンストラクタか init のみで設定可)
    public string Id { get; }

    // init アクセサ(C# 9以降: オブジェクト初期化子でのみ設定可)
    public string Category { get; init; } = "General";

    // private set(クラス外からは読み取りのみ・クラス内からは書き込み可)
    public int StockCount { get; private set; }
}

フルプロパティ(バリデーション付き)

値を検証したい・変換が必要な場合はフルプロパティで getter/setter を手書きします。

フルプロパティ(バリデーション付き)
class Person
{
    private string _name = "";
    private int _age;

    public string Name
    {
        get => _name;
        set
        {
            if (string.IsNullOrWhiteSpace(value))
                throw new ArgumentException("名前は空にできません");
            _name = value.Trim();
        }
    }

    public int Age
    {
        get => _age;
        set
        {
            if (value < 0 || value > 150)
                throw new ArgumentOutOfRangeException(nameof(value), "年齢が無効です");
            _age = value;
        }
    }

    // 計算プロパティ(get のみ・バッキングフィールドなし)
    public bool IsAdult => _age >= 18;
}
種類 書き方 使いどころ
自動実装 public T Prop { get; set; } バリデーション不要・シンプルなデータ
private set public T Prop { get; private set; } 外部から読み取り専用・内部からは書き換え可
init public T Prop { get; init; } オブジェクト初期化子でのみ設定・以後変更不可
フルプロパティ get/set を手書き バリデーション・変換・副作用が必要な場合
計算プロパティ public T Prop => 式; フィールドから計算・保存不要

コンストラクタ

コンストラクタは new でオブジェクトを生成するときに自動的に呼ばれる特殊なメソッドです。クラス名と同じ名前で、戻り値の型を持ちません。

コンストラクタの定義とオーバーロード
class Rectangle
{
    public double Width  { get; }
    public double Height { get; }

    // 引数なしコンストラクタ(デフォルトコンストラクタ)
    public Rectangle() : this(1.0, 1.0) { } // → 下のコンストラクタに委譲

    // 引数ありコンストラクタ
    public Rectangle(double width, double height)
    {
        if (width <= 0 || height <= 0)
            throw new ArgumentException("サイズは正の値である必要があります");
        Width  = width;
        Height = height;
    }

    // 正方形用コンストラクタ
    public Rectangle(double side) : this(side, side) { }

    public double Area     => Width * Height;
    public double Perimeter => 2 * (Width + Height);
}

var r1 = new Rectangle();       // 1×1
var r2 = new Rectangle(4, 3);   // 4×3
var r3 = new Rectangle(5);      // 5×5(正方形)
Console.WriteLine(r2.Area);     // 12
: this(…) による委譲
コンストラクタのオーバーロード間で処理を共有するには : this(引数) を使います。共通の初期化処理が1か所にまとまり、DRY(重複排除)の原則を守れます。詳しいコンストラクタの使い方はコンストラクタの役割と使い方を参照してください。

オブジェクト初期化子

オブジェクト初期化子(object initializer)を使うと、new の直後に { } でプロパティをまとめて設定できます。コンストラクタに引数を追加しなくても柔軟に初期化できます。

オブジェクト初期化子
class User
{
    public string Name    { get; set; } = "";
    public string Email   { get; set; } = "";
    public string Role    { get; set; } = "User";
    public bool   IsActive { get; set; } = true;
}

// オブジェクト初期化子で一括設定
var user1 = new User
{
    Name  = "Taro",
    Email = "taro@example.com",
    Role  = "Admin"
    // IsActive は省略 → デフォルト値 true が使われる
};

// C# 9以降: new() でターゲット型推論(型名を省略)
User user2 = new()
{
    Name = "Hanako",
    Email = "hanako@example.com"
};
ネストしたオブジェクトの初期化
class Address
{
    public string City    { get; set; } = "";
    public string ZipCode { get; set; } = "";
}

class Customer
{
    public string  Name    { get; set; } = "";
    public Address Address { get; set; } = new();
}

// ネストしたオブジェクトもまとめて初期化できる
var customer = new Customer
{
    Name = "Taro",
    Address = new Address
    {
        City    = "Tokyo",
        ZipCode = "100-0001"
    }
};

this キーワード

this は「現在のインスタンス自身」を指します。主に引数名とフィールド名が衝突したときの解消と、メソッドチェーンの実装に使います。

this の使い方
class Builder
{
    private string _name = "";
    private int    _value;

    // 引数名とフィールド名の衝突を解消
    public void SetName(string name)
    {
        this._name = name; // this でフィールドを明示
    }

    // メソッドチェーン(自分自身を返すビルダーパターン)
    public Builder WithName(string name)
    {
        _name = name;
        return this; // 自分自身を返す
    }

    public Builder WithValue(int value)
    {
        _value = value;
        return this;
    }

    public string Build() => $"{_name}:{_value}";
}

// メソッドチェーンで連続して設定
string result = new Builder()
    .WithName("Test")
    .WithValue(42)
    .Build();
Console.WriteLine(result); // Test:42

ToString・Equals・GetHashCode のオーバーライド

すべての C# クラスは object クラスを継承しており、ToString()Equals()GetHashCode() を持っています。カスタムクラスで適切にオーバーライドすることで、デバッグや等価比較が格段に扱いやすくなります。

ToString・Equals・GetHashCode のオーバーライド
class Point
{
    public int X { get; }
    public int Y { get; }

    public Point(int x, int y) { X = x; Y = y; }

    // デバッグやConsole.WriteLineで見やすい文字列表現を返す
    public override string ToString() => $"({X}, {Y})";

    // 値の等価性を定義(デフォルトは参照等価)
    public override bool Equals(object? obj)
    {
        if (obj is not Point other) return false;
        return X == other.X && Y == other.Y;
    }

    // Equals をオーバーライドしたら GetHashCode も必須
    public override int GetHashCode() => HashCode.Combine(X, Y);

    // == / != 演算子のオーバーロード(任意)
    public static bool operator ==(Point a, Point b) => a.Equals(b);
    public static bool operator !=(Point a, Point b) => !a.Equals(b);
}

var p1 = new Point(1, 2);
var p2 = new Point(1, 2);
var p3 = new Point(3, 4);

Console.WriteLine(p1);         // (1, 2)
Console.WriteLine(p1 == p2);   // True(値が同じ)
Console.WriteLine(p1 == p3);   // False

// Dictionary や HashSet のキーとして使えるようになる
var dict = new Dictionary<Point, string> { [p1] = "origin" };
Console.WriteLine(dict[p2]);   // "origin"(p2 は p1 と等価)
Equals をオーバーライドする場合は必ず GetHashCode も一緒にオーバーライドしてください。Equals だけ書いて GetHashCode を書かないと、Dictionary や HashSet で正しく動作しません(コンパイラ警告も出ます)。
データを保持するだけのクラスは record 型(C# 9以降)を使うと、値等価・ToString・分解(Deconstruct)が自動的に実装されます。詳しくはrecord型の基本とclass/structとの違いを参照してください。

sealed クラスと partial クラス

sealed クラス:継承を禁止する

sealed を付けたクラスは継承できません。設計上「これ以上派生クラスを作らせたくない」という意図を表明するときに使います。

sealed クラス
// 継承を禁止
sealed class DatabaseConfig
{
    public string ConnectionString { get; init; } = "";
    public int    TimeoutSeconds   { get; init; } = 30;
}

// コンパイルエラー: sealed クラスは継承できない
// class ExtendedConfig : DatabaseConfig { }

// string クラスも sealed なので継承できない
// class MyString : string { } // エラー
sealed はパフォーマンス上のメリットもあります。JIT コンパイラが仮想メソッドの呼び出しを最適化できるため、ホットパスのクラスに付けることで実行速度が向上する場合があります。

partial クラス:定義を複数ファイルに分割する

partial を付けると、1つのクラスを複数のファイルに分割して定義できます。自動生成コードと手書きコードを分離するときに有用です。

partial クラスの例
// ファイル1: User.cs(手書き部分)
public partial class User
{
    public string Name  { get; set; } = "";
    public string Email { get; set; } = "";

    public bool IsValid() => !string.IsNullOrEmpty(Name) && Email.Contains("@");
}

// ファイル2: User.Generated.cs(自動生成部分)
public partial class User
{
    // コード生成ツールが出力したコード(手書きと混ぜない)
    public Guid   Id        { get; set; } = Guid.NewGuid();
    public DateTime Created { get; set; } = DateTime.UtcNow;
}

// 使い方は通常のクラスと同じ
var u = new User { Name = "Taro", Email = "taro@example.com" };
Console.WriteLine(u.IsValid()); // true
Console.WriteLine(u.Id);        // Guid が自動付与されている

クラス設計の基本指針

指針 内容 具体例
単一責任の原則 クラスは1つのことだけを担当する UserRepository(DB操作)と UserValidator(検証)を分ける
不変(イミュータブル)を優先 変更不可なプロパティを使う { get; init; }{ get; } で外部からの書き換えを防ぐ
フィールドは private 内部データを直接公開しない フィールドは private、外部へはプロパティ経由で
バリデーションをプロパティに 不正値がフィールドに入らない設計 setter 内で例外をスローして防御
ToString をオーバーライド デバッグ時に中身が見える $"{名前}({年齢})" 形式で人間が読める文字列を返す

よくある落とし穴と注意点

Equals をオーバーライドして GetHashCode を忘れる

Equals をオーバーライドするとコンパイラが「GetHashCode もオーバーライドすることを推奨します」と警告します。両者をペアでオーバーライドしない場合、DictionaryHashSet のキーとして使ったときに「等しいはずのキー」でデータを取り出せない不具合が起きます。HashCode.Combine() を使うと簡単に実装できます。

オブジェクト初期化子は set または init がないプロパティには使えない

{ get; }(読み取り専用・コンストラクタのみで設定可)のプロパティはオブジェクト初期化子では設定できません。{ get; init; } なら初期化子で設定できます。コンパイルエラーになった場合はアクセサの種類を確認してください。

参照型フィールドの初期化し忘れ

クラスのフィールドで参照型(stringList<T> など)を宣言したとき、初期化しないと null のままです。C# 8以降で nullable 参照型を有効にすると、null になりうるフィールドを未初期化のまま使うとコンパイラ警告が出ます。フィールド宣言時に = ""= new() で必ず初期化しましょう。

フィールドを public にする

public int Count; のようにフィールドを直接公開するのは避けましょう。外部から任意の値が代入されてしまい、オブジェクトの整合性が壊れる可能性があります。代わりにプロパティを使い、setter でバリデーションを行うか private set / init で書き込みを制限します。

sealed の付け忘れによる意図しない継承

継承されることを想定していないクラスに sealed を付け忘れると、利用者が派生クラスを作って予期しない挙動が起きることがあります。特にシングルトンパターン・設定クラス・ユーティリティクラスには sealed を積極的に付けることを検討しましょう。

よくある質問

Qクラスと構造体(struct)はどう使い分けますか?
Aクラスは参照型でヒープに確保されます。構造体は値型でスタックに確保(小さいサイズ)されます。「データを保持するだけ・不変・小さい(16バイト以下が目安)」なら struct、それ以外は class を選ぶのが基本です。また C# 9以降の record は値等価を持つクラスとして使え、DTO・設定値に最適です。
Qプロパティとフィールドはどう使い分けますか?
A外部に公開するデータはプロパティ、内部でのみ使うデータはフィールド(private)にします。フィールドを直接 public にするとバリデーションができず、後からプロパティに変えるとAPIが変わってしまいます。最初からプロパティで定義する習慣を持つことを推奨します。詳しくはプロパティとフィールドの違いを参照してください。
Qコンストラクタとオブジェクト初期化子はどちらを使うべきですか?
A必須のデータ(ないとオブジェクトが意味をなさないもの)はコンストラクタで受け取ります。省略可能な追加設定はオブジェクト初期化子で行います。「必須+任意」の両方がある場合は、必須引数をコンストラクタに、任意設定をオブジェクト初期化子に分けるのがクリーンな設計です。
Qクラスに static メソッドを多く使うのは良くないですか?
A状態を持たないユーティリティ処理(変換・計算・バリデーション)は static メソッドが適しています。ただし static を多用するとテストが難しくなります(モックできない)。依存性の注入(DI)が必要な処理はインスタンスメソッドにし、インターフェースを介して差し替え可能にしましょう。詳しくはstaticメンバーの使い方と設計を参照してください。
Q継承はどんな場面で使うべきですか?
A「is-a 関係」(Dog は Animal の一種)が成り立つときに継承を使います。単に機能を追加したいだけなら継承よりコンポジション(フィールドとして持つ)やインターフェースが向いています。継承の詳細は継承とオーバーライドの基本を参照してください。

まとめ

要素 説明 ポイント
フィールド 内部データを保持する変数 原則 private。外部公開はプロパティ経由で
自動実装プロパティ { get; set; } シンプルなデータ保持に最適
フルプロパティ getter/setter を手書き バリデーション・副作用が必要な場合
計算プロパティ => 式 他フィールドから算出する値(保存不要)
コンストラクタ new 時に呼ばれる初期化 必須データの設定。: this() で委譲
オブジェクト初期化子 new C { Prop = val } 任意設定を柔軟に指定
this 自分自身のインスタンス 引数との名前衝突解消・メソッドチェーン
ToString 文字列表現を返す デバッグ用に必ずオーバーライド
Equals/GetHashCode 等価性の定義 ペアでオーバーライド。Dictionary のキーに必須
sealed 継承を禁止 意図しない派生を防ぐ・パフォーマンス向上
partial クラスを複数ファイルに分割 自動生成コードと手書きコードの分離

アクセス修飾子の詳細はカプセル化とアクセス修飾子を、継承・ポリモーフィズムは継承とオーバーライドの基本を参照してください。