CodeGym /課程 /C# SELF /集合契約: ICollection<T><...

集合契約: ICollection<T>

C# SELF
等級 28, 課堂 2
開放

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
ICollection<T> 擴展了 IEnumerable<T>,也是大多數集合的基礎介面。

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
List<T>
沒有
HashSet<T>
有* 沒有
Queue<T> / Stack<T>
沒有/沒有 沒有/沒有 沒有/沒有 有/有 有/有 沒有
ReadOnlyCollection<T>
沒有 沒有 沒有
Dictionary<TKey, TValue>.ValueCollection
沒有 沒有 沒有 有/沒有*

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 複製集合,之後陣列和集合的變動就互不影響了。陣列是獨立的記憶體空間。

2
任務
C# SELF, 等級 28, 課堂 2
上鎖
使用 ICollection 建立與修改集合
使用 ICollection 建立與修改集合
留言
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION