プロパティ
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
プロパティを宣言することはできません。 プロパティの宣言では、protected
、internal
、protected internal
、private
を宣言することもできます。
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# の機能をどれでも自由に使用して実装できます。 検証、各種アクセシビリティ、遅延評価など、目的のシナリオで必要となる要素はすべて提供できます。
.NET
フィードバック
https://aka.ms/ContentUserFeedback」を参照してください。
以下は間もなく提供いたします。2024 年を通じて、コンテンツのフィードバック メカニズムとして GitHub の issue を段階的に廃止し、新しいフィードバック システムに置き換えます。 詳細については、「フィードバックの送信と表示