1. 前言
如果你用 IEnumerable<T> 只能在集合裡面走來走去,那 ICollection<T> 就是你進入集合管理世界的門票啦:可以加、可以刪、可以查數量、可以複製到陣列,甚至還能監控變化(就像你想掌控購物車內容一樣,雖然現實常常失控XD)。
ICollection<T> 幾乎被所有 .NET 可變集合實作,像 List<T>、HashSet<T>、Dictionary<TKey, TValue>.ValueCollection,甚至比較冷門的 Queue<T>、Stack<T>(沒錯,連 queue 跟 stack 也有!)。這是所有允許你改內容的集合類別的基礎契約。
集合介面家族
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) — 把元素複製到一般陣列,從指定 index 開始。很適合丟給舊 API 或「古早」方法。
- Remove(T item) — 刪掉一個元素(如果有的話)。
3. 程式設計師的日常任務
為什麼要懂這個介面怎麼運作?實務上你常常會寫泛用程式碼,能處理任何集合。比如一個 function 可以吃 list、queue 或 set —— 不管傳進來的是哪種集合,只要它有實作 ICollection<T>,你就能加、刪、查數量,甚至複製到陣列。這是寫彈性 library 跟通用演算法的基礎。
生活範例: ICollection<T> 常常出現在 method 參數或 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");
超方便——我們根本不在乎是 list 還是 set,只要是 ICollection<T> 就行。其實 .NET 裡很多通用方法都是這樣設計的!
4. 跟其他集合的關係
ICollection<T> 不只是理論上很美的契約,實際上也是你在 .NET 會遇到幾乎所有集合的基礎。
想看它有多通用,來試試下面這段 code:
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);
不管你傳什麼型別的集合,只要有實作這個介面,結果都會正確。你可以試試 queue、stack,甚至包裝過的陣列!
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. 範例
假設我們在寫一個學習用的 console 應用程式,要做一個通訊錄。假設有個 Contact 類別,通訊錄可以用任何支援加、刪、遍歷的集合來實作。
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>,甚至未來你自己寫的資料庫或檔案集合都能用!用法如下:
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();
如果你想去除重複,只要把 set 傳給 AddressBook 就好:
var book = new AddressBook(new HashSet<Contact>());
(前提是 Contact 要正確實作比較,這個以後會再講!)
9. 方法特性與常見錯誤
先提醒一下:不是所有實作 ICollection<T> 的集合行為都一樣!比如 IsReadOnly == true 時想改集合會丟例外。用 HashSet<T> 時,Add 只有真的加進去才會回傳 true(之前沒有才會加)。而且不同集合方法效能也不一樣——但介面沒規定這些細節。
還有一個常見狀況:如果多執行緒同時操作集合,一個 thread 改集合、另一個 thread 遍歷,可能會丟例外。多執行緒要用專門的集合(ConcurrentBag<T>、ConcurrentQueue<T> 等等——這以後會講)。
再一個陷阱: 如果你用 CopyTo 複製集合,之後陣列和集合的變動就互不影響了。陣列是獨立的記憶體空間。
GO TO FULL VERSION