C#は、Microsoftが開発した強力なオブジェクト指向プログラミング言語であり、その中心的な役割を担うのがクラスです。

現代のソフトウェア開発において、大規模なシステムを効率よく、かつ保守性の高い形で構築するためには、クラスの概念を正しく理解し、適切に使いこなすことが欠かせません。

クラスは単なるデータの集まりではなく、データとその操作(振る舞い)を一つにまとめた「設計図」のような存在です。

本記事では、C#におけるクラスの基礎から、継承やポリモーフィズムといった高度な概念、さらにはC#の最新バージョンで導入されたプライマリコンストラクタレコード型といったモダンな書き方まで、実例を交えて詳しく解説します。

これからC#を学ぶ初心者の方から、より洗練されたコードを書きたい中級者の方まで、幅広く役立つ内容を目指します。

C#におけるクラスとは何か

C#におけるクラスは、オブジェクト指向プログラミング(OOP)の根幹を成す要素です。

よく例えられるのは「設計図と実体(インスタンス)」の関係です。

例えば、「車」というクラス(設計図)があれば、そこから「赤いスポーツカー」や「青いセダン」といった具体的なオブジェクト(実体)を生成することができます。

クラスの基本構成要素

クラスは主に以下の要素で構成されます。

  1. フィールド:クラスが持つデータ(変数)を保持します。
  2. プロパティ:フィールドへのアクセスを制御し、安全にデータを読み書きするための仕組みです。
  3. メソッド:クラスが行う動作や処理を定義します。
  4. コンストラクタ:インスタンスが生成される際に実行される初期化処理です。

クラスを定義することで、関連するデータと処理を一つの単位としてカプセル化し、コードの再利用性や可読性を飛躍的に高めることが可能になります。

クラスの定義とインスタンス化

まずは、最も基本的なクラスの定義方法と、そのクラスからオブジェクトを作成する(インスタンス化する)方法を見ていきましょう。

基本的なクラスの構文

C#でクラスを定義するには class キーワードを使用します。

以下のコードは、名前と年齢を持つシンプルな Person クラスの例です。

C#
using System;

namespace ClassExample
{
    // クラスの定義
    public class Person
    {
        // フィールド
        private string name;
        private int age;

        // コンストラクタ
        public Person(string name, int age)
        {
            this.name = name;
            this.age = age;
        }

        // メソッド
        public void Introduce()
        {
            Console.WriteLine($"こんにちは、私の名前は {name} で、{age} 歳です。");
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            // クラスのインスタンス化
            Person person1 = new Person("田中太郎", 25);
            
            // メソッドの呼び出し
            person1.Introduce();
        }
    }
}
実行結果
こんにちは、私の名前は 田中太郎 で、25 歳です。

インスタンス化の仕組み

上記の例では、new Person("田中太郎", 25) という記述によって、メモリ上に Person クラスの実体(インスタンス)が作成されます。

C#においてクラスは参照型であり、変数 person1 にはメモリ上のアドレス(参照)が格納されます。

アクセス修飾子によるカプセル化

クラスを設計する上で非常に重要なのが、アクセス修飾子です。

これにより、クラスの外部からどの範囲までアクセスを許可するかを細かく制御できます。

主要なアクセス修飾子一覧

修飾子説明
publicどこからでもアクセス可能。
private同じクラス内からのみアクセス可能(デフォルト)。
protected同じクラス内、および派生クラス(継承先)からアクセス可能。
internal同じアセンブリ(プロジェクト)内からアクセス可能。
protected internal同じアセンブリ内、または他アセンブリの派生クラスからアクセス可能。
private protected同じアセンブリ内の派生クラスからのみアクセス可能。

適切なアクセス制限を設けることで、クラス内部の複雑なロジックを隠蔽し、外部からの不正な書き換えを防ぐことができます。

これをカプセル化と呼びます。

プロパティの活用

フィールドを直接 public にすることは、データの整合性を損なう恐れがあるため推奨されません。

代わりに、プロパティを使用してアクセスを制御します。

自動実装プロパティと値の検証

現代のC#では、自動実装プロパティを使うことで簡潔に記述できます。

また、値を設定する際に条件チェックを行うことも可能です。

