ここでは.NET Frameworkにおけるプロパティと、プロパティの実装に関する事項・注意点などについて解説します。 またインデクサやインデックス付きプロパティについても解説します。
プロパティ
C#やVBではプロパティ構文がサポートされています。 プロパティはクラス・構造体・インターフェイスに持たせることができます。 見かけ上はプロパティに対する値の取得・設定はフィールドに対するものと変わりありません。
using System;
class Account {
// プロパティ
public int ID {
get {
return _id;
}
set {
_id = value;
}
}
// プロパティの値を保持するフィールド
private int _id;
}
class Sample {
static void Main()
{
var a = new Account();
// プロパティに値を設定する
a.ID = 3;
// プロパティから値を取得する
int id = a.ID;
Console.WriteLine(id);
}
}
Imports System
Class Account
' プロパティ
Public Property ID As Integer
Get
Return _id
End Get
Set
_id = value
End Set
End Property
' プロパティの値を保持するフィールド
Private _id As Integer
End Class
Class Sample
Shared Sub Main()
Dim a As New Account()
' プロパティに値を設定する
a.ID = 3
' プロパティから値を取得する
Dim id As Integer = a.ID
Console.WriteLine(id)
End Sub
End Class
3
プロパティではこのようにアクセサメソッドを使ってフィールドに対する値の取得・設定を行います。
実装を持たないインターフェイスでは、プロパティを以下のように記述します。
interface IAccount {
// プロパティ
int ID { get; set; }
}
Interface IAccount
' プロパティ
Property ID As Integer
End Interface
アクセサメソッド
.NET Frameworkにおけるプロパティは、set
アクセサまたは/およびget
アクセサのアクセサメソッドの組み合わせとなっています。 プロパティを記述するコードはコンパイル時にアクセサメソッドとして展開され、またプロパティに対するアクセスはアクセサメソッドの呼び出しに展開されます。
単にプロパティの値を取得・設定するといった操作であればアクセサメソッドの存在を意識する必要はありませんが、次の例のようにプロパティを持つ型をリフレクションによって調べると、プロパティとなるメンバとは別にアクセサメソッドが存在していることが確認できます。
using System;
using System.Reflection;
class Account {
public int ID {
get {
return _id;
}
set {
_id = value;
}
}
private int _id;
}
class Sample {
static void Main()
{
// 型に含まれるすべてのパブリックなインスタンスメンバを表示する
foreach (var m in typeof(Account).GetMembers(BindingFlags.Public | BindingFlags.Instance)) {
Console.WriteLine("{0}\t{1}", m.MemberType, m);
}
}
}
Imports System
Imports System.Reflection
Class Account
Public Property ID As Integer
Get
Return _id
End Get
Set
_id = value
End Set
End Property
Private _id As Integer
End Class
Class Sample
Shared Sub Main()
' 型に含まれるすべてのパブリックなインスタンスメンバを表示する
For Each m As MemberInfo In GetType(Account).GetMembers(BindingFlags.Public Or BindingFlags.Instance)
Console.WriteLine("{0}{1}{2}", m.MemberType, vbTab, m)
Next
End Sub
End Class
Method Int32 get_ID() Method Void set_ID(Int32) Method Boolean Equals(System.Object) Method Int32 GetHashCode() Method System.Type GetType() Method System.String ToString() Constructor Void .ctor() Property Int32 ID
この結果にあるget_ID
がプロパティID
に対応するget
アクセサ、set_ID
がset
アクセサとなります。
このようにして作成されるアクセサメソッドを直接呼び出すことはできません。 そのようなコードを記述した場合はコンパイルエラーとなります。
using System;
class Account {
public int ID {
get {
return _id;
}
set {
_id = value;
}
}
private int _id;
}
class Sample {
static void Main()
{
var a = new Account();
// error CS0571: `Account.ID.set': 演算子またはアクセサーを明示的に呼び出すことはできません。
a.set_ID(3);
// error CS0571: `Account.ID.get': 演算子またはアクセサーを明示的に呼び出すことはできません。
int id = a.get_ID();
Console.WriteLine(id);
}
}
Imports System
Class Account
Public Property ID As Integer
Get
Return _id
End Get
Set
_id = value
End Set
End Property
Private _id As Integer
End Class
Class Sample
Shared Sub Main()
Dim a As New Account()
' error BC30456: 'set_ID' は 'Account' のメンバではありません。
a.set_ID(3)
' error BC30456: 'set_ID' は 'Account' のメンバではありません。
Dim id As Integer = a.get_ID()
Console.WriteLine(id)
End Sub
End Class
また、アクセサメソッドと同じシグネチャのメソッドが存在する場合もコンパイルエラーとなります。 逆に、シグネチャが異なっていればアクセサメソッドと同名のメソッドを作成することはできます。
using System;
class Account {
// error CS0082: 型 'Account' は、'Account.get_ID()' と呼ばれるメンバを同じパラメータの型で既に予約しています。
// error CS0082: 型 'Account' は、'Account.set_ID(int)' と呼ばれるメンバを同じパラメータの型で既に予約しています。
public int ID {
get {
return _id;
}
set {
_id = value;
}
}
private int _id;
public int get_ID()
{
return 0;
}
public void set_ID(int val)
{
}
// このメソッドはアクセサメソッドとシグネチャが異なるため
// コンパイルエラーとはならない
public void set_ID(string arg)
{
}
}
Imports System
Class Account
' error BC31060: property 'ID' は、同じ名前のメンバーと class 'Account' で競合する 'get_ID' を暗黙的に定義しています。
' error BC31060: property 'ID' は、同じ名前のメンバーと class 'Account' で競合する 'set_ID' を暗黙的に定義しています。
Public Property ID As Integer
Get
Return _id
End Get
Set
_id = value
End Set
End Property
Private _id As Integer
Public Function get_ID() As Integer
Return 0
End Function
Public Sub set_ID(ByVal val As Integer)
End Sub
' このメソッドはアクセサメソッドとシグネチャが異なるため
' コンパイルエラーとはならない
Public Sub set_ID(ByVal arg As String)
End Sub
End Class
読み取り専用・書き込み専用・アクセシビリティ
.NET Frameworkにおけるプロパティでは、プロパティへのアクセスを読み取り専用(または書き込み専用)にすることができます。
using System;
class Account {
// getアクセサのみのプロパティ(読み取り専用)
public int ID {
get {
return _id;
}
}
private int _id;
public Account(int id)
{
// コンストラクタでプロパティの初期値を設定する
this._id = id;
}
}
class Sample {
static void Main()
{
var a = new Account(3);
// 読み取り専用プロパティで値を設定することはできない
// error CS0200: プロパティまたはインデクサー 'Account.ID' は読み取り専用なので、割り当てることはできません。
a.ID = 42;
}
}
Imports System
Class Account
' getアクセサのみのプロパティ(読み取り専用)
Public ReadOnly Property ID As Integer
Get
Return _id
End Get
End Property
Private _id As Integer
Public Sub New(ByVal id As Integer)
' コンストラクタでプロパティの初期値を設定する
Me._id = id
End Sub
End Class
Class Sample
Shared Sub Main()
Dim a As New Account(3)
' 読み取り専用プロパティで値を設定することはできない
' error BC30526: プロパティ 'ID' は 'ReadOnly' です。
a.ID = 42
End Sub
End Class
派生クラスのみに公開する場合などを除けば、書き込み専用プロパティを外部に公開することはまれです。 こういった場合はプロパティではなくSetXXX
といったメソッドを提供するほうが自然です。
using System;
class Account {
/*
* 書き込み専用プロパティ
public int ID {
set {
_id = value;
}
}
*/
// フィールドに値を設定するメソッド
public void SetID(int newid)
{
_id = newid;
}
private int _id;
}
Imports System
Class Account
' 書き込み専用プロパティ
'Public WriteOnly Property ID As Integer
' Set
' _id = value
' End Set
'End Property
' フィールドに値を設定するメソッド
Public Sub SetID(ByVal newid As Integer)
_id = newid
End Sub
Private _id As Integer
End Class
また、アクセサメソッドのアクセシビリティもset
とget
で異なるものを指定することができるため、例えば派生クラスからのみ設定可能なプロパティを作成するといったことができます。
using System;
class Account {
public int ID {
// getアクセサはpublic
get {
return _id;
}
// setアクセサはprotected
// (派生クラスからのみsetできる)
protected set {
_id = value;
}
}
private int _id;
}
Imports System
Class Account
Public Property ID As Integer
' GetアクセサはPublic
Get
Return _id
End Get
' SetアクセサはProtected
' (派生クラスからのみsetできる)
Protected Set
_id = value
End Set
End Property
Private _id As Integer
End Class
自動実装
C#やVBでは構文によるサポートによってプロパティの自動実装を行うことができます。 これは、プロパティのアクセサ部分で行う処理が単純に値を返すだけ/設定するだけとなる場合に、その記述を省略することができるものです。
using System;
class Account {
// プロパティの自動実装
public int ID {
get;
set;
}
// 自動実装したプロパティは次のようなコードに展開される
/*
public int ID {
get { return _id; }
set { _id = value; }
}
private int _id;
*/
}
class Sample {
static void Main()
{
var a = new Account();
a.ID = 3;
int id = a.ID;
Console.WriteLine(id);
}
}
Imports System
Class Account
' プロパティの自動実装
Public Property ID As Integer
' 自動実装したプロパティは次のようなコードに展開される
'Public Property ID As Integer
' Get
' Return _id
' End Get
' Set
' _id = value
' End Set
'End Property
'
'Private _id As Integer
End Class
Class Sample
Shared Sub Main()
Dim a As New Account()
a.ID = 3
Dim id As Integer = a.ID
Console.WriteLine(id)
End Sub
End Class
3
プロパティの自動実装はC# 3.0、VB2010以降でサポートされています。
バッキングフィールド
プロパティの自動実装を行う場合、プロパティの値を保持するフィールド(バッキングフィールド)も自動的に作成されます。 このフィールドはリフレクションによって調べることができます。
using System;
using System.Reflection;
class Account {
public int ID {
get;
set;
}
}
class Sample {
static void Main()
{
foreach (var f in typeof(Account).GetFields(BindingFlags.NonPublic | BindingFlags.Instance)) {
Console.WriteLine(f);
}
}
}
Int32 <ID>k__BackingField
Imports System
Imports System.Reflection
Class Account
Public Property ID As Integer
End Class
Class Sample
Shared Sub Main()
For Each f As FieldInfo In GetType(Account).GetFields(BindingFlags.NonPublic Or BindingFlags.Instance)
Console.WriteLine(f)
Next
End Sub
End Class
Int32 _ID
このようにC#とVBでは生成されるバッキングフィールド名が異なり、C#では<プロパティ名>k__BackingField
、VBでは_プロパティ名
となるようです。
自動実装プロパティの初期値
C# 6.0以降、VB2010以降では自動実装プロパティに初期値を与えることができます。
class Account {
public int ID { get; set } = 3;
}
Class Account
Public Property ID As Integer = 3
End Class
C#では読み取り専用かつ初期値を持たせたプロパティを自動実装することができますが、VBではできません。
class Account {
public int ID { get; } = 3;
}
読み取り専用かつ初期値を持たせたプロパティの自動実装は、インスタンス作成時に値を指定して以降は一切値を変更できないような不変オブジェクトを作成する上で非常に役立ちます。
このようなプロパティの自動実装を使えない場合、バッキングフィールド・アクセサメソッド・コンストラクタでの初期値の設定などをすべて記述することで不変オブジェクトを構築することができます。
class Account {
// 読み取り専用プロパティ
public int ID {
get { return _id; }
}
// 読み取り専用フィールド
private readonly int _id;
public Account(int id)
{
// フィールドの値をコンストラクタで設定する
_id = id;
}
}
Class Account
' 読み取り専用プロパティ
Public ReadOnly Property ID As Integer
Get
Return _id
End Get
End Property
' 読み取り専用フィールド
Private ReadOnly _id As Integer
Public Sub New(ByVal id As Integer)
' フィールドの値をコンストラクタで設定する
_id = id
End Sub
End Class
インデクサ
インデクサ(indexer)とは添字(index)をつけることができるプロパティで、添字を使ってインスタンスを配列のように扱えるようにするものです。 プロパティ名を省略して直接インスタンスに添字を指定して値の取得/設定を行うように見えるため、VBでは既定のプロパティとも呼ばれます。 インデクサはstring型やList, Dictionaryなどのコレクションクラスで使われています。
using System;
class Sample {
static void Main()
{
string str = "Hello, world!";
// インデクサによって文字列インスタンスをcharの配列のように扱える
Console.WriteLine(str[0]);
Console.WriteLine(str[1]);
Console.WriteLine(str[2]);
Console.WriteLine(str[3]);
Console.WriteLine(str[4]);
}
}
Imports System
Class Sample
Shared Sub Main()
Dim str As String = "Hello, world!"
' インデクサによって文字列インスタンスをCharの配列のように扱える
Console.WriteLine(str(0))
Console.WriteLine(str(1))
Console.WriteLine(str(2))
Console.WriteLine(str(3))
Console.WriteLine(str(4))
End Sub
End Class
H e l l o
配列とは異なり、インデクサの添字部分には整数型以外の型も指定することができます。 例えばDictionary<TKey, TValue>では任意の型を添字(キー)として使用することができ、インデクサではこのキーを指定することによって対応する値にアクセスすることが出来るようになっています。
using System;
using System.Collections.Generic;
class Sample {
static void Main()
{
// 文字列を添字(キー)として使用するDictionary
var dict = new Dictionary<string, int>();
dict["Alice"] = 0;
dict["Bob"] = 1;
dict["Charlie"] = 2;
}
}
Imports System
Imports System.Collections.Generic
Class Sample
Shared Sub Main()
' 文字列を添字(キー)として使用するDictionary
Dim dict As New Dictionary(Of String, Integer)()
dict("Alice") = 0
dict("Bob") = 1
dict("Charlie") = 2
End Sub
End Class
実装上はインデクサもプロパティの1形態となっています。 実際、リフレクションでもインデクサはPropertyInfoとして扱われます。 (リフレクション §.PropertyInfoを使ったプロパティ・インデクサの操作)
型にインデクサを実装する場合は、次のようにします。
using System;
class ByteArray {
private byte[] arr;
public ByteArray(int length)
{
arr = new byte[length];
}
// インデクサとなるプロパティ
public byte this[int index] { // 引数としてインデックスを受け取る
// 引数で指定されたインデックスの値を返す
get { return arr[index]; }
// 引数で指定されたインデックスに値を設定する
// (設定される値はキーワードvalueから参照することができる)
set { arr[index] = value; }
}
}
class Sample {
static void Main()
{
var arr = new ByteArray(3);
arr[0] = 2;
arr[1] = 3;
arr[2] = 4;
for (var i = 0; i < 3; i++) {
Console.WriteLine(arr[i]);
}
}
}
Imports System
Class ByteArray
Private arr() As Byte
Public Sub New(ByVal length As Integer)
arr = New Byte(length - 1) {}
End Sub
' インデクサとなるプロパティ
Public Default Property Item(ByVal index As Integer) As Byte ' 引数としてインデックスを受け取る
' 引数で指定されたインデックスの値を返す
Get
Return arr(index)
End Get
' 引数で指定されたインデックスに値を設定する
' (設定される値はキーワードvalueから参照することができる)
Set
arr(index) = value
End Set
End Property
End Class
Class Sample
Shared Sub Main()
Dim arr As New ByteArray(3)
arr(0) = 2
arr(1) = 3
arr(2) = 4
For i As Integer = 0 To 2
Console.WriteLine(arr(i))
Next
End Sub
End Class
2 3 4
インデックス付きプロパティとインデクサの名前
VBではプロパティに添字をもたせることでインデックス付きプロパティを作成することができます。 また、Default
修飾子によってインデックス付きプロパティを既定のプロパティとすることによって、プロパティをインデクサとすることができます。
Imports System
Class C
' インデクサ(既定のプロパティ)
Public Default Property Indexer(ByVal index As Integer) As String
' 実装は省略
Get
Return Nothing
End Get
Set
End Set
End Property
' インデックスつきプロパティ
Public Property IndexedProperty(ByVal index As Integer) As String
' 実装は省略
Get
Return Nothing
End Get
Set
End Set
End Property
End Class
Class Sample
Shared Sub Main()
Dim c As New C()
' インデクサに値を設定する
c(0) = "foo"
c(1) = "bar"
c(2) = "baz"
' 既定のプロパティではプロパティ名を省略することができる
' (つまり上記のコードは以下と同じ)
c.Indexer(0) = "foo"
c.Indexer(1) = "bar"
c.Indexer(2) = "baz"
' インデックス付きプロパティに値を設定する
c.IndexedProperty(0) = "foo"
c.IndexedProperty(1) = "bar"
c.IndexedProperty(2) = "baz"
End Sub
End Class
一方C#ではインデックス付きプロパティを作ることはできません。 そのため、かわりに配列やIList<T>などのコレクションを返すプロパティとして実装する必要があります。 また、任意の名前でインデクサを作成することもできず、型で定義できるインデクサの数もひとつに限られます。
C#ではインデクサに名前を指定できないため、C#で作成したインデクサを他の言語からインデックス付きプロパティとして参照する場合はデフォルトの名前であるItem
を使用します。 この名前を変更するには、インデクサに属性IndexerNameAttributeを指定します。
using System;
class C {
// Indexerという名前のインデックス付きプロパティとしてアクセスできるようにする
[System.Runtime.CompilerServices.IndexerName("Indexer")]
public string this[int index] {
// 実装は省略
get { return null; }
set { }
}
}
インデクサが他の言語からアクセスされることを考慮する場合、インデクサには適切な名前を付けておくことが推奨されます。 (言語間の相互運用性と共通言語仕様 (CLS))
コレクションを返すプロパティ
インデクサはコレクションやそれに類する機能を持つクラスで実装すべきもので、多くの場合はインデクサよりも単に配列やList<T>などのコレクション、IList<T>やICollection<T>などのインターフェイスを返すプロパティを用意するほうが適切です。 特にList<T>やIList<T>を返すプロパティはインデックス付きプロパティの代替として使用することができます。
インデックス付きプロパティのような機能をコレクションを返すプロパティとして公開する場合は、値を格納するコレクション自体を変更されないように読み取り専用で公開します。
using System;
using System.Collections.Generic;
class Account {
private readonly List<string> _addresses = new List<string>();
public List<string> Addresses {
get { return _addresses; }
}
}
class Sample {
static void Main()
{
var a = new Account();
a.Addresses.Add("alice@example.com");
a.Addresses.Add("alice-2@mail.example.net");
a.Addresses[0] = "alice-1@mail.example.net";
// 読み取り専用なのでコレクション自体を置き換える操作はできない
//a.Addresses = new List<string>() {"alice@example.com"};
}
}
Imports System
Imports System.Collections.Generic
Class Account
Private ReadOnly _addresses As List(Of String) = New List(Of String)()
Public ReadOnly Property Addresses As List(Of String)
Get
Return _addresses
End Get
End Property
End Class
Class Sample
Shared Sub Main()
Dim a As New Account()
a.Addresses.Add("alice@example.com")
a.Addresses.Add("alice-2@mail.example.net")
a.Addresses(0) = "alice-1@mail.example.net"
' 読み取り専用なのでコレクション自体を置き換える操作はできない
'a.Addresses = New List(Of String)() {"alice@example.com"}
End Sub
End Class
さらに、公開されるコレクション自体も参照専用としたい(コレクションの内容を変更させたくない)場合は、IReadOnlyListインターフェイス(.NET Framework 4.5以降)やReadOnlyCollectionとして公開する方法をとることができます。
using System;
using System.Collections.Generic;
class Account {
public Account(IEnumerable<string> addresses)
{
_addresses = new List<string>(addresses);
}
private readonly List<string> _addresses;
// IReadOnlyListとして公開する
public IReadOnlyList<string> Addresses {
get { return _addresses; }
}
}
class Sample {
static void Main()
{
var a = new Account(new[] {"alice@example.com", "alice-2@mail.example.net"});
// コレクションの参照
Console.WriteLine(a.Addresses.Count);
Console.WriteLine(a.Addresses[0]);
// IReadOnlyListではインデクサに対する設定はサポートされない
//a.Addresses[0] = "alice-1@mail.example.net";
// またAddなどコレクションの変更を行うメソッドも用意されない
//a.Addresses.Add("alice-1@mail.example.net");
}
}
Imports System
Imports System.Collections.Generic
Class Account
Public Sub New(ByVal addresses As IEnumerable(Of String))
_addresses = New List(Of String)(addresses)
End Sub
Private ReadOnly _addresses As List(Of String)
' IReadOnlyListとして公開する
Public ReadOnly Property Addresses As IReadOnlyList(Of String)
Get
Return _addresses
End Get
End Property
End Class
Class Sample
Shared Sub Main()
Dim a As New Account(New String() {"alice@example.com", "alice-2@mail.example.net"})
' コレクションの参照
Console.WriteLine(a.Addresses.Count)
Console.WriteLine(a.Addresses(0))
' IReadOnlyListではインデクサに対する設定はサポートされない
'a.Addresses(0) = "alice-1@mail.example.net"
' またAddなどコレクションの変更を行うメソッドも用意されない
'a.Addresses.Add("alice-1@mail.example.net")
End Sub
End Class
その他コレクションクラスおよびインターフェイスについてはコレクションの種類と特徴、読み取り専用コレクションについては汎用ジェネリックコレクション(1) Collection/ReadOnlyCollection §.ReadOnlyCollectionを参照してください。
イテレータ
プロパティにおいてもイテレータ構文を使用することができます。 これにより、IEnumerableを返すプロパティを簡単に記述することが出来ます。
using System;
using System.Collections.Generic;
class C {
public IEnumerable<int> P {
get {
yield return 0;
yield return 1;
yield return 2;
yield return 3;
yield return 4;
}
}
}
class Sample {
static void Main()
{
var c = new C();
// プロパティPから列挙される値を表示する
foreach (var val in c.P) {
Console.WriteLine(val);
}
}
}
Imports System
Imports System.Collections.Generic
Class C
Public ReadOnly Iterator Property P As IEnumerable(Of Integer)
Get
Yield 0
Yield 1
Yield 2
Yield 3
Yield 4
End Get
End Property
End Class
Class Sample
Shared Sub Main()
Dim c As New C()
' プロパティPから列挙される値を表示する
For Each val As Integer In c.P
Console.WriteLine(val)
Next
End Sub
End Class
0 1 2 3 4
イテレータに関してはイテレータを参照してください。
プロパティと例外
プロパティではアクセサメソッドを使ってフィールドの値を取得・設定するため、その際に値の検証を行う処理を記述することができます。 また、検証した結果として例外をスローすることもできます。 例えば、設定される値がプロパティとして有効な値の範囲外だった場合にはArgumentOutOfRangeException、null
/Nothing
を許容しない場合にはArgumentNullException、その他不正な値であればArgumentExceptionをスローすることができます。
これらの例外をスローする場合は、例外コンストラクタの引数で例外メッセージを記述するとともに、引数paramNameに原因となったプロパティの名前を設定します。 また、ArgumentOutOfRangeExceptionでは引数actualValueに原因となった値を指定することができ、これによりエラー原因が把握しやすくなります。
using System;
// 角度を表すクラス
class Degree {
public int Value {
get { return val; }
set {
// プロパティに設定される値を検証する
if (value < 0 || 360 <= value)
throw new ArgumentOutOfRangeException("Value", value, "角度には0以上360未満の値を指定してください。");
// 検証した結果問題ない値ならフィールドに値を保持する
val = value;
}
}
private int val;
}
class Sample {
static void Main()
{
var d = new Degree();
d.Value = 360;
}
}
Imports System
' 角度を表すクラス
Class Degree
Public Property Value As Integer
Get
Return val
End Get
Set
' プロパティに設定される値を検証する
If value < 0 OrElse 360 <= value Then
Throw New ArgumentOutOfRangeException("Value", value, "角度には0以上360未満の値を指定してください。")
End If
' 検証した結果問題ない値ならフィールドに値を保持する
val = value
End Set
End Property
Private val As Integer
End Class
Class Sample
Shared Sub Main()
Dim d As New Degree()
d.Value = 360
End Sub
End Class
ハンドルされていない例外: System.ArgumentOutOfRangeException: 角度には0以上360未満の値を指定してください。 パラメーター名:Value 実際の値は 360 です。 場所 Degree.set_Value(Int32 value) 場所 Sample.Main()
一方この例の場合では、例外をスローせず、次のように値を適正な範囲に丸め込む実装とすることも考えられます。
using System;
// 角度を表すクラス
class Degree {
public int Value {
get { return val; }
set {
val = value;
// 値を0 <= val < 360の範囲に正規化する
for (;;) {
if (val < 0)
val += 360;
else if (360 <= val)
val -= 360;
else
break;
}
}
}
private int val;
}
class Sample {
static void Main()
{
var d = new Degree();
d.Value = 480;
Console.WriteLine(d.Value);
}
}
Imports System
' 角度を表すクラス
Class Degree
Public Property Value As Integer
Get
Return val
End Get
Set
val = value
' 値を0 <= val < 360の範囲に正規化する
Do
If val < 0 Then
val += 360
Else If 360 <= val
val -= 360
Else
Exit Do
End If
Loop
End Set
End Property
Private val As Integer
End Class
Class Sample
Shared Sub Main()
Dim d As New Degree()
d.Value = 480
Console.WriteLine(d.Value)
End Sub
End Class
120
一般に、プロパティでは単に値の取得・設定のみを行うべきで、それ以上の副作用が起こることは避けるべきです。 例えば上記の例においては、設定した値とその後に取得される値が異なることから、実装を知らずに結果だけを見ると意図した動作と異なるような違和感を覚える場合もあります。 この他にも、プロパティを設定することがインスタンス内の他のメンバに影響するような実装(一つのプロパティで複数のフィールドを変更するなど)は避けるべきです。
また例外に関しても、プロパティから以下に挙げるようなもの以外の例外をスローする場合にはメソッドとして実装したほうがよいとされます。 プロパティからスローされることが想定(あるいは許容)される例外と状況の主なものとしては次のようなものがあります。
- ArgumentOutOfRangeException, ArgumentNullException, ArgumentException
- プロパティに設定される値としては不正な場合
- InvalidEnumArgumentException
- プロパティに設定される列挙体の値が不正な場合
- IndexOutOfRangeException
- インデクサに指定されるインデックスが範囲内の場合の場合
- InvalidOperationException
- 現在のインスタンスの状態ではプロパティの表す機能を要求できない場合 (例えば、処理の進行中にその処理に影響するプロパティを変更しようとするなど)
- ObjectDisposedException
- インスタンスが破棄された後にプロパティにアクセスしようとした場合 (オブジェクトの破棄 §.解放されたリソースへのアクセス拒否 (ObjectDisposedException))
- NotSupportedException
- インスタンスがプロパティの表す機能をサポートしていない場合 (例えば、読み取り専用として作成したインスタンスに対するプロパティの設定など)
- NotImplementedException
- プロパティの機能が未実装の場合
これ以外の例外をスローする必要がある場合は、プロパティよりメソッドとして公開するほうが望ましいかもしれません。
プロパティ変更の通知 (INotifyPropertyChanged)
プロパティに対する変更をインスタンス外に通知する汎用的な手段として、.NET FrameworkではINotifyPropertyChangedインターフェイスが用意されています。 これはデータバインディングなどの目的でプロパティの変更を通知したい場合に使用するもので、データソースとなるインスタンスでプロパティが変更された場合にPropertyChangedイベントを発生させ、データの表示を行うビューなどに変更が行われたことを通知することができます。
using System;
using System.ComponentModel;
using System.Reflection;
class Account : INotifyPropertyChanged {
// プロパティが変更された場合に発行するイベント
public event PropertyChangedEventHandler PropertyChanged;
// 変更を通知するプロパティ
private int _id;
public int ID {
get { return _id; }
set {
if (value != _id) {
// 値が変更された場合、フィールドの値を更新したのちイベントを発行する
_id = value;
RaisePropertyChanged("ID");
}
}
}
private string _name;
public string Name {
get { return _name; }
set {
if (!string.Equals(value, _name)) {
_name = value;
RaisePropertyChanged("Name");
}
}
}
// プロパティが変更された場合にPropertyChangedイベントを発行するメソッド
private void RaisePropertyChanged(string propertyName)
{
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
class Sample {
static void Main()
{
var a = new Account();
// プロパティの変更を購読するイベントハンドラを割り当てる
a.PropertyChanged += PropertyChanged;
// プロパティの値を変更する
a.ID = 3;
a.Name = "Alice";
}
private static void PropertyChanged(object sender, PropertyChangedEventArgs e)
{
// 変更されたプロパティの値をリフレクションによって取得する
object newValue = sender.GetType().GetProperty(e.PropertyName).GetValue(sender, null);
Console.WriteLine("プロパティ'{0}'の値が'{1}'に変更されました", e.PropertyName, newValue);
}
}
Imports System
Imports System.ComponentModel
Imports System.Reflection
Class Account
Implements INotifyPropertyChanged
' プロパティが変更された場合に発行するイベント
Public Event PropertyChanged As PropertyChangedEventHandler Implements INotifyPropertyChanged.PropertyChanged
' 変更を通知するプロパティ
Private _id As Integer
Public Property ID As Integer
Get
Return _id
End Get
Set
If value <> _id Then
' 値が変更された場合、フィールドの値を更新したのちイベントを発行する
_id = value
RaisePropertyChanged("ID")
End If
End Set
End Property
Private _name As String
Public Property Name As String
Get
Return _name
End Get
Set
If Not String.Equals(value, _name) Then
_name = value
RaisePropertyChanged("Name")
End If
End Set
End Property
' プロパティが変更された場合にPropertyChangedイベントを発行するメソッド
Private Sub RaisePropertyChanged(ByVal propertyName As String)
RaiseEvent PropertyChanged(Me, New PropertyChangedEventArgs(propertyName))
End Sub
End Class
Class Sample
Shared Sub Main()
Dim a As New Account()
' プロパティの変更を購読するイベントハンドラを割り当てる
AddHandler a.PropertyChanged, AddressOf PropertyChanged
' プロパティの値を変更する
a.ID = 3
a.Name = "Alice"
End Sub
Private Shared Sub PropertyChanged(ByVal sender As Object, ByVal e As PropertyChangedEventArgs)
' 変更されたプロパティの値をリフレクションによって取得する
Dim newValue As Object = sender.GetType().GetProperty(e.PropertyName).GetValue(sender, Nothing)
Console.WriteLine("プロパティ'{0}'の値が'{1}'に変更されました", e.PropertyName, newValue)
End Sub
End Class
プロパティ'ID'の値が'3'に変更されました プロパティ'Name'の値が'Alice'に変更されました
この例で使用しているリフレクションについての解説はリフレクション §.PropertyInfoを使ったプロパティ・インデクサの操作、イベント機構についてはイベントを参照してください。
INotifyPropertyChanged.PropertyChangedイベントでは、変更があったプロパティ名をPropertyChangedEventArgsで文字列として通知します。 このため、INotifyPropertyChangedを実装したクラスでプロパティ名を変更することになった場合には、このプロパティ名となる文字列(上記の例におけるRaisePropertyChangedに渡す引数)も合わせて変更する必要があります。 コンパイラではこの変更が妥当かどうかを検知できないため、変更を行う際には注意を払う必要があります。
このような問題に対して、.NET Framework 4.5以降ではCallerMemberNameAttributeを使うことができます。 この属性は、呼び出し元のメンバ名をメソッドの引数に自動的に代入するもので、C/C++において行番号やファイル名をソース中に埋め込む__LINE__
や__FILE__
といったマクロに似た効果をもつものです。 この属性を使うことで、メソッドの呼び出し元ではプロパティ名を指定する必要がなくなり、プロパティ名を文字列で指定する手間と誤りの可能性を減らすことができます。
これを使うと上記のサンプルにおけるプロパティ名の指定箇所は次のように簡略化することができます。
using System;
using System.ComponentModel;
using System.Runtime.CompilerServices;
class Account : INotifyPropertyChanged {
public event PropertyChangedEventHandler PropertyChanged;
private int _id;
public int ID {
get { return _id; }
set {
if (value != _id) {
_id = value;
RaisePropertyChanged(); // 呼び出し元はプロパティ名を指定する必要が無い
}
}
}
private string _name;
public string Name {
get { return _name; }
set {
if (!string.Equals(value, _name)) {
_name = value;
RaisePropertyChanged();
}
}
}
// 引数propertyNameに呼び出し元のプロパティ名が代入された上で呼び出される
private void RaisePropertyChanged([CallerMemberName] string propertyName = null)
{
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
Imports System
Imports System.ComponentModel
Imports System.Runtime.CompilerServices
Class Account
Implements INotifyPropertyChanged
Public Event PropertyChanged As PropertyChangedEventHandler Implements INotifyPropertyChanged.PropertyChanged
Private _id As Integer
Public Property ID As Integer
Get
Return _id
End Get
Set
If value <> _id Then
_id = value
RaisePropertyChanged() ' 呼び出し元はプロパティ名を指定する必要が無い
End If
End Set
End Property
Private _name As String
Public Property Name As String
Get
Return _name
End Get
Set
If Not String.Equals(value, _name) Then
_name = value
RaisePropertyChanged()
End If
End Set
End Property
' 引数propertyNameに呼び出し元のプロパティ名が代入された上で呼び出される
Private Sub RaisePropertyChanged(<CallerMemberName> Optional ByVal propertyName As String = Nothing)
RaiseEvent PropertyChanged(Me, New PropertyChangedEventArgs(propertyName))
End Sub
End Class
CallerMemberNameAttributeを使うことによってプロパティの値の比較・設定・イベントの発行の一連の処理を共通化できるため、さらに次のように簡略化することができます。
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Runtime.CompilerServices;
class Account : INotifyPropertyChanged {
public event PropertyChangedEventHandler PropertyChanged;
private int _id;
public int ID {
get { return _id; }
set { SetValue(ref _id, value, EqualityComparer<int>.Default); }
}
private string _name;
public string Name {
get { return _name; }
set { SetValue(ref _name, value, StringComparer.Ordinal); }
}
private void SetValue<T>(ref T storage,
T newValue,
IEqualityComparer<T> comparer,
[CallerMemberName] string propertyName = null)
{
// フィールドの現在の値と新しい値を比較する
if (!comparer.Equals(storage, newValue)) {
// もとのフィールドに新しい値を設定する
storage = newValue;
// イベントを発行する
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
}
Imports System
Imports System.Collections.Generic
Imports System.ComponentModel
Imports System.Runtime.CompilerServices
Class Account
Implements INotifyPropertyChanged
Public Event PropertyChanged As PropertyChangedEventHandler Implements INotifyPropertyChanged.PropertyChanged
Private _id As Integer
Public Property ID As Integer
Get
Return _id
End Get
Set
SetValue(_id, value, EqualityComparer(Of Integer).Default)
End Set
End Property
Private _name As String
Public Property Name As String
Get
Return _name
End Get
Set
SetValue(_name, value, StringComparer.Ordinal)
End Set
End Property
Private Sub SetValue(Of T)(ByRef storage As T, _
ByVal newValue As T, _
ByVal comparer As IEqualityComparer(Of T), _
<CallerMemberName> Optional ByVal propertyName As String = Nothing)
' フィールドの現在の値と新しい値を比較する
If Not comparer.Equals(storage, newValue) Then
' もとのフィールドに新しい値を設定する
storage = newValue
' イベントを発行する
RaiseEvent PropertyChanged(Me, New PropertyChangedEventArgs(propertyName))
End If
End Sub
End Class
この例で使用しているEqualityComparerおよびIEqualityComparerについては等価性の定義と比較を参照してください。
なお、ObservableCollectionクラスはINotifyPropertyChangedを実装しています。 コレクションへの通知を検知したい場合にはこのクラスを使うことができます。