CodeGym /课程 /C# SELF /集合契约: ICollection<T><...

集合契约: ICollection<T>

C# SELF
第 28 级 , 课程 2
可用

1. 入门

如果说IEnumerable<T>只能让你遍历集合里已经有的内容,那ICollection<T>就是你管理集合的通行证:可以添加、删除、查数量、拷贝到数组,甚至还能监控变化(就像你现实生活里想管住购物篮里的东西一样,虽然不一定能管住)。

ICollection<T>被所有可变集合实现,比如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
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) —— 把元素拷贝到普通数组,从指定下标开始。给老API或者“老古董”方法传数据很有用。
  • Remove(T item) —— 删掉元素(如果有的话)。

3. 程序员的实际需求

为啥要懂这个接口怎么用?实际开发里你经常写通用代码,能处理各种集合。比如一个函数可以接收任何list、queue或者set——具体啥类型无所谓。只要实现了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");

是不是很神奇——我们根本不关心是list还是set,只要是ICollection<T>就行。别惊讶,.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);

不管啥类型的集合,只要实现了这个接口,结果都一样靠谱。你可以试试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. 示例

假设我们在写一个控制台通讯录小程序。有个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();

如果想去重,只要把list换成set传给AddressBook就行:


var book = new AddressBook(new HashSet<Contact>());

(前提是Contact要正确实现比较,这个以后会专门讲!)

9. 方法细节和常见坑

注意:不是所有实现ICollection<T>的集合行为都一样!比如IsReadOnly == true时想改集合会抛异常。用HashSet<T>时,Add只有元素真加进去了才返回true(之前没有才行)。而且不同集合方法实现的速度也不一样——接口没规定。

还有个常见问题:如果集合被多个线程同时用,一个线程改集合另一个线程遍历,可能会抛异常。多线程场景要用专门的集合(ConcurrentBag<T>ConcurrentQueue<T>等——以后会讲)。

还有个坑:如果你用CopyTo拷贝了集合,后面数组和集合各自改互不影响。数组就是独立的一块内存了。

评论
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION