1. 集合为啥要用接口?
说实话:当我们用具体类型写代码的时候(List<int>、Dictionary<string, string>),一切都很简单明了。但你想象下,突然你想把List<T>换成HashSet<T>,因为你只需要唯一值。或者你想写个方法,能同时处理这两种集合类型。咋办?
你得有个统一的“接口”——C#里这个“接口”就是接口(interface)。它规定了一套规则(方法、属性),集合必须有这些东西。只要实现了接口——就能支持这些操作。
就像手机充电:有了标准的USB-C接口,你能给手机充电,也能给现代吸尘器、甚至电动牙刷充电(就是别在黑灯瞎火的时候搞混了)。
实际例子
比如你想写个方法,算任意数字集合的和。如果不用接口,大概是这样:
// 这个方法只能处理数组:
static int SumArray(int[] array)
{
int sum = 0;
for (int i = 0; i < array.Length; i++)
{
sum += array[i];
}
return sum;
}
// 这个只能处理List<int>:
static int SumList(List<int> list)
{
int sum = 0;
for (int i = 0; i < list.Count; i++)
sum += list[i];
return sum;
}
很不方便吧!其实代码几乎一样。用IEnumerable<int>接口就能写成通用的:
static int SumAll(IEnumerable<int> collection)
{
int sum = 0;
foreach (var number in collection)
sum += number;
return sum;
}
现在这个方法能接收任何实现了IEnumerable<int>的集合类型:数组、列表、集合啥的都行。
2. 集合的主要接口
.NET里集合有几个核心接口。咱们先简单过一遍——后面课程会细讲每个的实现细节。
| 接口 | 作用 | 实现该接口的集合例子 |
|---|---|---|
|
遍历元素(foreach) | 所有:、、数组 |
|
可变集合(Add、Remove、Count) | 、、 |
|
按索引访问、修改 | 、数组 |
|
处理“键-值”对 | 、 |
|
集合(唯一元素) | 、 |
P.S. 为了简单起见,暂时不讲这些接口之间的“多层继承”关系。谁继承谁——后面再说。
可视化:集合接口树
IEnumerable<T>
^
|
ICollection<T>
^ ^
| |
IList<T> ISet<T>
| |
List<T> HashSet<T>
对于字典:
IEnumerable<KeyValuePair<K,V>>
^
|
ICollection<KeyValuePair<K,V>>
^
|
IDictionary<K,V>
|
Dictionary<K,V>
3. 现实中接口有啥用?
灵活性
当你的方法或类操作的不是具体类型(List<int>),而是接口(IEnumerable<int>),你就能传入任何实现了这个接口的集合。这样程序就变得灵活、可扩展。(不再是“只能用iPhone”——现在只要有蓝牙的手机都行。)
static void PrintAll(IEnumerable<string> collection)
{
foreach (var item in collection)
Console.WriteLine(item);
}
现在你可以打印任何列表、数组、集合,甚至LINQ查询的结果!
统一
如果你实现了自己的集合类型(比如某个很特殊的集合),只要实现了需要的接口——你的集合就能和别人的代码配合,哪怕他们根本不知道你的集合类型。这也是为啥.NET里常说:“面向接口编程,而不是面向具体实现”。
兼容标准工具
支持标准接口后,你的集合能和LINQ、排序、序列化等.NET功能无缝配合。
4. List<T>——既是列表、又是集合、还是可遍历的
关于遍历的几句话
IEnumerable<T>是.NET里最重要、最基础的集合接口。它只定义了一个方法——GetEnumerator(),返回一个枚举器(Enumerator),用来遍历集合。正因为有它,你才能写:
foreach (var name in names)
{
Console.WriteLine(name);
}
这里names可以是列表、集合,甚至数组。foreach其实是语法糖,底层就是用GetEnumerator()方法。
IEnumerable(没有<T>)是更老的、非泛型的版本。现在基本都用泛型的。
List类的声明语法
来看看List<T>类的声明,稍微揭秘下它的魔法:
public class List<T> : IList<T>, ICollection<T>, IEnumerable<T>, ...
这意味着List<T>对象可以当作:
- 支持索引访问的列表(IList<T>)
- 能增删元素的集合(ICollection<T>)
- 可以遍历的元素集合(IEnumerable<T>)
例子:
List<string> names = new List<string> { "安娜", "鲍里斯", "瓦夏" };
// 作为IEnumerable - 可以遍历
IEnumerable<string> asEnumerable = names;
foreach (var n in asEnumerable) Console.WriteLine(n);
// 作为ICollection - 可以获取Count:
ICollection<string> asCollection = names;
int count = asCollection.Count;
// 作为IList - 可以按索引访问:
IList<string> asList = names;
string firstItem = asList[0];
5. 应用实例
比如我们有个简单的用户和角色管理应用(上节课刚开始写的):
// 用户类
public class User
{
public string Name { get; set; }
public int Age { get; set; }
}
我们可以把用户存在任何实现了IEnumerable<User>的集合里:
List<User> listUsers = new List<User> { new User { Name = "安娜", Age = 20 } };
HashSet<User> setUsers = new HashSet<User> { new User { Name = "鲍里斯", Age = 25 } };
// 输出所有用户名字的方法
static void PrintUserNames(IEnumerable<User> users)
{
foreach (var user in users)
Console.WriteLine(user.Name);
}
// 用不同类型的集合调用:
PrintUserNames(listUsers);
PrintUserNames(setUsers);
现在应用变得超级灵活——不用魔法,也不用写一堆重载!
6. 常见错误和坑
最常见的错误之一——用太具体的类型,其实用接口就够了:
// 不好(死死绑在实现上)
void DoSomethingWithList(List<int> numbers) { ... }
// 更好(方法能处理任何集合)
void DoSomethingWithNumbers(IEnumerable<int> numbers) { ... }
还有个坑——别忘了接口只规定了它声明的那些操作。比如IEnumerable<T>没法获取元素数量(Count),你得用ICollection<T>,或者自己数一遍。
GO TO FULL VERSION