ここでは.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_IDsetアクセサとなります。

このようにして作成されるアクセサメソッドを直接呼び出すことはできません。 そのようなコードを記述した場合はコンパイルエラーとなります。

アクセサメソッドを直接呼び出す
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

また、アクセサメソッドのアクセシビリティもsetgetで異なるものを指定することができるため、例えば派生クラスからのみ設定可能なプロパティを作成するといったことができます。

アクセサごとに異なるアクセシビリティを指定する
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以降では自動実装プロパティに初期値を与えることができます。

自動実装プロパティの初期値に初期値を与える(C# 6.0以降) 
class Account {
  public int ID { get; set } = 3;
}
自動実装プロパティの初期値に初期値を与える(VB2010以降) 
Class Account
  Public Property ID As Integer = 3
End Class

C#では読み取り専用かつ初期値を持たせたプロパティを自動実装することができますが、VBではできません。

読み取り専用の自動実装プロパティに初期値を与える(C# 6.0以降)
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などのコレクションクラスで使われています。

string型とインデクサによるアクセス
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]);
  }
}
string型とインデクサによるアクセス
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>では任意の型を添字(キー)として使用することができ、インデクサではこのキーを指定することによって対応する値にアクセスすることが出来るようになっています。

Dictionaryとインデクサによるアクセス
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;
  }
}
Dictionaryとインデクサによるアクセス
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

イテレータに関してはイテレータを参照してください。

プロパティと例外

プロパティではアクセサメソッドを使ってフィールドの値を取得・設定するため、その際に値の検証を行う処理を記述することができます。 また、検証した結果として例外をスローすることもできます。 例えば、設定される値がプロパティとして有効な値の範囲外だった場合にはArgumentOutOfRangeExceptionnull/Nothingを許容しない場合にはArgumentNullException、その他不正な値であればArgumentExceptionをスローすることができます。

これらの例外をスローする場合は、例外コンストラクタの引数で例外メッセージを記述するとともに、引数paramNameに原因となったプロパティの名前を設定します。 また、ArgumentOutOfRangeExceptionでは引数actualValueに原因となった値を指定することができ、これによりエラー原因が把握しやすくなります。

プロパティで値を検証してArgumentExceptionをスローする例
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;
  }
}
プロパティで値を検証してArgumentExceptionをスローする例
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イベントを発生させ、データの表示を行うビューなどに変更が行われたことを通知することができます。

INotifyPropertyChangedの実装と使用例
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);
  }
}
INotifyPropertyChangedの実装と使用例
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__といったマクロに似た効果をもつものです。 この属性を使うことで、メソッドの呼び出し元ではプロパティ名を指定する必要がなくなり、プロパティ名を文字列で指定する手間と誤りの可能性を減らすことができます。

これを使うと上記のサンプルにおけるプロパティ名の指定箇所は次のように簡略化することができます。

CallerMemberNameAttributeを使ってプロパティ名の指定を省略する例
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));
  }
}
CallerMemberNameAttributeを使ってプロパティ名の指定を省略する例
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を使うことによってプロパティの値の比較・設定・イベントの発行の一連の処理を共通化できるため、さらに次のように簡略化することができます。

CallerMemberNameAttributeを使ってINotifyPropertyChangedの実装を簡略化した例
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));
    }
  }
}
CallerMemberNameAttributeを使ってINotifyPropertyChangedの実装を簡略化した例
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を実装しています。 コレクションへの通知を検知したい場合にはこのクラスを使うことができます。