C#におけるプロパティは、オブジェクト指向プログラミングの核心である「カプセル化」を実現するための極めて重要な機能です。

クラスの内部状態(フィールド)へのアクセスを制御し、安全かつ柔軟なデータの読み書きを可能にします。

単なる変数の公開とは異なり、値の妥当性検証や計算、変更通知などのロジックを介入させることができるため、堅牢なアプリケーション開発には欠かせません。

C#の進化とともに、プロパティの記述方法は劇的に簡略化されてきました。

かつては冗長な記述が必要でしたが、現在は「自動実装プロパティ」や「init専用セッター」、さらには「requiredプロパティ」といった最新機能により、簡潔さと安全性を両立したコードを書くことが可能です。

本記事では、C#プロパティの基礎から応用、そして最新の記法までを網羅的に解説します。

C#プロパティの基本概念

C#のプロパティは、外部からは「変数(フィールド)」のように見えますが、内部的には「メソッド」として振る舞う特殊なメンバーです。

これを理解するために、まずはプロパティが解決する課題と、最も基本的な書き方を確認しましょう。

プロパティが必要な理由

クラス内のデータを外部に公開する際、フィールドを直接 public にすることは推奨されません。

なぜなら、外部から予期せぬ値を代入されたり、内部構造を変更した際に外部コードに影響が出たりするからです。

プロパティを使用することで、以下のメリットが得られます。

  • アクセスの制御:読み取り専用や書き込み専用の設定が可能。
  • バリデーション:代入される値が適切かどうかをチェックできる。
  • 抽象化:内部の保持形式(フィールド)と外部への見せ方を変えることができる。

基本的なプロパティの書き方(フルプロパティ)

最も伝統的なプロパティの書き方は、「バッキングフィールド(裏打ちフィールド)」と呼ばれるプライベート変数とセットで記述する方法です。

C#
using System;

namespace PropertySample
{
    public class Person
    {
        // プライベートなバッキングフィールド
        private string _name;

        // プロパティの定義
        public string Name
        {
            // 取得時の処理
            get
            {
                return _name;
            }
            // 設定時の処理
            set
            {
                // valueキーワードは代入される値を指す
                if (!string.IsNullOrEmpty(value))
                {
                    _name = value;
                }
                else
                {
                    Console.WriteLine("名前を空にすることはできません。");
                }
            }
        }
    }

    class Program
    {
        static void Main()
        {
            Person p = new Person();
            p.Name = "田中太郎"; // setアクセサーが呼ばれる
            Console.WriteLine(p.Name); // getアクセサーが呼ばれる

            p.Name = ""; // バリデーションに引っかかる
        }
    }
}
実行結果
田中太郎
名前を空にすることはできません。

この例では、set アクセサー内で value キーワードを使用して、渡された値のチェックを行っています。

これがプロパティによるデータの保護の基本です。

自動実装プロパティとその進化

ロジックを必要とせず、単に値を保持するだけのプロパティに対して、毎回バッキングフィールドを手動で定義するのは面倒です。

そこで導入されたのが「自動実装プロパティ(Auto-Implemented Properties)」です。

自動実装プロパティの基本

自動実装プロパティを使用すると、コンパイラが自動的に隠れたバッキングフィールドを作成してくれます。

C#
public class Product
{
    // 自動実装プロパティ
    public string ProductName { get; set; }
    
    public decimal Price { get; set; }
}

非常に簡潔ですが、これだけで getset の機能が備わっています。

初期値の設定

自動実装プロパティには、宣言と同時に初期値を代入することが可能です。

C#
public class User
{
    // プロパティに初期値を設定
    public string Role { get; set; } = "Guest";
    
    public DateTime CreatedAt { get; } = DateTime.Now;
}

getのみのプロパティ(読み取り専用)

set を記述しないことで、読み取り専用プロパティを作成できます。

この場合、値の設定は「宣言時の初期化」または「コンストラクタ」でのみ可能です。

C#
public class Point
{
    public int X { get; }
    public int Y { get; }

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

これにより、オブジェクトの生成後に状態が変わらない「不変(Immutable)」な設計が容易になります。

アクセス修飾子による制御とinit専用セッター

プロパティの読み取りと書き込みに対して、異なるアクセス権限を設定したい場合があります。

アクセス修飾子の個別設定

例えば、「外部からは読めるが、書き込みはクラス内部だけに制限したい」という場合は、private set を使用します。

C#
public class BankAccount
{
    // 外部からは読み取れるが、変更はクラス内からのみ
    public decimal Balance { get; private set; }

    public void Deposit(decimal amount)
    {
        if (amount > 0) Balance += amount;
    }
}

init専用セッター(C# 9.0以降)

private set は便利ですが、オブジェクト初期化子(Object Initializers)による値の設定ができないという欠点がありました。

これを解決するのが 「init専用セッター」 です。

init を使用すると、「オブジェクト作成時のみ値を設定でき、その後は変更不可」という挙動を実現できます。

機能コンストラクタでの設定オブジェクト初期化子作成後の変更
set
private set××(内部のみ〇)
init×
C#
public class Book
{
    public string Title { get; init; }
    public string Isbn { get; init; }
}

// 使用例
var book = new Book 
{ 
    Title = "C#実践ガイド", 
    Isbn = "123-456789" 
};

// book.Title = "新しいタイトル"; // コンパイルエラー:変更不可

式形式のメンバー(Expression-bodied members)

C# 6.0および7.0以降、プロパティの記述をさらに短縮できる「式形式」の構文が導入されました。

特に、他のプロパティから計算される「計算済みプロパティ」の実装に非常に便利です。

読み取り専用プロパティの短縮

get { return ...; }=> (ラムダ演算子)で書き換えることができます。

C#
public class Circle
{
    public double Radius { get; set; }
    
    // 式形式のプロパティ(読み取り専用)
    public double Area => Math.PI * Radius * Radius;
    
    // 上記はこれと同じ意味
    // public double Area { get { return Math.PI * Radius * Radius; } }
}

get/set 両方の式形式

C# 7.0からは、getset の両方を式形式で記述できるようになりました。

C#
private string _category;
public string Category
{
    get => _category;
    set => _category = value.Trim();
}

最新機能:requiredプロパティ(C# 11以降)

C# 11で導入された 「required修飾子」 は、プロパティの設計に大きな変化をもたらしました。

これまで、プロパティの値を必須にしたい場合はコンストラクタで引数を強制する必要がありました。

しかし、プロパティの数が増えるとコンストラクタの引数も増え、コードの可読性が低下します。

required を使用すると、「オブジェクト初期化子で必ず値を指定しなければならない」という制約をコンパイル時に課すことができます。

C#
public class Employee
{
    // 必須プロパティ
    public required string Id { get; init; }
    
    public required string Name { get; set; }
    
    public string? Department { get; set; } // 任意
}

// OK
var emp1 = new Employee { Id = "E001", Name = "佐藤" };

// NG: コンパイルエラー(IdとNameが指定されていない)
// var emp2 = new Employee();

この機能により、コンストラクタを大量にオーバーロードすることなく、安全にオブジェクトを生成できるようになりました。

実践的な活用シーンとテクニック

プロパティは単なるデータの保持場所ではありません。

実務でよく使われる高度な活用方法を紹介します。

1. 変更通知の実装(MVVMパターンなど)

デスクトップアプリ(WPF/WinUI)やデータバインディングを利用するライブラリでは、プロパティの値が変わったことを外部に知らせる必要があります。

C#
using System.ComponentModel;
using System.Runtime.CompilerServices;

public class ViewModel : INotifyPropertyChanged
{
    private string _status;
    public string Status
    {
        get => _status;
        set
        {
            if (_status != value)
            {
                _status = value;
                OnPropertyChanged(); // 値が変わったことを通知
            }
        }
    }

    public event PropertyChangedEventHandler? PropertyChanged;
    protected void OnPropertyChanged([CallerMemberName] string? name = null)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
    }
}

2. 計算済みプロパティと依存関係

複数のプロパティを組み合わせて新しい値を生成する場合です。

C#
public class Rectangle
{
    public double Width { get; set; }
    public double Height { get; set; }
    
    // WidthやHeightが変われば、周の長さも変わる
    public double Perimeter => (Width + Height) * 2;
}

3. デバッグ時の利便性

プロパティにブレークポイントを設定することで、「いつ、どこで、誰がこの値を変更したのか」を容易に追跡できます。

これはパブリックフィールドでは不可能な、プロパティならではの大きなメリットです。

プロパティ設計のベストプラクティス

プロパティを効果的に使用するためのガイドラインをまとめます。

命名規則

  • プロパティ名は PascalCase(大文字で始まる)を使用します。
  • 名詞、または形容詞的な名前を付けます(例:IsVisible, Count, SelectedUserName)。
  • バッキングフィールドは _camelCase(アンダースコアで始まる小文字開始)で命名するのが一般的です。

負荷の高い処理を避ける

プロパティは外部から見ると「変数の参照」に見えます。

そのため、利用者はプロパティへのアクセスが高速であることを期待します。

  • データベースへの問い合わせや、重い計算処理を get 内で行うべきではありません。
  • 時間がかかる処理は、メソッド(例:CalculateStatisticsAsync())として定義するのが適切です。

適切なアクセシビリティの選択

デフォルトでは { get; set; } を使いがちですが、不必要な書き込み権限を与えないように意識しましょう。

  • 変更の必要がないなら { get; init; }
  • クラス内部でのみ変更するなら { get; private set; }

まとめ

C#のプロパティは、データの安全性を守るための「カプセル化」の道具から、簡潔なコードを記述するための「構文糖衣」へと進化を遂げてきました。

  • 基本:バッキングフィールドを用いたフルプロパティで詳細な制御が可能。
  • 効率:自動実装プロパティや式形式のメンバーにより、記述量を最小化できる。
  • 安全性init アクセサーや required 修飾子により、バグの少ない不変オブジェクトや確実な初期化を実現。

現代のC#開発においては、「できるだけ制限を強く(読み取り専用に近く)作り、必要な部分だけを公開する」という設計思想が主流です。

今回解説した様々な記法を適切に使い分けることで、メンテナンス性が高く、意図が明確なコードを記述できるようになります。

プロパティの挙動を深く理解することは、C#マスターへの第一歩です。

日々のコーディングの中で、最適なプロパティの形式はどれかを常に意識してみてください。