C#
public class BankAccount
{
    // 自動実装プロパティ(読み取りは公開、書き込みは非公開など柔軟に設定可能)
    public string Owner { get; private set; }

    private decimal balance;
    public decimal Balance
    {
        get { return balance; }
        set
        {
            if (value < 0)
            {
                throw new ArgumentException("残高を負の値にすることはできません。");
            }
            balance = value;
        }
    }

    public BankAccount(string owner, decimal initialBalance)
    {
        Owner = owner;
        Balance = initialBalance;
    }
}

このように、プロパティを介することで「残高がマイナスにならないようにする」といったビジネスロジックの保護が可能になります。

モダンなC#の書き方:プライマリコンストラクタ

C# 12から導入されたプライマリコンストラクタは、クラス定義を劇的に簡潔にします。

クラス名の直後に引数を定義することで、フィールドへの代入などの定型的なコードを削減できます。

C#
// 従来の書き方
public class UserOld
{
    private readonly string _name;
    public UserOld(string name) => _name = name;
    public string Name => _name;
}

// C# 12以降のプライマリコンストラクタ
public class UserModern(string name)
{
    public string Name => name;
}

この記法は特に、依存性の注入(DI)を利用する場合や、シンプルなデータ保持クラスを作成する際に非常に強力です。

クラスの継承とポリモーフィズム

オブジェクト指向の大きな特徴の一つが、既存のクラスを拡張する継承です。

継承の基本

継承を利用することで、共通の機能を親クラス(基底クラス)にまとめ、差異のある部分だけを子クラス(派生クラス)で実装できます。

C#
// 親クラス
public class Animal
{
    public string Name { get; set; }
    
    // 派生クラスで上書きを許可するメソッド
    public virtual void MakeSound()
    {
        Console.WriteLine("動物が鳴いています。");
    }
}

// 子クラス
public class Dog : Animal
{
    // メソッドのオーバーライド
    public override void MakeSound()
    {
        Console.WriteLine($"{Name}が「ワンワン!」と鳴いています。");
    }
}

抽象クラス(abstract)とインターフェース(interface)

より厳格な設計を行うために、抽象クラスインターフェースが使用されます。

  • 抽象クラス:それ自体はインスタンス化できず、派生クラスに実装を強制する「共通の枠組み」です。
  • インターフェース:クラスが持つべき「能力(メソッドの署名など)」を定義します。複数のインターフェースを実装することが可能です。

「is-a」関係には継承、「can-do」関係にはインターフェースを使うのが一般的な設計指針です。

レコード型(record class)による不変性の実現

C# 9以降、データの保持に特化したレコード型(record class)が導入されました。

これは、不変(Immutable)なオブジェクトを簡単に作成するための仕組みです。

C#
// 1行で定義可能
public record class Product(string Name, decimal Price);

class Program
{
    static void Main()
    {
        var p1 = new Product("Laptop", 150000);
        var p2 = new Product("Laptop", 150000);

        // 値の比較(通常のクラスとは異なり、中身が同じならTrue)
        Console.WriteLine(p1 == p2); 

        // 非破壊的書き換え(with式)
        var p3 = p1 with { Price = 120000 };
        Console.WriteLine(p3);
    }
}
実行結果
True
Product { Name = Laptop, Price = 120000 }

レコード型は、マルチスレッド環境やドメイン駆動設計(DDD)における「値オブジェクト」の実装において、バグを減らし安全なコードを書くために極めて重要です。

クラスと構造体(struct)の使い分け

C#にはクラスに似たものとして「構造体(struct)」があります。

この2つの使い分けは、パフォーマンスを最適化する上で避けて通れません。

特徴クラス (class)構造体 (struct)
型の種類参照型値型
メモリ配置ヒープ領域スタック領域(または親オブジェクト内)
既定のコピー参照のコピー値の完全コピー
継承可能不可(インターフェース実装は可)
推奨用途複雑なロジックや大きなデータ小さな数値データ、座標、色など

「基本的にはクラスを使い、サイズが小さく(16~32バイト以下)、不変で、頻繁に生成・破棄される場合に限り構造体を検討する」のがベストプラクティスです。

静的クラス(static class)と拡張メソッド

インスタンスを生成せずに利用する機能を提供したい場合は、静的クラスを使用します。

