KeyedCollection

System.Collections.ObjectModel.KeyedCollectionクラスは、DictionaryクラスSortedListクラスによく似た抽象クラスです。 KeyedCollectionクラスではキーによるアクセスとインデックスによるアクセスの両方が出来るようになっています。

Dictionaryクラス・SortedListクラスがキー・値のペアで要素を格納するのに対し、KeyedCollectionクラスでは値のみを格納しキーは値自体から取得される(値が持つ構成要素の一つをキーとして使う)という点で異なります。 例えば、IDと表示名から構成されるアカウントを一つの値と考えたとき、IDをキーとして使用してKeyedCollectionクラスに格納するといったことが出来ます。

KeyedCollectionクラスは抽象クラスで、継承する場合は値からキーを取得するためのプロテクトメソッドGetKeyForItemを実装する必要があります。 以下は、IDと表示名から構成されるアカウントを表すクラスAccountを作成し、それを格納するためのコレクションを実装した例です。

using System;
using System.Collections.ObjectModel;

class Account {
  public string Id;
  public string DisplayName;

  public Account(string id, string displayName)
  {
    this.Id = id;
    this.DisplayName = displayName;
  }

  public override string ToString()
  {
    return string.Format("{0} ({1})", DisplayName, Id);
  }
}

// Accountクラスを格納するためのKeyedCollection
class AccountCollection : KeyedCollection<string, Account> {
  protected override string GetKeyForItem(Account item)
  {
    // Idフィールドをキーとして使用する
    return item.Id;
  }
}

class Sample {
  static void Main()
  {
    AccountCollection col = new AccountCollection();

    col.Add(new Account("0alice", "Alice"));
    col.Add(new Account("1bob", "Bob"));
    col.Add(new Account("2charlie", "Charlie"));
    col.Add(new Account("3dave", "Dave"));
    col.Add(new Account("4eve", "Eve"));

    Console.WriteLine(col["0alice"]);
    Console.WriteLine("Contains '3dave': {0}", col.Contains("3dave"));

    Console.WriteLine("for");

    for (int i = 0; i < col.Count; i++) {
      Console.WriteLine("col[{0}] = {1}", i, col[i]);
    }

    Console.WriteLine("foreach");

    foreach (Account e in col) {
      Console.WriteLine("col[{0}] = {1}", col.IndexOf(e), e);
    }
  }
}
Imports System
Imports System.Collections.ObjectModel

Class Account
  Public Id As String
  Public DisplayName As String

  Public Sub New(ByVal id As String, ByVal displayName As String)
    MyClass.Id = id
    MyClass.DisplayName = displayName
  End Sub

  Public Overrides Function ToString() As String
    Return string.Format("{0} ({1})", DisplayName, Id)
  End Function
End Class

' Accountクラスを格納するためのKeyedCollection
Class AccountCollection
  Inherits KeyedCollection(Of String, Account)

  Protected Overrides Function GetKeyForItem(ByVal item As Account) As String
    ' Idフィールドをキーとして使用する
    Return item.Id
  End Function
End Class

Class Sample
  Shared Sub Main()
    Dim col As New AccountCollection()

    col.Add(New Account("0alice", "Alice"))
    col.Add(New Account("1bob", "Bob"))
    col.Add(New Account("2charlie", "Charlie"))
    col.Add(New Account("3dave", "Dave"))
    col.Add(New Account("4eve", "Eve"))

    Console.WriteLine(col("0alice"))
    Console.WriteLine("Contains '3dave': {0}", col.Contains("3dave"))

    Console.WriteLine("For")

    For i As Integer = 0 To col.Count - 1
      Console.WriteLine("col[{0}] = {1}", i, col(i))
    Next

    Console.WriteLine("For Each")

    For Each e As Account In col
      Console.WriteLine("col[{0}] = {1}", col.IndexOf(e), e)
    Next
  End Sub
End Class
実行結果
Alice (0alice)
Contains '3dave': True
col[0] = Alice (0alice)
col[1] = Bob (1bob)
col[2] = Charlie (2charlie)
col[3] = Dave (3dave)
col[4] = Eve (4eve)

例で挙げたとおり、for文とインデックスを指定した列挙と、foreach文を用いた列挙の両方が行えます。 foreach文で列挙した場合はDictionaryやSortedListとは異なり、個々の要素はKeyValuePair<TKey, TItem>ではなく、型パラメータTItemで指定した型で列挙されます。

