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(引数) を使います。共通の初期化処理が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 は「現在のインスタンス自身」を指します。主に引数名とフィールド名が衝突したときの解消と、メソッドチェーンの実装に使います。
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() を持っています。カスタムクラスで適切にオーバーライドすることで、デバッグや等価比較が格段に扱いやすくなります。
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 で正しく動作しません(コンパイラ警告も出ます)。ToString・分解(Deconstruct)が自動的に実装されます。詳しくはrecord型の基本とclass/structとの違いを参照してください。sealed クラスと partial クラス
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つのクラスを複数のファイルに分割して定義できます。自動生成コードと手書きコードを分離するときに有用です。
// ファイル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 もオーバーライドすることを推奨します」と警告します。両者をペアでオーバーライドしない場合、Dictionary や HashSet のキーとして使ったときに「等しいはずのキー」でデータを取り出せない不具合が起きます。HashCode.Combine() を使うと簡単に実装できます。
オブジェクト初期化子は set または init がないプロパティには使えない
{ get; }(読み取り専用・コンストラクタのみで設定可)のプロパティはオブジェクト初期化子では設定できません。{ get; init; } なら初期化子で設定できます。コンパイルエラーになった場合はアクセサの種類を確認してください。
参照型フィールドの初期化し忘れ
クラスのフィールドで参照型(string・List<T> など)を宣言したとき、初期化しないと null のままです。C# 8以降で nullable 参照型を有効にすると、null になりうるフィールドを未初期化のまま使うとコンパイラ警告が出ます。フィールド宣言時に = "" や = new() で必ず初期化しましょう。
フィールドを public にする
public int Count; のようにフィールドを直接公開するのは避けましょう。外部から任意の値が代入されてしまい、オブジェクトの整合性が壊れる可能性があります。代わりにプロパティを使い、setter でバリデーションを行うか private set / init で書き込みを制限します。
sealed の付け忘れによる意図しない継承
継承されることを想定していないクラスに sealed を付け忘れると、利用者が派生クラスを作って予期しない挙動が起きることがあります。特にシングルトンパターン・設定クラス・ユーティリティクラスには sealed を積極的に付けることを検討しましょう。
よくある質問
record は値等価を持つクラスとして使え、DTO・設定値に最適です。まとめ
| 要素 | 説明 | ポイント |
|---|---|---|
| フィールド | 内部データを保持する変数 | 原則 private。外部公開はプロパティ経由で |
| 自動実装プロパティ | { get; set; } |
シンプルなデータ保持に最適 |
| フルプロパティ | getter/setter を手書き | バリデーション・副作用が必要な場合 |
| 計算プロパティ | => 式 |
他フィールドから算出する値(保存不要) |
| コンストラクタ | new 時に呼ばれる初期化 |
必須データの設定。: this() で委譲 |
| オブジェクト初期化子 | new C { Prop = val } |
任意設定を柔軟に指定 |
| this | 自分自身のインスタンス | 引数との名前衝突解消・メソッドチェーン |
| ToString | 文字列表現を返す | デバッグ用に必ずオーバーライド |
| Equals/GetHashCode | 等価性の定義 | ペアでオーバーライド。Dictionary のキーに必須 |
| sealed | 継承を禁止 | 意図しない派生を防ぐ・パフォーマンス向上 |
| partial | クラスを複数ファイルに分割 | 自動生成コードと手書きコードの分離 |
アクセス修飾子の詳細はカプセル化とアクセス修飾子を、継承・ポリモーフィズムは継承とオーバーライドの基本を参照してください。