静的クラスの用途

静的クラスは、数学関数やユーティリティ関数(共通処理)をまとめるのに適しています。

C#
public static class MathHelper
{
    public static double CalculateCircleArea(double radius)
    {
        return Math.PI * radius * radius;
    }
}

拡張メソッド

また、既存のクラスに後付けでメソッドを追加できる拡張メソッドも静的クラスを利用して実現します。

C#
public static class StringExtensions
{
    // string型に「IsEmail」というメソッドを追加する例
    public static bool IsEmail(this string str)
    {
        return str.Contains("@") && str.Contains(".");
    }
}

// 使い方
string myEmail = "test@example.com";
bool isValid = myEmail.IsEmail(); // あたかもstringの標準メソッドのように呼べる

拡張メソッドを活用することで、コードの可読性を高め、Utility.Process(data) のような書き方を data.Process() という直感的な記述に変換できます。

ジェネリッククラスによる汎用化

特定の型に依存しないクラスを作成するには、ジェネリクスを使用します。

これにより、型安全性を保ちながらコードを再利用できます。

C#
public class DataStore<T>
{
    private T _data;
    public void SetData(T value) => _data = value;
    public T GetData() => _data;
}

// 利用例
var intStore = new DataStore<int>();
intStore.SetData(100);

var strStore = new DataStore<string>();
strStore.SetData("Hello");

標準ライブラリの List<T>Dictionary<TKey, TValue> は、このジェネリッククラスの代表例です。

クラス設計の原則:SOLID

優れたクラスを設計するためには、単に文法を知るだけでなく、SOLID原則を意識することが推奨されます。

  1. 単一責任の原則 (SRP): 一つのクラスは一つの役割だけを持つべき。
  2. 開放閉鎖の原則 (OCP): 拡張に対しては開いており、修正に対しては閉じているべき。
  3. リスコフの置換原則 (LSP): 子クラスは親クラスと置き換え可能でなければならない。
  4. インターフェース分離の原則 (ISP): 巨大なインターフェースより、小さなインターフェースを複数作るべき。
  5. 依存関係逆転の原則 (DIP): 具象ではなく抽象(インターフェース)に依存すべき。

これらの原則を守ることで、変更に強く、テストがしやすい高品質なC#プログラムを構築できます。

クラスのメモリ管理とガベージコレクション

C#におけるクラスのインスタンスは、メモリのヒープ領域に確保されます。

開発者が明示的にメモリを解放する必要はありません。

これはガベージコレクション (GC)という仕組みが自動的に不要になったオブジェクトを回収してくれるからです。

ただし、ファイルハンドルやデータベース接続といった「アンマネージドリソース」を扱う場合は注意が必要です。

その際は、IDisposable インターフェースを実装し、using 文(または using 宣言)を使用して確実にリソースを解放するのが通例です。

C#
public class FileManager : IDisposable
{
    private System.IO.StreamWriter _writer;

    public FileManager(string path)
    {
        _writer = new System.IO.StreamWriter(path);
    }

    public void Dispose()
    {
        _writer?.Dispose();
        Console.WriteLine("リソースが解放されました。");
    }
}

// 利用方法
using (var fm = new FileManager("log.txt"))
{
    // 書き込み処理など
} // ここを抜けると自動的にDisposeが呼ばれる

まとめ

C#のクラスは、単純なデータの入れ物から、高度なビジネスロジックをカプセル化する器へと進化を続けています。

本記事では、基本となる定義方法やアクセス修飾子から始まり、継承、ポリモーフィズム、そして最新のプライマリコンストラクタやレコード型までを網羅的に解説しました。

重要なポイントを振り返ると以下の通りです。

  • クラスは参照型であり、オブジェクトの「設計図」として機能する。
  • プロパティアクセス修飾子を使い、安全にデータを管理する(カプセル化)。
  • モダンなC#では、プライマリコンストラクタレコード型を活用して記述を簡略化する。
  • 継承やインターフェース、SOLID原則を意識することで、柔軟で拡張性の高い設計が可能になる。

クラスの仕組みを深く理解することは、C#マスターへの第一歩です。

日々のコーディングにおいて、どの機能が最適かを常に考え、よりクリーンで効率的なコードを目指してください。