ここでは.NET Frameworkにおけるプロパティと、プロパティの実装に関する事項・注意点などについて解説します。 またインデクサやインデックス付きプロパティについても解説します。
プロパティ
C#やVBではプロパティ構文がサポートされています。 プロパティはクラス・構造体・インターフェイスに持たせることができます。 見かけ上はプロパティに対する値の取得・設定はフィールドに対するものと変わりありません。
プロパティではこのようにアクセサメソッドを使ってフィールドに対する値の取得・設定を行います。
実装を持たないインターフェイスでは、プロパティを以下のように記述します。
アクセサメソッド
.NET Frameworkにおけるプロパティは、set
アクセサまたは/およびget
アクセサのアクセサメソッドの組み合わせとなっています。 プロパティを記述するコードはコンパイル時にアクセサメソッドとして展開され、またプロパティに対するアクセスはアクセサメソッドの呼び出しに展開されます。
単にプロパティの値を取得・設定するといった操作であればアクセサメソッドの存在を意識する必要はありませんが、次の例のようにプロパティを持つ型をリフレクションによって調べると、プロパティとなるメンバとは別にアクセサメソッドが存在していることが確認できます。
この結果にあるget_ID
がプロパティID
に対応するget
アクセサ、set_ID
がset
アクセサとなります。
このようにして作成されるアクセサメソッドを直接呼び出すことはできません。 そのようなコードを記述した場合はコンパイルエラーとなります。
また、アクセサメソッドと同じシグネチャのメソッドが存在する場合もコンパイルエラーとなります。 逆に、シグネチャが異なっていればアクセサメソッドと同名のメソッドを作成することはできます。
読み取り専用・書き込み専用・アクセシビリティ
.NET Frameworkにおけるプロパティでは、プロパティへのアクセスを読み取り専用(または書き込み専用)にすることができます。
派生クラスのみに公開する場合などを除けば、書き込み専用プロパティを外部に公開することはまれです。 こういった場合はプロパティではなくSetXXX
といったメソッドを提供するほうが自然です。
また、アクセサメソッドのアクセシビリティもset
とget
で異なるものを指定することができるため、例えば派生クラスからのみ設定可能なプロパティを作成するといったことができます。
自動実装
C#やVBでは構文によるサポートによってプロパティの自動実装を行うことができます。 これは、プロパティのアクセサ部分で行う処理が単純に値を返すだけ/設定するだけとなる場合に、その記述を省略することができるものです。
プロパティの自動実装はC# 3.0、VB2010以降でサポートされています。
バッキングフィールド
プロパティの自動実装を行う場合、プロパティの値を保持するフィールド(バッキングフィールド)も自動的に作成されます。 このフィールドはリフレクションによって調べることができます。
このようにC#とVBでは生成されるバッキングフィールド名が異なり、C#では<プロパティ名>k__BackingField
、VBでは_プロパティ名
となるようです。
自動実装プロパティの初期値
C# 6.0以降、VB2010以降では自動実装プロパティに初期値を与えることができます。
C#では読み取り専用かつ初期値を持たせたプロパティを自動実装することができますが、VBではできません。
読み取り専用かつ初期値を持たせたプロパティの自動実装は、インスタンス作成時に値を指定して以降は一切値を変更できないような不変オブジェクトを作成する上で非常に役立ちます。
このようなプロパティの自動実装を使えない場合、バッキングフィールド・アクセサメソッド・コンストラクタでの初期値の設定などをすべて記述することで不変オブジェクトを構築することができます。
インデクサ
インデクサ(indexer)とは添字(index)をつけることができるプロパティで、添字を使ってインスタンスを配列のように扱えるようにするものです。 プロパティ名を省略して直接インスタンスに添字を指定して値の取得/設定を行うように見えるため、VBでは既定のプロパティとも呼ばれます。 インデクサはstring型やList, Dictionaryなどのコレクションクラスで使われています。
配列とは異なり、インデクサの添字部分には整数型以外の型も指定することができます。 例えばDictionary<TKey, TValue>では任意の型を添字(キー)として使用することができ、インデクサではこのキーを指定することによって対応する値にアクセスすることが出来るようになっています。
実装上はインデクサもプロパティの1形態となっています。 実際、リフレクションでもインデクサはPropertyInfoとして扱われます。 (リフレクション §.PropertyInfoを使ったプロパティ・インデクサの操作)
型にインデクサを実装する場合は、次のようにします。
インデックス付きプロパティとインデクサの名前
VBではプロパティに添字をもたせることでインデックス付きプロパティを作成することができます。 また、Default
修飾子によってインデックス付きプロパティを既定のプロパティとすることによって、プロパティをインデクサとすることができます。
一方C#ではインデックス付きプロパティを作ることはできません。 そのため、かわりに配列やIList<T>などのコレクションを返すプロパティとして実装する必要があります。 また、任意の名前でインデクサを作成することもできず、型で定義できるインデクサの数もひとつに限られます。
C#ではインデクサに名前を指定できないため、C#で作成したインデクサを他の言語からインデックス付きプロパティとして参照する場合はデフォルトの名前であるItem
を使用します。 この名前を変更するには、インデクサに属性IndexerNameAttributeを指定します。
インデクサが他の言語からアクセスされることを考慮する場合、インデクサには適切な名前を付けておくことが推奨されます。 (言語間の相互運用性と共通言語仕様 (CLS))
コレクションを返すプロパティ
インデクサはコレクションやそれに類する機能を持つクラスで実装すべきもので、多くの場合はインデクサよりも単に配列やList<T>などのコレクション、IList<T>やICollection<T>などのインターフェイスを返すプロパティを用意するほうが適切です。 特にList<T>やIList<T>を返すプロパティはインデックス付きプロパティの代替として使用することができます。
インデックス付きプロパティのような機能をコレクションを返すプロパティとして公開する場合は、値を格納するコレクション自体を変更されないように読み取り専用で公開します。
さらに、公開されるコレクション自体も参照専用としたい(コレクションの内容を変更させたくない)場合は、IReadOnlyListインターフェイス(.NET Framework 4.5以降)やReadOnlyCollectionとして公開する方法をとることができます。
その他コレクションクラスおよびインターフェイスについてはコレクションの種類と特徴、読み取り専用コレクションについては汎用ジェネリックコレクション(1) Collection/ReadOnlyCollection §.ReadOnlyCollectionを参照してください。
イテレータ
プロパティにおいてもイテレータ構文を使用することができます。 これにより、IEnumerableを返すプロパティを簡単に記述することが出来ます。
イテレータに関してはイテレータを参照してください。
プロパティと例外
プロパティではアクセサメソッドを使ってフィールドの値を取得・設定するため、その際に値の検証を行う処理を記述することができます。 また、検証した結果として例外をスローすることもできます。 例えば、設定される値がプロパティとして有効な値の範囲外だった場合にはArgumentOutOfRangeException、null
/Nothing
を許容しない場合にはArgumentNullException、その他不正な値であればArgumentExceptionをスローすることができます。
これらの例外をスローする場合は、例外コンストラクタの引数で例外メッセージを記述するとともに、引数paramNameに原因となったプロパティの名前を設定します。 また、ArgumentOutOfRangeExceptionでは引数actualValueに原因となった値を指定することができ、これによりエラー原因が把握しやすくなります。
一方この例の場合では、例外をスローせず、次のように値を適正な範囲に丸め込む実装とすることも考えられます。
一般に、プロパティでは単に値の取得・設定のみを行うべきで、それ以上の副作用が起こることは避けるべきです。 例えば上記の例においては、設定した値とその後に取得される値が異なることから、実装を知らずに結果だけを見ると意図した動作と異なるような違和感を覚える場合もあります。 この他にも、プロパティを設定することがインスタンス内の他のメンバに影響するような実装(一つのプロパティで複数のフィールドを変更するなど)は避けるべきです。
また例外に関しても、プロパティから以下に挙げるようなもの以外の例外をスローする場合にはメソッドとして実装したほうがよいとされます。 プロパティからスローされることが想定(あるいは許容)される例外と状況の主なものとしては次のようなものがあります。
- ArgumentOutOfRangeException, ArgumentNullException, ArgumentException
- プロパティに設定される値としては不正な場合
- InvalidEnumArgumentException
- プロパティに設定される列挙体の値が不正な場合
- IndexOutOfRangeException
- インデクサに指定されるインデックスが範囲内の場合の場合
- InvalidOperationException
- 現在のインスタンスの状態ではプロパティの表す機能を要求できない場合 (例えば、処理の進行中にその処理に影響するプロパティを変更しようとするなど)
- ObjectDisposedException
- インスタンスが破棄された後にプロパティにアクセスしようとした場合 (オブジェクトの破棄 §.解放されたリソースへのアクセス拒否 (ObjectDisposedException))
- NotSupportedException
- インスタンスがプロパティの表す機能をサポートしていない場合 (例えば、読み取り専用として作成したインスタンスに対するプロパティの設定など)
- NotImplementedException
- プロパティの機能が未実装の場合
これ以外の例外をスローする必要がある場合は、プロパティよりメソッドとして公開するほうが望ましいかもしれません。
プロパティ変更の通知 (INotifyPropertyChanged)
プロパティに対する変更をインスタンス外に通知する汎用的な手段として、.NET FrameworkではINotifyPropertyChangedインターフェイスが用意されています。 これはデータバインディングなどの目的でプロパティの変更を通知したい場合に使用するもので、データソースとなるインスタンスでプロパティが変更された場合にPropertyChangedイベントを発生させ、データの表示を行うビューなどに変更が行われたことを通知することができます。
この例で使用しているリフレクションについての解説はリフレクション §.PropertyInfoを使ったプロパティ・インデクサの操作、イベント機構についてはイベントを参照してください。
INotifyPropertyChanged.PropertyChangedイベントでは、変更があったプロパティ名をPropertyChangedEventArgsで文字列として通知します。 このため、INotifyPropertyChangedを実装したクラスでプロパティ名を変更することになった場合には、このプロパティ名となる文字列(上記の例におけるRaisePropertyChangedに渡す引数)も合わせて変更する必要があります。 コンパイラではこの変更が妥当かどうかを検知できないため、変更を行う際には注意を払う必要があります。
このような問題に対して、.NET Framework 4.5以降ではCallerMemberNameAttributeを使うことができます。 この属性は、呼び出し元のメンバ名をメソッドの引数に自動的に代入するもので、C/C++において行番号やファイル名をソース中に埋め込む__LINE__
や__FILE__
といったマクロに似た効果をもつものです。 この属性を使うことで、メソッドの呼び出し元ではプロパティ名を指定する必要がなくなり、プロパティ名を文字列で指定する手間と誤りの可能性を減らすことができます。
これを使うと上記のサンプルにおけるプロパティ名の指定箇所は次のように簡略化することができます。
CallerMemberNameAttributeを使うことによってプロパティの値の比較・設定・イベントの発行の一連の処理を共通化できるため、さらに次のように簡略化することができます。
この例で使用しているEqualityComparerおよびIEqualityComparerについては等価性の定義と比較を参照してください。
なお、ObservableCollectionクラスはINotifyPropertyChangedを実装しています。 コレクションへの通知を検知したい場合にはこのクラスを使うことができます。