ここでは.NET Frameworkにおけるプロパティと、プロパティの実装に関する事項・注意点などについて解説します。 またインデクサやインデックス付きプロパティについても解説します。

§1 プロパティ

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);
  }
}
実行結果
3

プロパティではこのようにアクセサメソッドを使ってフィールドに対する値の取得・設定を行います。

実装を持たないインターフェイスでは、プロパティを以下のように記述します。

インターフェイスにプロパティを持たせる例
interface IAccount {
  // プロパティ
  int ID { get; set; }
}

§1.1 アクセサメソッド

.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);
    }
  }
}
実行結果
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);
  }
}

また、アクセサメソッドと同じシグネチャのメソッドが存在する場合もコンパイルエラーとなります。 逆に、シグネチャが異なっていればアクセサメソッドと同名のメソッドを作成することはできます。

アクセサメソッドと同名のメソッド
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)
  {
  }
}

§1.2 読み取り専用・書き込み専用・アクセシビリティ

.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;
  }
}

派生クラスのみに公開する場合などを除けば、書き込み専用プロパティを外部に公開することはまれです。 こういった場合はプロパティではなくSetXXXといったメソッドを提供するほうが自然です。

書き込み専用プロパティと、設定を行うメソッド
using System;

class Account {
  /*
   * 書き込み専用プロパティ
  public int ID {
    set {
      _id = value;
    }
  }
  */

  // フィールドに値を設定するメソッド
  public void SetID(int newid)
  {
    _id = newid;
  }

  private int _id;
}

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

アクセサごとに異なるアクセシビリティを指定する
using System;

class Account {
  public int ID {
    // getアクセサはpublic
    get {
      return _id;
    }
    // setアクセサはprotected
    // (派生クラスからのみsetできる)
    protected set {
      _id = value;
    }
  }

  private int _id;
}


§2 自動実装

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);
  }
}
実行結果
3

プロパティの自動実装はC# 3.0、VB2010以降でサポートされています。

§2.1 バッキングフィールド

プロパティの自動実装を行う場合、プロパティの値を保持するフィールド(バッキングフィールド)も自動的に作成されます。 このフィールドはリフレクションによって調べることができます。

バッキングフィールド名の表示
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

このようにC#とVBでは生成されるバッキングフィールド名が異なり、C#では<プロパティ名>k__BackingField、VBでは_プロパティ名となるようです。

§2.2 自動実装プロパティの初期値

C# 6.0以降、VB2010以降では自動実装プロパティに初期値を与えることができます。

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

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;
  }
}

§3 インデクサ

インデクサ(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]);
  }
}
実行結果
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;
  }
}

実装上はインデクサもプロパティの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]);
    }
  }
}
実行結果
2
3
4

§3.1 インデックス付きプロパティとインデクサの名前

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))

§4 コレクションを返すプロパティ

インデクサはコレクションやそれに類する機能を持つクラスで実装すべきもので、多くの場合はインデクサよりも単に配列や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"};
  }
}

さらに、公開されるコレクション自体も参照専用としたい(コレクションの内容を変更させたくない)場合は、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");
  }
}

その他コレクションクラスおよびインターフェイスについてはコレクションの種類と特徴、読み取り専用コレクションについては汎用ジェネリックコレクション(1) Collection/ReadOnlyCollection §.ReadOnlyCollectionを参照してください。

§4.1 イテレータ

プロパティにおいてもイテレータ構文を使用することができます。 これにより、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);
    }
  }
}
実行結果
0
1
2
3
4

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

§5 プロパティと例外

プロパティではアクセサメソッドを使ってフィールドの値を取得・設定するため、その際に値の検証を行う処理を記述することができます。 また、検証した結果として例外をスローすることもできます。 例えば、設定される値がプロパティとして有効な値の範囲外だった場合には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;
  }
}
実行結果
ハンドルされていない例外: 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);
  }
}
実行結果
120

一般に、プロパティでは単に値の取得・設定のみを行うべきで、それ以上の副作用が起こることは避けるべきです。 例えば上記の例においては、設定した値とその後に取得される値が異なることから、実装を知らずに結果だけを見ると意図した動作と異なるような違和感を覚える場合もあります。 この他にも、プロパティを設定することがインスタンス内の他のメンバに影響するような実装(一つのプロパティで複数のフィールドを変更するなど)は避けるべきです。

また例外に関しても、プロパティから以下に挙げるようなもの以外の例外をスローする場合にはメソッドとして実装したほうがよいとされます。 プロパティからスローされることが想定(あるいは許容)される例外と状況の主なものとしては次のようなものがあります。

ArgumentOutOfRangeException, ArgumentNullException, ArgumentException
プロパティに設定される値としては不正な場合
InvalidEnumArgumentException
プロパティに設定される列挙体の値が不正な場合
IndexOutOfRangeException
インデクサに指定されるインデックスが範囲内の場合の場合
InvalidOperationException
現在のインスタンスの状態ではプロパティの表す機能を要求できない場合 (例えば、処理の進行中にその処理に影響するプロパティを変更しようとするなど)
ObjectDisposedException
インスタンスが破棄された後にプロパティにアクセスしようとした場合 (オブジェクトの破棄 §.解放されたリソースへのアクセス拒否 (ObjectDisposedException))
NotSupportedException
インスタンスがプロパティの表す機能をサポートしていない場合 (例えば、読み取り専用として作成したインスタンスに対するプロパティの設定など)
NotImplementedException
プロパティの機能が未実装の場合

これ以外の例外をスローする必要がある場合は、プロパティよりメソッドとして公開するほうが望ましいかもしれません。

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

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));
    }
  }
}

この例で使用しているEqualityComparerおよびIEqualityComparerについては等価性の定義と比較を参照してください。

なお、ObservableCollectionクラスはINotifyPropertyChangedを実装しています。 コレクションへの通知を検知したい場合にはこのクラスを使うことができます。