在C#中,`IEnumerable<T>` 是一个定义于 `System.Collections.Generic` 命名空间中的接口,它表示可以枚举的一系列元素的集合。`IEnumerable<T>` 接口是许多集合类(如 `List<T>`, `Dictionary<TKey, TValue>`, `Array` 等)的基础,并且它是实现迭代器模式的关键接口之一。
### 主要特性
- **GetEnumerator()**:这是 `IEnumerable<T>` 接口中唯一的方法。它返回一个实现了 `IEnumerator<T>` 接口的对象,该对象用于遍历集合。每次调用 `GetEnumerator()` 都会返回一个新的枚举器,这意味着你可以同时拥有多个独立的枚举器来遍历同一个集合。
```csharp
public IEnumerator<T> GetEnumerator();
```
- **非泛型版本**:还有一个非泛型版本的 `IEnumerable` 接口,它存在于 `System.Collections` 命名空间中。这个接口也只有一个 `GetEnumerator` 方法,但它返回的是 `IEnumerator`,而不是 `IEnumerator<T>`。当处理的对象类型不是已知的时候,或者你需要与旧版代码兼容时,可能会用到非泛型版本。
```csharp
public IEnumerator GetEnumerator();
```
### 使用场景
`IEnumerable<T>` 的主要用途是在 foreach 循环中遍历集合,因为 C# 编译器会在编译期间将 foreach 循环转换为对 `GetEnumerator()`、`MoveNext()` 和 `Current` 属性的调用。
```csharp
var numbers = new List<int> { 1, 2, 3, 4, 5 };
foreach (int number in numbers)
{
Console.WriteLine(number);
}
```
上述代码实际上会被编译器转换成类似下面的形式:
```csharp
var enumerator = numbers.GetEnumerator();
while (enumerator.MoveNext())
{
int number = enumerator.Current;
Console.WriteLine(number);
}
```
### 惰性求值和延迟执行
当一个方法返回 `IEnumerable<T>` 类型的结果时,通常意味着该方法支持惰性求值(Lazy Evaluation)。这意味着实际的数据获取或计算只会在你开始遍历序列时发生,而且只会为当前访问的元素进行。这对于处理大数据集或流式数据非常有用,因为它可以节省内存并提高性能。
例如,LINQ 查询表达式经常返回 `IEnumerable<T>`,并且只有当你开始枚举结果时才会执行查询。
```csharp
var query = from n in numbers
where n > 3
select n;
// 此时,query 还没有执行任何操作
// 只有当我们遍历 query 时,查询才会被执行
foreach (var item in query)
{
Console.WriteLine(item); // 输出: 4, 5
}
```
### 注意事项
- 如果你在遍历 `IEnumerable<T>` 时修改了底层集合,可能会抛出 `InvalidOperationException` 异常。这是因为大多数集合在检测到其结构被改变时会禁止继续枚举。
- `IEnumerable<T>` 不保证线程安全。如果你需要在多线程环境中使用,你可能需要额外的同步逻辑。
- 枚举器是一次性的,一旦完成遍历后,就不能再次使用同一个枚举器来重新遍历集合。如果需要多次遍历,必须通过 `GetEnumerator()` 获取新的枚举器。
### 实现自己的 IEnumerable<T>
如果你想创建一个自定义的集合类,并使其能够支持 `foreach` 循环,那么你需要实现 `IEnumerable<T>` 接口,并提供一个合适的枚举器。这通常涉及到定义一个内部类来实现 `IEnumerator<T>` 接口,或者利用 C# 的迭代器语法(yield return/yield break),它可以大大简化实现过程。