1. 入门
你可能已经注意到了,C#里各种集合其实有点像。比如,几乎所有集合都能用foreach循环遍历:
var names = new List<string> { "安雅", "鲍里斯", "维卡" };
// 注意——这段代码对List、数组甚至HashSet都能用!
foreach (var name in names)
{
Console.WriteLine(name);
}
为啥会这样?其实就是因为所有现代集合都实现了接口IEnumerable<T>——这就是个“契约”,保证集合能一个一个地把元素吐出来,顺序来。
想象下公司里,不管你有啥员工集合(部门、项目组、聚会名单啥的),只要对象实现了"IEnumerable"接口,你就能按顺序遍历所有员工,根本不用管他们内部怎么存的,反正能遍历就行。
2. IEnumerable<T>接口——里面有啥?
来看看.NET里这个接口长啥样:
public interface IEnumerable<out T>
{
IEnumerator<T> GetEnumerator();
}
它就一个方法——GetEnumerator,返回一个IEnumerator<T>对象。这个对象负责“枚举”过程:它知道现在是哪个元素,怎么到下一个,啥时候结束。
简单说:只要一个类(或集合)实现了IEnumerable<T>,你就能用foreach遍历它,一个个拿元素——不用管它内部怎么实现的,哪怕是list、哈希表、甚至硬盘上的数据都行。
小图解
集合(比如List<T>)
|
v
IEnumerable<T>
|
v
IEnumerator<T>(真正“遍历”元素的家伙)
3. 实战和代码通用性
这种契约最大的好处就是通用性。如果一个函数或方法参数是IEnumerable<T>,那它就能兼容任何集合类型——数组、list、hashset、队列,甚至你自己写的集合都行!
比如,来个求和函数:
// 只要支持IEnumerable<int>,啥int集合都能求和
int 求和(IEnumerable<int> 数字们)
{
int 结果 = 0;
foreach (var n in 数字们)
结果 += n;
return 结果;
}
// 用List<int>也行
var list = new List<int> { 1, 2, 3 };
Console.WriteLine(求和(list));
// 用数组也没问题!
int[] array = { 4, 5, 6 };
Console.WriteLine(求和(array));
// 甚至可以用过滤后的结果
Console.WriteLine(求和(list.Where(x => x % 2 == 0))); // 用LINQ
生活中的例子:通用方法
比如你要写个工具,找出任意字符串集合里最长的单词。多亏有IEnumerable<string>,你可以对任何来源的数据搞定——数组、list、过滤结果啥的都行:
string 找最长(IEnumerable<string> 单词们)
{
string 最长 = "";
foreach (var word in 单词们)
if (word.Length > 最长.Length)
最长 = word;
return 最长;
}
用啥集合都能直接用。
4. 为啥foreach能用在IEnumerable<T>上?
常见问题:为啥foreach循环能“懂”各种集合?其实很简单:C#编译器会找类里的GetEnumerator方法,然后期望拿到一个有MoveNext()方法和Current属性的对象。这个就是标准接口——IEnumerator<T>。
所以你甚至可以自己写个类,能“一个个吐”元素(比如斐波那契数列生成器),只要它实现了IEnumerable<int>,就能像普通list一样用foreach遍历。
5. Enumerator是啥,它怎么工作的?
任何实现了IEnumerable<T>的集合内部,都藏着一个特殊的“遍历器”——Enumerator(学名:实现了IEnumerator<T>接口的对象)。就是它在你写foreach循环时,一个个把元素“搬”出来。
IEnumerator<T>接口
标准Enumerator能干这些:
public interface IEnumerator<T> : IDisposable
{
T Current { get; } // 当前元素
bool MoveNext(); // 移到下一个元素
void Reset(); // 回到开头(基本不用)
}
内部怎么工作的?
- MoveNext()——把“指针”移到下一个元素,如果有元素就返回true,没了就false。
- Current——拿到当前元素(遍历器现在指着的那个)。
- Reset()——把Enumerator重置到开头(但基本没人用)。
- Dispose()——释放资源(比如集合用到文件或网络时需要)。
例子:foreach“底层”其实干了啥
你写:
var numbers = new List<int> { 1, 2, 3 };
foreach (var n in numbers)
Console.WriteLine(n);
其实编译器会变成这样:
var numbers = new List<int> { 1, 2, 3 };
// 拿到“遍历器”
var enumerator = numbers.GetEnumerator();
while (enumerator.MoveNext())
{
var n = enumerator.Current;
Console.WriteLine(n);
}
// 编译器会自动在using块里调用Dispose()(如果enumerator实现了IDisposable)
注意:如果集合用到了外部资源(比如文件、数据库),Enumerator遍历完会自动释放它们。
可视化流程
开始遍历 -> GetEnumerator() -> Enumerator
|
v
MoveNext() -> Current
|
v
MoveNext() -> Current
|
...
|
v
MoveNext() == false -> 遍历结束
6. IEnumerable和数组、list、set:谁和谁是一家?
来看看.NET里哪些标准容器实现了这个接口:
| 集合类型 | 实现IEnumerable<T>? | 能用foreach遍历? |
|---|---|---|
数组() |
✅ | ✅ |
|
✅ | ✅ |
|
✅(遍历键值对、key、value) | ✅ |
|
✅ | ✅ |
|
✅ | ✅ |
|
✅ | ✅ |
甚至字符串(string)也实现了普通的IEnumerable,所以你也能遍历字符串的每个字符。
7. 自己实现一个Enumerable
有意思的任务:咱们来写个最简单的集合,能存0到N之间的所有偶数,并实现IEnumerable<int>。这样就能用foreach遍历,也能配合LINQ用。
// 能“遍历”的集合类
class 偶数们 : IEnumerable<int>
{
private int 最大值;
public 偶数们(int 最大值)
{
this.最大值 = 最大值;
}
public IEnumerator<int> GetEnumerator()
{
for (int i = 0; i <= 最大值; i += 2)
yield return i; // 这就是实现enumerator的魔法
}
// 显式实现非泛型IEnumerable接口,兼容老代码
System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
}
// 用法:
var evens = new 偶数们(10);
foreach(var e in evens)
Console.Write($"{e} "); // 0 2 4 6 8 10
注意重点:只要你的类实现了IEnumerable<T>——它就能自动兼容.NET大部分工具:LINQ、foreach、各种接收enumerable的方法。
8. 常见坑和细节
有些新手会以为IEnumerable<T>是某种自带元素的集合。其实它只是个“承诺”,意思是只要你开始遍历,它就会一个个“吐”元素出来。
如果你想按索引随机访问(myList[5]),或者用Add、Remove这些方法——IEnumerable<T>就帮不上忙了。它只管顺序遍历!
常见错误:遍历时修改集合。比如:
foreach (var item in 我的列表)
{
if (item < 0)
我的列表.Remove(item); // 危险!InvalidOperationException
}
更安全的做法是先单独建个要删的列表,或者用能生成新集合的方法:
// 安全做法——手动建新集合
var 新列表 = new List<int>();
foreach (var item in 我的列表)
{
if (item >= 0)
新列表.Add(item);
}
我的列表 = 新列表;
// 或者倒着按索引删
for (int i = 我的列表.Count - 1; i >= 0; i--)
{
if (我的列表[i] < 0)
我的列表.RemoveAt(i);
}
GO TO FULL VERSION