派生クラスからは、Dictionaryプロパティを参照することで内部ディクショナリにアクセスすることが出来ます。 また、KeyedCollectionクラス自体はCollectionクラスを継承しているため、SetItem, InsertItem, RemoveItem, ClearItemの各メソッドをオーバーライドすることで要素の挿入・削除・設定時の動作を拡張できます。 さらに、DictionaryやSortedList同様、コンストラクタで適切なIEqualityComparerインターフェイスを指定することで、KeyedCollectionのキー比較時の動作をカスタマイズ出来ます。 内部ディクショナリに追加した要素のキーを後から変更するには、ChangeItemKeyプロテクトメソッドを呼び出します。

なお、キーの型をintにすることも出来ますが、その場合はインデックスによる指定なのかキーによる指定なのか曖昧になるため注意が必要です。 キーの型をintにした場合、インデクサ(もしくはItemプロパティ)に指定した値は常にキーとして扱われます。 インデックスとして指定したい場合は、いったん基本型であるCollectionクラスにキャストする必要があります。 以下は、先の例におけるAccountCollectionクラスのキーの型をintに変更し、キーとインデックスの両方で列挙する例です。

using System;
using System.Collections.ObjectModel;

class Account {
  public int Id;
  public string DisplayName;

  public Account(int id, string displayName)
  {
    this.Id = id;
    this.DisplayName = displayName;
  }

  public override string ToString()
  {
    return string.Format("{0} ({1})", DisplayName, Id);
  }
}

// Accountクラスを格納するためのKeyedCollection
class AccountCollection : KeyedCollection<int, Account> {
  protected override int GetKeyForItem(Account item)
  {
    // Idフィールドをキーとして使用する
    return item.Id;
  }
}

class Sample {
  static void Main()
  {
    AccountCollection col = new AccountCollection();

    col.Add(new Account(1, "Alice"));
    col.Add(new Account(2, "Bob"));
    col.Add(new Account(0, "Charlie"));
    col.Add(new Account(4, "Dave"));
    col.Add(new Account(3, "Eve"));

    // キーを指定して列挙
    Console.WriteLine("by key");

    for (int i = 0; i < col.Count; i++) {
      Console.WriteLine("col[{0}] = {1}", i, col[i]);
    }

    // インデックスを指定して列挙
    Console.WriteLine("by index");

    Collection<Account> c = col; // 基底クラスにキャストする

    for (int i = 0; i < c.Count; i++) {
      Console.WriteLine("col[{0}] = {1}", i, c[i]);
    }
  }
}
Imports System
Imports System.Collections.ObjectModel

Class Account
  Public Id As Integer
  Public DisplayName As String

  Public Sub New(ByVal id As Integer, ByVal displayName As String)
    MyClass.Id = id
    MyClass.DisplayName = displayName
  End Sub

  Public Overrides Function ToString() As String
    Return string.Format("{0} ({1})", DisplayName, Id)
  End Function
End Class

' Accountクラスを格納するためのKeyedCollection
Class AccountCollection
  Inherits KeyedCollection(Of Integer, Account)

  Protected Overrides Function GetKeyForItem(ByVal item As Account) As Integer
    ' Idフィールドをキーとして使用する
    Return item.Id
  End Function
End Class

Class Sample
  Shared Sub Main()
    Dim col As New AccountCollection()

    col.Add(New Account(1, "Alice"))
    col.Add(New Account(2, "Bob"))
    col.Add(New Account(0, "Charlie"))
    col.Add(New Account(4, "Dave"))
    col.Add(New Account(3, "Eve"))

    ' キーを指定して列挙
    Console.WriteLine("by key")

    For i As Integer = 0 To col.Count - 1
      Console.WriteLine("col[{0}] = {1}", i, col(i))
    Next

    ' インデックスを指定して列挙
    Console.WriteLine("by index")

    Dim c As Collection(Of Account) = col ' 基底クラスにキャストする

    For i As Integer = 0 To c.Count - 1
      Console.WriteLine("col[{0}] = {1}", i, c(i))
    Next
  End Sub
End Class
実行結果
by key
col[0] = Charlie (0)
col[1] = Alice (1)
col[2] = Bob (2)
col[3] = Eve (3)
col[4] = Dave (4)
by index
col[0] = Alice (1)
col[1] = Bob (2)
col[2] = Charlie (0)
col[3] = Dave (4)
col[4] = Eve (3)