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
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 |
|---|---|---|---|---|---|---|
|
可以 | 可以 | 可以 | 可以 | 可以 | 不可以 |
|
可以* | 可以 | 可以 | 可以 | 可以 | 不可以 |
|
不行/不行 | 不行/不行 | 不行/不行 | 可以/可以 | 可以/可以 | 不可以 |
|
不行 | 不行 | 可以 | 不行 | 可以 | 可以 |
|
不行 | 不行 | 可以 | 不行 | 可以 | 可以/不可以* |
对于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拷贝了集合,后面数组和集合各自改互不影响。数组就是独立的一块内存了。
GO TO FULL VERSION