CodeGym /课程 /C# SELF /可枚举性契约: IEnumerable<T>...

可枚举性契约: IEnumerable<T>

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

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遍历?
数组(
int[]
List<T>
Dictionary<TKey, V>
✅(遍历键值对、key、value)
HashSet<T>
Queue<T>
Stack<T>

甚至字符串(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]),或者用AddRemove这些方法——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);
}
评论
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION