プロパティ

C# のプロパティは、非常に優れた機能です。 開発者は C# で定義されている構文を使用して、設計の意図を正確に表すコードを記述できます。

プロパティは、アクセスされるとフィールドのように動作します。 ただし、フィールドとは異なり、プロパティの実装ではアクセサーを使用します。プロパティがアクセスされたときや値を割り当てられたときに実行されるステートメントをアクセサーで定義します。

プロパティの構文

プロパティの構文は、フィールドを自然に拡張したものです。 フィールドで格納場所を定義します。

public class Person
{
    public string FirstName;

    // Omitted for brevity.
}

プロパティの定義には、プロパティの値を取得する get アクセサーとプロパティに値を割り当てる set アクセサーの宣言が含まれます。

public class Person
{
    public string FirstName { get; set; }

    // Omitted for brevity.
}

上記の構文は "自動プロパティ" の構文です。 コンパイラによって、プロパティをバックアップするフィールドの格納場所が生成されます。 また、get アクセサーと set アクセサーの本体もコンパイラによって実装されます。

場合によっては、その型の既定以外の値にプロパティを初期化する必要があります。 C# では、プロパティの右中かっこの後で値を設定することにより可能です。 FirstName プロパティの初期値は null より空の文字列の方がよい場合があります。 その場合は次に示すように指定します。

public class Person
{
    public string FirstName { get; set; } = string.Empty;

    // Omitted for brevity.
}

この記事で後述するように、特定の初期化は読み取り専用プロパティに最も役に立ちます。

格納場所は、下に示すように、開発者が定義することもできます。

public class Person
{
    public string FirstName
    {
        get { return _firstName; }
        set { _firstName = value; }
    }
    private string _firstName;

    // Omitted for brevity.
}

プロパティの実装が 1 つの式の場合は、式形式のメンバーを get アクセス操作子または set アクセス操作子に使用できます。

public class Person
{
    public string FirstName
    {
        get => _firstName;
        set => _firstName = value;
    }
    private string _firstName;

    // Omitted for brevity.
}

この記事では、該当する箇所ではこの簡単な構文を使います。

上に示したプロパティの定義は、読み取り/書き込みプロパティです。 set アクセサーの value に注目してください。 set アクセサーには常に、value という名前のパラメーターが 1 つあります。 get アクセサーは、プロパティの型に変換可能な値を返す必要があります (この例では string)。

これが構文の基本です。 さまざまな設計手法をサポートするバリエーションが多数あります。 これらを詳しく確認しながら、各種シナリオに応じた構文の選択肢を見てみましょう。

検証

ここまでに示した例は、検証が行われない読み取り/書き込みプロパティという、プロパティ定義の中でも単純なものでした。 目的のコードを get アクセサーと set アクセサーで記述することで、さまざまなシナリオに対応できます。

set アクセサーにコードを記述すると、プロパティが表す値を常に有効な値にすることができます。 たとえば、Person クラスに対するルールの 1 つに、名前はブランクにも空白文字にもできないというものがあるとします。 これは次のように記述できます。

public class Person
{
    public string FirstName
    {
        get => _firstName;
        set
        {
            if (string.IsNullOrWhiteSpace(value))
                throw new ArgumentException("First name must not be blank");
            _firstName = value;
        }
    }
    private string _firstName;

    // Omitted for brevity.
}

上記の例は、プロパティ セッターの検証の一部として throw 式を使用して簡略化できます。

public class Person
{
    public string FirstName
    {
        get => _firstName;
        set => _firstName = (!string.IsNullOrWhiteSpace(value)) ? value : throw new ArgumentException("First name must not be blank");
    }
    private string _firstName;

    // Omitted for brevity.
}

上記の例では、名前を無指定または空白文字にしてはいけないというルールが強制的に適用されます。 もし開発者が下のように指定すると、

hero.FirstName = "";

この割り当てに対して ArgumentException がスローされます。 プロパティの set アクセサーの戻り値は void でなければならないため、例外をスローすることで set アクセサーにエラーを報告します。

この構文を拡張して、シナリオに必要なあらゆる要素に対応できます。 たとえば、各種プロパティ間の関係をチェックしたり、外部条件に対して検証したりできます。 C# で有効なステートメントは、すべてプロパティ アクセサーでも有効です。

アクセス制御

ここまでのプロパティ定義はすべて、パブリック アクセサーを持つ読み取り/書き込みプロパティでした。 これ以外にも、プロパティに有効なアクセシビリティがあります。 たとえば、読み取り専用プロパティを作成したり、set アクセサーと get アクセサーに異なるアクセシビリティを設定したりすることができます。 具体例として、Person クラスで、クラス内の他のメソッドからのみ FirstName プロパティの値を変更できるようにしたい場合は、 set アクセサーのアクセシビリティを public ではなく private に設定します。

