§1 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);
    }
  }
}
Copyright© 2016 . Released under the WTFPL version 2.
実行結果
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]);
    }
  }
}
Copyright© 2016 . Released under the WTFPL version 2.
実行結果
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)