1. はじめに
IEnumerable<T>だと既存のコレクションの中身をただ歩くだけだったけど、ICollection<T>インターフェースはコレクション管理の世界へのパスポートみたいなもんだよ。要素の追加・削除・数のチェック・配列へのコピー、さらには変更の監視までできる(リアルで買い物カゴの中身をコントロールしようとするのとちょっと似てるかも)。
ICollection<T>は.NETのすべてのミュータブルなコレクション、たとえばList<T>、HashSet<T>、Dictionary<TKey, TValue>.ValueCollection、そしてあまり人気ないけどQueue<T>やStack<T>(そう、キューやスタックも!)にも実装されてる。中身を変更できるコレクションクラスの基本契約だね。
コレクションインターフェースのファミリー
graph TD
A[IEnumerable<T>]
B[ICollection<T>]
C[IList<T>]
D[IDictionary<TKey, TValue>]
E[Queue<T>, Stack<T>, List<T>, HashSet<T>...]
A --> B
B --> C
B --> D
B --> E
2. ICollection<T>インターフェースに含まれるもの
IEnumerable<T>はイテレーション(foreach)だけだったけど、ICollection<T>はまるでスイスアーミーナイフみたいなインターフェース。どんなタスクにもメソッドがある!
主な中身はこれ:
public interface ICollection<T> : IEnumerable<T>
{
int Count { get; }
bool IsReadOnly { get; }
void Add(T item);
void Clear();
bool Contains(T item);
void CopyTo(T[] array, int arrayIndex);
bool Remove(T item);
}
メソッドとプロパティのリスト:
- Count — コレクション内の要素数。お気に入りのカウント関数みたいなやつ。
- IsReadOnly — コレクションを変更できるかどうか。trueなら読み取り専用(ラッパーとかでよくある)。
- Add(T item) — 要素をコレクションに追加。これがメインツール!
- Clear() — 全要素を削除。完全クリア。
- Contains(T item) — その要素がコレクションにあるかサクッとチェック。
- CopyTo(T[] array, int arrayIndex) — 指定インデックスから普通の配列にコピー。外部APIや「レガシー」なメソッドに渡すとき便利。
- Remove(T item) — 要素を削除(もしあれば)。
3. プログラマーのリアルな課題
このインターフェースの仕組みを理解するのがなぜ大事か?実際には、どんなコレクションでも動く汎用コードを書くことが多いから。たとえば、関数がリストでもキューでもセットでも受け取れるようにしたいとき、コレクションがICollection<T>を実装してれば、追加・削除・サイズチェック・配列へのコピーまで全部できる。柔軟なライブラリや汎用アルゴリズムの基礎だよ。
実生活の例: ICollection<T>インターフェースは、メソッドのパラメータやAPIのプロパティで「変更可能なコレクション」を返すときによく使われる。たとえば有名なORM(Entity FrameworkやDapper)では、関連オブジェクトのコレクションを表すプロパティにICollection<T>型がよく出てくるよ。
使い方の例
どんなコレクションでも受け取って要素を追加する汎用メソッドを書いてみよう。条件はICollection<T>を実装してることだけ。
// 要素追加の汎用メソッド
void AppendItem<T>(ICollection<T> collection, T item)
{
collection.Add(item);
}
このメソッドはList<T>にもHashSet<T>にも、独自実装のコレクションにも使える!使い方はこんな感じ:
var numbers = new List<int> { 1, 2, 3 };
AppendItem(numbers, 42);
var uniqueNames = new HashSet<string> { "Alice", "Bob" };
AppendItem(uniqueNames, "Charlie");
リストでもセットでも気にしない、ICollection<T>ならOK。実は.NETの汎用メソッドもこんな感じで作られてることが多いよ!
4. 他のコレクションとの関係
ICollection<T>は理論的に美しい契約だけじゃなく、.NETで出会うほぼすべてのコレクションのリアルな基盤でもある。
どれだけ汎用的か実感したいなら、次のコードを試してみて:
void ShowCollectionInfo<T>(ICollection<T> collection)
{
Console.WriteLine($"要素数: {collection.Count}");
Console.WriteLine($"変更できる: {!collection.IsReadOnly}");
Console.WriteLine("要素一覧:");
foreach (var item in collection)
{
Console.WriteLine(item);
}
}
// List<T>の場合
var list = new List<string> { "りんご", "オレンジ" };
ShowCollectionInfo(list);
// HashSet<T>の場合
var set = new HashSet<string> { "りんご", "オレンジ" };
ShowCollectionInfo(set);
この結果は、どんな型のコレクションでもICollection<T>を実装してれば同じように正しく動くよ。キューやスタック、ラップされた配列でも試してみて!
5. 読み取り専用コレクション
ICollection<T>インターフェースにはIsReadOnlyプロパティがあって、コレクションを変更できるかどうかを示してる。配列のプロパティと混同しないでね!もしIsReadOnly == trueなコレクションで追加や削除をしようとすると、例外が発生するよ。
例:
// 配列をReadOnlyCollectionでラップ
var array = new int[] { 1, 2, 3 };
ICollection<int> readOnly = Array.AsReadOnly(array);
// 要素を追加しようとする
try
{
readOnly.Add(4); // NotSupportedExceptionが投げられる
}
catch (NotSupportedException)
{
Console.WriteLine("要素を追加できない:読み取り専用コレクションだよ!");
}
これは、外部からもらったコレクションが変更不可なときによくあるパターン。要素の読み取りや数の取得はできるけど、変更はできない。
6. 人気コレクションのメソッド比較表(ICollection<T>視点)
| コレクション | Add() | Remove() | Contains() | Clear() | CopyTo() | IsReadOnly |
|---|---|---|---|---|---|---|
|
はい | はい | はい | はい | はい | いいえ |
|
はい* | はい | はい | はい | はい | いいえ |
|
いいえ/いいえ | いいえ/いいえ | いいえ/いいえ | はい/はい | はい/はい | いいえ |
|
いいえ | いいえ | はい | いいえ | はい | はい |
|
いいえ | いいえ | はい | いいえ | はい | はい/いいえ* |
HashSet<T>のAddメソッドは、要素が追加されたらtrue、すでにあったらfalseを返すよ。
7. CopyTo — どんなときに使う?
CopyToメソッドは、コレクションの中身を普通の配列に一気にコピーできる。たとえば、古いAPIが配列しか受け付けないときや、配列で一括処理したいときに便利。
var contactsArray = new Contact[book.Count];
((ICollection<Contact>)book).CopyTo(contactsArray, 0);
// これでcontactsArrayを昔ながらのやり方で使える
8. サンプル
学習用のコンソールアプリを作るとして、アドレス帳を開発し始めたとしよう。Contactクラスがあって、アドレス帳は追加・削除・イテレーションができればどんなコレクションでもOK。
public class Contact
{
public string Name { get; set; }
public string Phone { get; set; }
}
public class AddressBook
{
private readonly ICollection<Contact> _contacts;
public AddressBook(ICollection<Contact> contacts)
{
_contacts = contacts;
}
public void AddContact(Contact contact)
{
_contacts.Add(contact);
}
public bool RemoveContact(Contact contact)
{
return _contacts.Remove(contact);
}
public int Count => _contacts.Count;
public void PrintAll()
{
foreach (var contact in _contacts)
{
Console.WriteLine($"{contact.Name} — {contact.Phone}");
}
}
}
これでアドレス帳はList<Contact>でもHashSet<Contact>でも、将来的にはDBやファイルベースの独自コレクションでも動く!使い方例:
var book = new AddressBook(new List<Contact>());
book.AddContact(new Contact { Name = "イワン", Phone = "+7-123-456" });
book.AddContact(new Contact { Name = "マリア", Phone = "+7-987-654" });
Console.WriteLine($"全コンタクト数: {book.Count}");
book.PrintAll();
重複を避けたいなら、リストの代わりにセットをAddressBookに渡せばOK:
var book = new AddressBook(new HashSet<Contact>());
(Contactが正しく比較できるように実装されてる必要があるけど、それはまた別のレクチャーで!)
9. メソッドの特徴とよくあるミス
注意点:ICollection<T>を実装してるコレクションでも、挙動は同じじゃない!たとえばIsReadOnly == trueなコレクションを変更しようとすると例外が出るし、HashSet<T>のAddは、要素が新規ならtrue、すでにあればfalseを返す。あと、コレクションごとにメソッドの速度も違うけど、それはインターフェースでは保証されてない。
もうひとつよくあるのが、複数スレッドでコレクションを同期してる場合。あるスレッドでコレクションを変更して、別のスレッドでイテレーションすると例外が出ることがある。マルチスレッド用には専用コレクション(ConcurrentBag<T>、ConcurrentQueue<T>など)が必要(これはまた後で)。
もうひとつの落とし穴: CopyToでコレクションを配列にコピーしたら、その後は配列とコレクションは別物。配列を変えてもコレクションには影響しないし、その逆も同じだよ。
GO TO FULL VERSION