public class Person
{
    public string FirstName { get; private set; }

    // Omitted for brevity.
}

これで、FirstName プロパティにはどのコードからもアクセスできる一方で、値の割り当ては Person クラス内の他のコードからしかできなくなります。

制限を設定するアクセス修飾子を set アクセサーと get アクセサーのどちらか 1 つに追加することもできます。 個々のアクセサーには、プロパティ定義のアクセス修飾子よりも制限が強いアクセス修飾子を設定する必要があります。 上記は、FirstName プロパティが public ですが set アクセサーが private であるため、有効です。 public アクセサーを指定して private プロパティを宣言することはできません。 プロパティの宣言では、protectedinternalprotected internalprivate を宣言することもできます。

get アクセサーに制限の高い修飾子を設定することも有効です。 たとえば、public なプロパティで、get アクセサーを private に制限できます。 ただし、このようなシナリオは実際にはほとんどありません。

読み取り専用

また、コンストラクターでのみ設定できるように、プロパティに対する変更を制限することもできます。 Person クラスを次のように変更することができます。

public class Person
{
    public Person(string firstName) => FirstName = firstName;

    public string FirstName { get; }

    // Omitted for brevity.
}

init 専用

前の例では、呼び出し元は FirstName パラメーターを含むコンストラクターを使う必要があります。 呼び出し元は、オブジェクト初期化子を使ってプロパティに値を割り当てることはできません。 初期化子をサポートするには、次のコードで示すように、set アクセサーを init アクセサーにすることができます。

public class Person
{
    public Person() { }
    public Person(string firstName) => FirstName = firstName;

    public string FirstName { get; init; }

    // Omitted for brevity.
}

前の例では、そのコードで FirstName プロパティが設定されていない場合でも、呼び出し元は既定のコンストラクターを使って Person を作成できます。 C# 11 以降では、呼び出し元にそのプロパティを設定するように "要求" できます。

public class Person
{
    public Person() { }

    [SetsRequiredMembers]
    public Person(string firstName) => FirstName = firstName;

    public required string FirstName { get; init; }

    // Omitted for brevity.
}

上のコードは、Person クラスに 2 つのものを追加しています。 1 つ目の FirstName プロパティの宣言には、required 修飾子が含まれます。 つまり、新しい Person を作成するすべてのコードで、このプロパティを設定する必要があります。 2 つ目の、firstName パラメーターを受け取るコンストラクターには、System.Diagnostics.CodeAnalysis.SetsRequiredMembersAttribute 属性があります。 この属性は、このコンストラクターが "すべての" required メンバーを設定することをコンパイラに伝えます。

重要

required と "null 非許容" を混同しないでください。 required プロパティは null または default に設定することができます。 これらの例の string のように、型が null 非許容の場合、コンパイラは警告を発行します。

呼び出し元は、次のコードに示すように、コンストラクターで SetsRequiredMembers を使うか、オブジェクト初期化子を使って FirstName プロパティを設定する必要があります。

var person = new VersionNinePoint2.Person("John");
person = new VersionNinePoint2.Person{ FirstName = "John"};
// Error CS9035: Required member `Person.FirstName` must be set:
//person = new VersionNinePoint2.Person();

計算されたプロパティ

プロパティが返す値は、メンバー フィールドの値でなくてもかまいません。 計算された値を返すプロパティを作成できます。 姓と名を連結する計算をしてフルネームを返すように Person オブジェクトを拡張してみましょう。

public class Person
{
    public string FirstName { get; set; }

    public string LastName { get; set; }

    public string FullName { get { return $"{FirstName} {LastName}"; } }
}

上の例では、"文字列補間" 機能を使用して、フルネームを表す書式設定された文字列を作成しています。

式形式のメンバーを使用することもできます。式形式のメンバーを使用すると、計算された FullName プロパティを簡潔な方法で作成できます。

public class Person
{
    public string FirstName { get; set; }

    public string LastName { get; set; }

    public string FullName => $"{FirstName} {LastName}";
}

式形式のメンバーでは、式が 1 つだけ含まれたメソッドを定義するラムダ式構文を使用します。 ここでは、その式が Person オブジェクトのフルネームを返しています。

キャッシュ済みの評価されたプロパティ

計算されたプロパティの概念をストレージと組み合わせて、キャッシュ済みの評価されたプロパティを作成できます。 たとえば、FullName プロパティを更新して、プロパティが最初にアクセスされたときに文字列が書式設定されるようにすることができます。

