C#におけるクラスのプロパティ設計とアクセス制御の詳細解説

プロパティの基本特性

C#のクラス設計において、プロパティはデータのカプセル化と安全なインターフェース提供のための核となる機能です。フィールドと同様に直感的な代入・参照シンタックスをサポートしつつ、内部では関数メンバとして振る舞います。これにより、外部からの見た目はシンプルでありながら、内部ロジックや制約条件を完全に隠蔽することが可能です。

  • クラス内で一意な識別子を持つメンバです。
  • 明確なデータ型を定義します。
  • 外部からは値の読み取りまたは書き込みが可能な状態ですが、内部の実装は定義済みのコードによって制御されます。
  • メモリを直接確保するデータメンバとは異なり、必ずペアになったgetアクセサsetアクセサを内包しています。
  • アクセサは暗黙的に呼ばれ、プログラム上の任意の地点から直接呼び出すことは許可されていません。

アクセサ(Accessor)の仕様と動作原理

プロパティはgetsetの2つのアクセサブロックで構成されます。順序は自由ですが、両者の組み合わせ以外は許容されません。

  • setアクセサ: 外部から渡された値を受け取り、内部状態を更新します。暗黙的にvalueという名前のパラメータを持ち、その型はプロパティの型と一致します。戻り値の型はvoidです。
  • getアクセサ: 現在の内部状態の値を呼び出し元に返却します。パラメータは存在せず、暗黙的にプロパティと同じ戻り値型を持ちます。メソッドと同様、全ての制御フローで値を返すreturn文が存在する必要があります。
// 基本構造の定義例
private int _backupStorage;

public int TargetData
{
    set
    {
        // 暗黙的な value パラメータを使用して内部フィールドに反映
        _backupStorage = value;
    }
    get
    {
        // プロパティの型と同じ値を返却
        return _backupStorage;
    }
}

バッキングフィールドとの連携設計

多くの場合、プロパティは内部的にデータを保持するための非公開フィールド(バッキングフィールド、またはバックストア)とセットで設計されます。カプセル化の原則に従い、フィールドをprivateで宣言し、外部アクセスをプロパティ経由に制限するのが標準的なパターンです。

命名規約としては以下のような分け方が一般的です:

  • フィールドを接頭辞付きのキャメルケース(例: `_price`)、プロパティを PascalCase(例: `Price`)とする。
  • フィールドを小文字始まり、プロパティを大文字始まりで区別する(例: field `quantity`, property `Quantity`)。

計算と入力検証の統合

アクセサは単なるメモリ操作にとどまらず、任意の計算やバリデーションを埋め込むことができます。getアクセサは常にプロパティの型を返す必要があり、setアクセサは受け取った値に対する前処理を実行可能です。

class TemperatureSensor
{
    private double _currentReading;

    // 範囲外の数値が入力された場合、閾値までクリップする
    public double Reading
    {
        set => _currentReading = (value < -50) ? -50 : ((value > 120) ? 120 : value);
        get => _currentReading;
    }
}

上記のように式本文メンバ(`=>`)を利用すれば、簡易的な getter/setter の記述が冗長さを排して記せます。

読み取り専用と書き込み専用の制御

アクセサの宣言は片方だけでも問題ありません。両方を省略するとコンパイルエラーになります。

  • 読み取り専用(Get-only): setアクセサを定義しません。インスタンス生成後に変更不可能な不変状態を提供する際に有効です。
  • 書き込み専用(Set-only): getアクセサを定義しません。実際の開発では稀であり、データの受取だけで何らかのシステム状態を変更したい場合は、メソッドとして設計することを強く推奨します。

公開フィールド(Public Fields)との比較優位性

単純なデータの透過的保持であれば公開フィールドでも同等に見えますが、プロパティを採用する技術的な根拠は複数存在します。

  1. 後方互換性の維持: 初めはフィールドだったものを後日バリデーションや計算ロジックへ移行しても、利用側(クライアント)の呼び出しコードを一切修正する必要がありません。
  2. 粒度の細かなアクセス制御: 読み取りのみ、または書き込みのみを許可できます。
  3. 継承対応: virtualoverride キーワードに対応しており、派生クラスでの挙動上書きが可能ですが、フィールドは該当しません。
  4. フレームワークとの親和性: シリアライズライブラリ、UI バインディング、DIコンテナなどのエコシステムがプロパティのパターンを前提として最適化されています。

自動実装プロパティ(Auto-Implemented Properties)

内部フィールドへの単純な渡し替え以外にカスタムロジックが不要な場合、コンパイラに裏方のフィールド生成を任せる自動実装構文が利用できます。

class DocumentInfo
{
    // バックストアフィールドの生成をコンパイラに委譲
    public string Title { get; set; }
    public int PageCount { get; private set; } // setter を保護
    public double Score { get; }              // コンストラクタ内のみ初期化可能
}

この形式ではアクセサの本体(中括弧)を定義できず、単なる読み書きのゲートとして機能します。実装の手間とボイラープレートコードを大幅に削減できる反面、内部ロジックの注入が必要な場面では適用できません。

静的プロパティ(Static Properties)

static修飾子を付与すると、クラス自体に紐づくグローバルな状態管理プロパティとして定義できます。静的メンバーからは他の静的メンバーへアクセス可能ですが、インスタンスメンバへの直接的な参照は禁止されています。

class GlobalCounter
{
    public static int ClickCount { get; set; }

    public static void LogClick()
    {
        ClickCount++;
        // インスタンスメンバへのアクセスはコンパイルエラー発生
    }
}

// クラス外での利用方法
GlobalCounter.ClickCount = 150;
Console.WriteLine(GlobalCounter.ClickCount);

外部からは完全修飾名を用いるか、using static ネームスペース.クラス; ディレクティブで短縮表記を有効化できます。

タグ: C# プロパティ カプセル化 アクセサ C#構文

7月3日 23:52 投稿