public class Person
{
    public string FirstName { get; set; }

    public string LastName { get; set; }

    private string _fullName;
    public string FullName
    {
        get
        {
            if (_fullName is null)
                _fullName = $"{FirstName} {LastName}";
            return _fullName;
        }
    }
}

ただし、上記のコードにはバグが含まれています。 コードによって FirstName プロパティと LastName プロパティのいずれかの値が更新されると、以前に評価された fullName フィールドは無効になります。 fullName フィールドが再計算されるように、FirstName プロパティと LastName プロパティの set アクセサーを変更します。

public class Person
{
    private string _firstName;
    public string FirstName
    {
        get => _firstName;
        set
        {
            _firstName = value;
            _fullName = null;
        }
    }

    private string _lastName;
    public string LastName
    {
        get => _lastName;
        set
        {
            _lastName = value;
            _fullName = null;
        }
    }

    private string _fullName;
    public string FullName
    {
        get
        {
            if (_fullName is null)
                _fullName = $"{FirstName} {LastName}";
            return _fullName;
        }
    }
}

上の最終版では、必要になった場合にのみ FullName プロパティが評価されます。 以前に計算されたものが有効であれば、それが使用されます。 状態が変化したことで、以前に計算されたバージョンが無効になると、再計算が行われます。 このクラスを使用するにあたって、開発者は実装の詳細を知っている必要はありません。 内部で変化があっても Person オブジェクトの使用には影響しません。 これが、プロパティを使用してオブジェクトのデータ メンバーを公開するする重要な利点です。

自動実装プロパティに属性をアタッチする

自動実装プロパティのコンパイラの生成したバッキング フィールドにフィールド属性をアタッチできるようになりました。 たとえば、一意の整数 Id プロパティを追加する Person クラスのリビジョンについて考えてみましょう。 自動実装プロパティを使用して Id プロパティを記述しますが、この設計では Id プロパティの永続化を呼び出しません。 NonSerializedAttribute は、プロパティではなく、フィールドにのみアタッチすることができます。 次の例のように、属性に対して field: 指定子を使用して Id プロパティのバッキング フィールドに NonSerializedAttribute をアタッチできます。

public class Person
{
    public string FirstName { get; set; }

    public string LastName { get; set; }

    [field:NonSerialized]
    public int Id { get; set; }

    public string FullName => $"{FirstName} {LastName}";
}

この手法は、自動実装プロパティのバッキング フィールドにアタッチする任意の属性に利用できます。

INotifyPropertyChanged を実装する

プロパティ アクセサーでコードを記述する必要があるシナリオとして、値が変更されたことをデータ バインディング クライアントに通知するための INotifyPropertyChanged インターフェイスのサポートというものもあります。 プロパティの値が変更されると、オブジェクトはその変更を示す INotifyPropertyChanged.PropertyChanged イベントを発生させます。 データ バインディング ライブラリは、その変更に基づいて表示要素を更新します。 下のコードは、この Person クラスの FirstName プロパティに INotifyPropertyChanged を実装する方法を示しています。

public class Person : INotifyPropertyChanged
{
    public string FirstName
    {
        get => _firstName;
        set
        {
            if (string.IsNullOrWhiteSpace(value))
                throw new ArgumentException("First name must not be blank");
            if (value != _firstName)
            {
                _firstName = value;
                PropertyChanged?.Invoke(this,
                    new PropertyChangedEventArgs(nameof(FirstName)));
            }
        }
    }
    private string _firstName;

    public event PropertyChangedEventHandler PropertyChanged;
}

?. は "null 条件演算子" と呼ばれる演算子です。 演算子の右側を評価する前に、null 参照がないかをチェックします。 チェックの結果、PropertyChanged イベントに対するサブスクライバーがない場合は、イベントを発生させるコードは実行されません。 その場合、評価は行われず NullReferenceException がスローされます。 詳細については、「events」を参照してください。 上記の例では、新たに nameof という演算子を使用して、記号としてのプロパティ名を文字列に変換しています。 nameof を使用すると、プロパティ名にタイプミスが含まれるというエラーを減らすことができます。

INotifyPropertyChanged の実装も、アクセサーでコードを記述することで目的のシナリオをサポートできるケースの一例です。

要約

プロパティは、クラスまたはオブジェクトに含まれた一種のスマート フィールドです。 オブジェクトの外部からは、オブジェクト内にあるフィールドのように見えます。 一方、プロパティは、C# の機能をどれでも自由に使用して実装できます。 検証、各種アクセシビリティ、遅延評価など、目的のシナリオで必要となる要素はすべて提供できます。