目录
前言少叙
也许一切该从 foreach 说起。使用 foreach 遍历一个集合,在应用程序中是不可或缺的。
在 C# 中,可以使用 foreach 的基本类包括(但不限于):字符串(String)、数组(Array)、列表(List)、字典(Dictionary<TKey, TValue>)、队列(Queue)、堆栈(Stack)、集合(HashSet)、链表(LinkedList)等。
这些类之所以可以使用 foreach,是因为它们实现了 IEnumerable 接口,也被称为可枚举对象。IEnumerable 接口中的方法 IEnumerator GetEnumerator(); 返回一个枚举器。这一过程符合设计模式中的行为型——迭代器模式,使用枚举器和可枚举对象的组合实现集合的迭代。因此,迭代器可以看作是枚举器和可枚举对象的组合。
本文将详细介绍上述内容,并提供贴合实际应用的示例代码。
需要注意的是,IEnumerator 和 IEnumerable 是 IEnumerator 和 IEnumerable 的泛型版本,以下内容中不严格区分它们。
1. IEnumerator
与 枚举器
IEnumerator
是一个接口,它表示一个集合的枚举器。
public interface IEnumerator
{
bool MoveNext();
Object Current { get; }
void Reset();
}
它提供两个方法和一个只读属性。
bool MoveNext()
: 将枚举器推进到集合的下一个元素。如果还有更多的元素,返回true
,否则返回false
。void Reset()
: 将枚举器的位置重置到集合的开始位置。object Current
: 获取当前元素。
工作原理
- 初始状态:枚举器在集合的第一个元素之前。调用
MoveNext()
将枚举器移动到集合的第一个元素。 - 迭代过程:每次调用
MoveNext()
,枚举器都会前进到下一个元素。如果到达集合的末尾,MoveNext()
返回false
。 - 访问元素:通过
Current
属性访问当前元素。需要注意的是,在调用MoveNext()
之前或在MoveNext()
返回false
之后访问Current
会引发异常。 - 重置:调用
Reset()
将枚举器重新设置到集合的第一个元素之前。
2. IEnumerable
与 可枚举对象
IEnumerable
是一个接口,它表示一个集合可以枚举。换句话说,如果一个类实现了IEnumerable
接口,那么它就可以被迭代,也就是可枚举对象。
public interface IEnumerable
{
IEnumerator GetEnumerator();
}
这个方法返回一个IEnumerator
对象,用于实际的迭代操作。
3. 使用IEnumerator<in T>、IEnumerable<in T>
实现正向或反向遍历的迭代器
如下代码BidirectionalEnumerator
枚举器决定怎样枚举,BidirectionalEnumerable
决定怎样使用枚举器
using System;
using System.Collections;
using System.Collections.Generic;
public class BidirectionalEnumerable<T> : IEnumerable<T>
{
private T[] items; // 存储元素的数组
private bool reverse; // 是否反向遍历
public BidirectionalEnumerable(T[] items, bool reverse = false)
{
this.items = items; // 初始化数组
this.reverse = reverse; // 初始化遍历方向
}
// 非泛型版本的GetEnumerator方法
public IEnumerator GetEnumerator()
{
return new BidirectionalEnumerator(items, reverse);
}
// 泛型版本的GetEnumerator方法
IEnumerator<T> IEnumerable<T>.GetEnumerator()
{
return new BidirectionalEnumerator(items, reverse);
}
// 内部类,实现了IEnumerator<T>接口
private class BidirectionalEnumerator : IEnumerator<T>
{
private T[] items; // 存储元素的数组
private int currentIndex; // 当前索引
private bool reverse; // 是否反向遍历
public BidirectionalEnumerator(T[] items, bool reverse)
{
this.items = items; // 初始化数组
this.reverse = reverse; // 初始化遍历方向
this.currentIndex = reverse ? items.Length : -1; // 根据遍历方向初始化索引
}
// 移动到下一个元素
public bool MoveNext()
{
if (reverse)
{
currentIndex--; // 反向遍历时,索引减1
return currentIndex >= 0; // 检查是否到达数组开头
}
else
{
currentIndex++; // 正向遍历时,索引加1
return currentIndex < items.Length; // 检查是否到达数组末尾
}
}
// 重置枚举器
public void Reset()
{
currentIndex = reverse ? items.Length : -1; // 根据遍历方向重置索引
}
// 获取当前元素
public T Current
{
get { return items[currentIndex]; }
}
// 显式接口实现,获取当前元素
object IEnumerator.Current => Current;
// 释放资源
public void Dispose()
{
// 在这个实现中没有需要释放的资源
}
}
}
使用:
// 使用示例
public class Program
{
public static void Main()
{
int[] numbers = { 1, 2, 3, 4, 5 };
// 正向遍历
var forwardEnumerable = new BidirectionalEnumerable<int>(numbers);
Console.WriteLine("正向遍历:");
foreach (var number in forwardEnumerable)
{
Console.WriteLine(number);
}
// 反向遍历
var reverseEnumerable = new BidirectionalEnumerable<int>(numbers, true);
Console.WriteLine("反向遍历:");
foreach (var number in reverseEnumerable)
{
Console.WriteLine(number);
}
}
}
输出结果:
正向遍历:
1
2
3
4
5
反向遍历:
5
4
3
2
1
4. 设计模式-迭代器模式Iterator Pattern
使用IEnumerator<in T>、IEnumerable<in T>
正向或反向遍历的迭代器,实际上就遵循了迭代器模式(Iterator Pattern)。
迭代器模式(Iterator Pattern)是一种行为型设计模式,它提供了一种方法来顺序访问一个聚合对象中的各个元素,而不暴露其内部的表示。
1. 意图
迭代器模式的主要目的是:
- 提供一种统一的方法 来遍历不同的聚合对象。
- 隐藏聚合对象的内部表示,使得外部代码可以透明地访问集合内部数据。
2. 主要角色
迭代器模式包含以下几个主要角色:
2.1 迭代器接口(Iterator)——对应IEnumerator<in T>
定义了访问和遍历聚合对象中各个元素的方法,通常包括以下方法:
hasNext()
:判断是否还有下一个元素。next()
:返回下一个元素。
//迭代器接口(Iterator)
public interface Iterator
{
bool hasNext();
Object next();
}
2.2 具体迭代器(Concrete Iterator)——对应IEnumerable<in T>
实现了迭代器接口,负责对聚合对象进行遍历和访问,同时记录遍历的当前位置。
//定义了创建迭代器对象的接口,通常包括一个工厂方法用于创建迭代器对象。
public interface Container
{
Iterator getIterator();
}
2.3 聚合对象接口(Aggregate)——对应 IEnumerable<in T>
实现了对应可枚举对象
定义了创建迭代器对象的接口,通常包括一个工厂方法用于创建迭代器对象。
public class NameRepository : Container
{
public String[] names = {"Robert", "John", "Julie", "Lora"};
public Iterator getIterator()
{
return new NameIterator();
}
private class NameIterator : Iterator
{
int index;
public bool hasNext()
{
return index < names.Length;
}
public Object next()
{
if (this.hasNext())
{
return names[index++];
}
return null;
}
}
}
2.4 具体聚合对象(Concrete Aggregate)——对应使用Demo
实现了聚合对象接口,负责创建具体的迭代器对象,并提供需要遍历的数据。
public class IteratorPatternDemo
{
public static void Main(string[] args)
{
NameRepository namesRepository = new NameRepository();
for (Iterator iter = namesRepository.getIterator(); iter.hasNext();)
{
String name = (String) iter.next();
Console.WriteLine("Name : " + name);
}
}
}
3. 优缺点
3.1 优点
- 支持多种遍历方式:不同的迭代器可以定义不同的遍历方式。
- 简化聚合类:聚合类不需要关心遍历逻辑。
- 多遍历支持:可以同时对同一个聚合对象进行多次遍历。
- 扩展性:增加新的聚合类和迭代器类都很方便,无需修改现有代码。
3.2 缺点
- 系统复杂性:每增加一个聚合类,就需要增加一个对应的迭代器类,增加了类的数量。
4. foreach
与IEnumerator<in T>、IEnumerable<in T>
“Whereas a foreach statement is the consumer of the enumerator, an iterator is the producer of the enumerator.”
“foreach 语句是枚举器的使用者,而迭代器是枚举器的生产者。” 《C# 5.0 In A NutShell》
可以从简单的foreach示例来看,foreach到底做了什么
示例1:
foreach (var item in collection)
{
Console.WriteLine(item?.ToString());
}
编译器生成的确切代码更复杂一些,也更能清楚看到foreach到底都干了些什么。
{
var enumerator = collection.GetEnumerator();
try
{
while (enumerator.MoveNext())
{
var item = enumerator.Current;
Console.WriteLine(item.ToString());
}
}
finally
{
// dispose of enumerator.
}
}
可以看到foreach 语句使用 MoveNext() 和 IEnumerator 的 Current 属性来迭代序列。
扩展
如果遵循迭代器模式
:提供一个GetEnumerator
方法。这个方法返回一个包含Current
、MoveNext
和Reset
的类或结构体。即使并没有实现IEnumerable<in T >
和IEnumerator<in T >
接口, 也可以支持foreach语句。
Demo
using System;
public class MyCollection
{
private int[] _items = new int[10];
public MyCollection()
{
for (int i = 0; i < _items.Length; i++)
{
_items[i] = i * 2;
}
}
// 自定义迭代器类
public class MyIterator
{
private readonly MyCollection _collection;
private int _index;
public MyIterator(MyCollection collection)
{
_collection = collection;
_index = -1;
}
public bool MoveNext()
{
_index++;
return _index < _collection._items.Length;
}
public int Current => _collection._items[_index];
}
// 返回一个迭代器实例
public MyIterator GetEnumerator()
{
return new MyIterator(this);
}
}
class Program
{
static void Main(string[] args)
{
MyCollection myCollection = new MyCollection();
foreach (var item in myCollection)
{
Console.WriteLine(item);
}
}
}
输出结果:
0
2
4
6
8
10
12
14
16
18
总结
1 . foreach
使用 了枚举器提供的方法,而枚举器由迭代器来提供
2. 在C#中的最佳实践是在集合类中实现IEnumerable<in T >
和IEnumerator<in T >
接口是最佳实践,这样可以启用foreach
语法来遍历集合。但并不是必须。
5. yield
语句与迭代器
yield
关键字使方法成为迭代器,。用于在提供下一个值或信号迭代结束。它有两种形式:
yield return
:提供迭代中的下一个值。yield break
:显式信号迭代结束。
1. 使用示例
以下是一个方法示例,它会生成一个序列中的所有偶数,直到遇到一个特定的停止条件。
using System;
using System.Collections.Generic;
class Program
{
static void Main()
{
foreach (var number in GenerateEvenNumbers(20))
{
Console.WriteLine(number);
}
}
static IEnumerable<int> GenerateEvenNumbers(int max)
{
for (int i = 0; i <= max; i++)
{
if (i % 2 == 0)
{
yield return i; // 返回偶数
}
if (i > 10)
{
yield break; // 当i大于10时停止生成
}
}
}
}
在这个示例中:
yield return
用于返回序列中的偶数。yield break
用于在i
大于10时停止生成序列。
这个方法会生成0到10之间的所有偶数,并在i
大于10时停止。
输出
0
2
4
6
8
10
2. yield
的工作原理
当调用迭代器方法时,它不会立即执行代码,而是返回一个实现了 IEnumerable
或 IEnumerator
接口的对象。
每次调用 MoveNext
方法时,迭代器方法会继续执行,直到遇到 yield return
或 yield break
,迭代器的执行会暂停。
调用方会获得第一个迭代值并处理该值。
在后续的每次迭代中,迭代器的执行都会在导致上一次挂起的 yield return
语句之后恢复,并继续执行,直到到达下一个 yield return
语句为止。
当控件到达迭代器或 yield break
语句的末尾时,迭代完成。
3. yield
的优点
- 简化代码:使用
yield
可以避免手动实现IEnumerable
和IEnumerator
接口。 - 延迟执行:
yield
使得迭代器方法可以延迟执行,直到实际需要元素时才生成它们。 - 状态管理:
yield
使得迭代器方法可以在每次迭代时保持状态。
4. 注意事项
- 不能在带有
in
、ref
或out
参数的方法中使用yield
。 - 不能在匿名方法或 lambda 表达式中使用
yield
。 - 不能在不安全代码块中使用
yield
。
6. 枚举器实际应用场景及Demo
C# 中的 Enumerator(枚举器)在实际开发中有很多应用场景。以下是一些常见的例子:
1. 自定义集合与自定义迭代方式
在处理复杂的数据结构,如树和图时,标准遍历方法可能不足以满足需求。通过自定义枚举器,可以按照特定逻辑(如深度优先搜索DFS或广度优先搜索BFS)来遍历这些结构。
- 定义一个简单的树节点类:
public class TreeNode<T>
{
public T Value { get; set; }
public List<TreeNode<T>> Children { get; set; }
public TreeNode(T value)
{
Value = value;
Children = new List<TreeNode<T>>();
}
}
- 实现一个自定义枚举器来进行深度优先搜索:
public class DepthFirstEnumerator<T> : IEnumerator<T>
{
private readonly TreeNode<T> _root;
private Stack<TreeNode<T>> _stack;
private TreeNode<T> _current;
public DepthFirstEnumerator(TreeNode<T> root)
{
_root = root;
_stack = new Stack<TreeNode<T>>();
_stack.Push(_root);
}
public T Current => _current.Value;
object IEnumerator.Current => Current;
public bool MoveNext()
{
if (_stack.Count == 0)
return false;
_current = _stack.Pop();
for (int i = _current.Children.Count - 1; i >= 0; i--)
{
_stack.Push(_current.Children[i]);
}
return true;
}
public void Reset()
{
_stack.Clear();
_stack.Push(_root);
}
public void Dispose()
{
// No resources to dispose
}
}
- 在树结构上使用这个枚举器:
public class Tree<T> : IEnumerable<T>
{
private readonly TreeNode<T> _root;
public Tree(T rootValue)
{
_root = new TreeNode<T>(rootValue);
}
public TreeNode<T> Root => _root;
public IEnumerator<T> GetEnumerator()
{
return new DepthFirstEnumerator<T>(_root);
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
}
- 测试示例:
namespace TreeNodeTests
{
[TestClass]
public class TreeNodeTests
{
[TestMethod]
public void TestTreeCreation()
{
var tree = new Tree<int>(1);
Assert.AreEqual(1, tree.Root.Value);
Assert.AreEqual(0, tree.Root.Children.Count);
}
[TestMethod]
public void TestDepthFirstEnumerator()
{
var root = new TreeNode<int>(1);
root.Children.Add(new TreeNode<int>(2));
root.Children.Add(new TreeNode<int>(3));
root.Children[0].Children.Add(new TreeNode<int>(4));
root.Children[0].Children.Add(new TreeNode<int>(5));
root.Children[1].Children.Add(new TreeNode<int>(6));
var enumerator = new DepthFirstEnumerator<int>(root);
var expectedValues = new List<int> { 1, 2, 4, 5, 3, 6 };
var actualValues = new List<int>();
while (enumerator.MoveNext())
{
actualValues.Add(enumerator.Current);
}
CollectionAssert.AreEqual(expectedValues, actualValues);
}
}
}
2. 延迟计算与大量数据处理
使用yield
关键字可以创建一个延迟计算的枚举器,这在处理大量数据,分页数据,或复杂计算时,枚举器可以逐页或逐行提供数据,从而减少内存消耗并提高效率。
- 计算素数
例如,生成素数的序列时,可以逐个计算素数,而不是一次性计算所有素数。
using System;
using System.Collections.Generic;
class Program
{
static void Main()
{
//foreach 循环来逐个获取素数并打印出来
foreach (var prime in GeneratePrimes(50))
{
Console.WriteLine(prime);
}
}
static IEnumerable<int> GeneratePrimes(int max)
{
for (int number = 2; number <= max; number++)
{
if (IsPrime(number))//
{
yield return number;
}
}
}
//检查一个数是否为素数
static bool IsPrime(int number)
{
if (number < 2) return false;
for (int i = 2; i <= Math.Sqrt(number); i++)
{
if (number % i == 0) return false;
}
return true;
}
}
输出
2
3
5
7
11
13
17
19
23
29
31
37
41
43
47
- 数据分页
通过延迟计算,可以在请求下一页数据时才进行计算和加载,而不是一次性加载所有数据。
using System;
using System.Collections.Generic;
class Program
{
static void Main()
{
foreach (var item in GetPagedData(100, 10))
{
Console.WriteLine(item);
}
}
static IEnumerable<int> GetPagedData(int totalItems, int pageSize)
{
for (int i = 0; i < totalItems; i += pageSize)
{
for (int j = i; j < i + pageSize && j < totalItems; j++)
{
yield return j;
}
Console.WriteLine("Press any key to load next page...");
Console.ReadKey();
}
}
}
- 文件读取
使用延迟计算逐行读取文件内容,而不是一次性读取整个文件。
using System;
using System.Collections.Generic;
using System.IO;
class Program
{
static void Main()
{
foreach (var line in ReadLines("largefile.txt"))
{
Console.WriteLine(line);
}
}
static IEnumerable<string> ReadLines(string filePath)
{
using (StreamReader reader = new StreamReader(filePath))
{
string line;
while ((line = reader.ReadLine()) != null)
{
yield return line;
}
}
}
}
3. LINQ查询
通过自定义迭代器和扩展方法,可以扩展LINQ查询功能,实现更复杂的数据处理逻辑。
- 自定义迭代器方法:
GetSingleDigitNumbers
方法使用yield return
关键字返回从 0 到 9 的整数序列。 - 扩展方法:
Median
扩展方法计算整数序列的中位数。它首先对序列进行排序,然后根据元素来计算中位数。 - 主程序:在
Main
方法中,调用自定义迭代器获取数字序列,并使用扩展方法计算中位数,最后将结果输出到控制台。
using System;
using System.Collections.Generic;
using System.Linq;
namespace CustomIteratorDemo
{
class Program
{
static void Main(string[] args)
{
// 使用自定义迭代器获取单个数字序列
var numbers = GetSingleDigitNumbers();
// 使用扩展方法计算中位数
var median = numbers.Median();
Console.WriteLine($"Median: {median}");
}
// 自定义迭代器方法,返回从 0 到 9 的整数序列
public static IEnumerable<int> GetSingleDigitNumbers()
{
for (int i = 0; i < 10; i++)
{
yield return i;
}
}
}
// 扩展方法类
public static class EnumerableExtensions
{
// 扩展方法,计算序列的中位数
public static double Median(this IEnumerable<int> source)
{
if (source == null || !source.Any())
{
throw new InvalidOperationException("Cannot compute median for a null or empty set.");
}
var sortedList = source.OrderBy(number => number).ToList();
int count = sortedList.Count;
if (count % 2 == 0)
{
return (sortedList[count / 2 - 1] + sortedList[count / 2]) / 2.0;
}
else
{
return sortedList[count / 2];
}
}
}
}
输出
Median: 4.5
4. 状态机
状态机是一种用于在不同状态之间进行转换的模型。可以通过 yield return
语句在不同状态之间进行转换。
代码说明
- ParserState 枚举定义了解析器的三个状态:读取文本、读取数字和结束。
- ParserStateMachine 类实现了
IEnumerable<ParserState>
接口,用于迭代解析器的状态。 - GetEnumerator 方法使用
yield return
语句根据输入字符的类型返回当前状态,并更新解析器的位置。 - Program 类中的
Main
方法创建一个解析器实例,并使用foreach
循环迭代解析器的状态。
using System;
using System.Collections;
using System.Collections.Generic;
// 定义解析器状态的枚举
public enum ParserState
{
ReadingText, // 读取文本状态
ReadingNumber, // 读取数字状态
End // 结束状态
}
// 定义解析器状态机类,实现了 IEnumerable<ParserState> 接口
public class ParserStateMachine : IEnumerable<ParserState>
{
private readonly string _input; // 输入字符串
private int _position; // 当前解析位置
public ParserStateMachine(string input)
{
_input = input;
_position = 0;
}
// 实现 GetEnumerator 方法,返回解析器状态的枚举器
public IEnumerator<ParserState> GetEnumerator()
{
// 遍历输入字符串
while (_position < _input.Length)
{
char currentChar = _input[_position]; // 获取当前字符
if (char.IsLetter(currentChar)) // 如果是字母
{
yield return ParserState.ReadingText; // 返回读取文本状态
// 跳过连续的字母
while (_position < _input.Length && char.IsLetter(_input[_position]))
{
_position++;
}
}
else if (char.IsDigit(currentChar)) // 如果是数字
{
yield return ParserState.ReadingNumber; // 返回读取数字状态
// 跳过连续的数字
while (_position < _input.Length && char.IsDigit(_input[_position]))
{
_position++;
}
}
else // 其他字符
{
_position++; // 跳过
}
}
yield return ParserState.End; // 返回结束状态
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
}
// 使用Demo
class Program
{
static void Main(string[] args)
{
string input = "Hello123World456"; // 输入字符串
ParserStateMachine parser = new ParserStateMachine(input); // 创建解析器状态机实例
// 遍历解析器状态并输出
foreach (var state in parser)
{
Console.WriteLine($"Current State: {state}");
}
}
}
输出
Current State: ReadingText
Current State: ReadingNumber
Current State: ReadingText
Current State: ReadingNumber
Current State: End
5. 异步编程
在异步编程中,IEnumerator
可以与async
和await
结合使用,实现异步迭代。例如,可以从异步数据流中逐个读取数据项。
public class DataLoader
{
// 模拟异步加载数据的方法
public async IAsyncEnumerable<string> LoadDataAsync()
{
List<string> dataSources = new List<string> { "数据源1", "数据源2", "数据源3" };
foreach (var source in dataSources)
{
// 模拟异步加载数据
await Task.Delay(1000); // 模拟异步操作
yield return $"从{source}加载的数据";
}
}
}
单元测试:
[TestClass]
public class DataLoaderTests
{
[TestMethod]
public async Task LoadDataAsync_ShouldReturnCorrectData()
{
// Arrange
var dataLoader = new DataLoader();
var expectedData = new List<string> { "从数据源1加载的数据", "从数据源2加载的数据", "从数据源3加载的数据" };
var actualData = new List<string>();
// Act
await foreach (var data in dataLoader.LoadDataAsync())
{
actualData.Add(data);
}
// Assert
CollectionAssert.AreEqual(expectedData, actualData);
}
}
总结
- 实现**
IEnumerator
**接口:枚举器
专门用于遍历集合。它就像一个游标,可以指向当前数据并移动到下一个数据,从而逐个取出集合中的数据,而无需关心其内部的数据结构或地址等细节。 - 实现**
IEnumerable
**接口:可枚举对象
表示该对象提供了获取枚举器的方法,可以使用此枚举器对集合进行遍历。 - 在C#中,通过实现
IEnumerator
和IEnumerable
接口的可枚举对象,可以支持foreach
语句,从而简洁地遍历数组或集合。这一过程遵循了迭代器模式。 - yield语句:可以用于实现迭代器方法。使用
yield
可以避免手动实现IEnumerable
和IEnumerator
接口,在延迟执行和状态管理上发挥重要作用。 - 异步迭代:可以与
async
和await
结合使用,实现异步迭代。 foreach
使用 了枚举器提供的方法,而枚举器由迭代器来提供。因此,迭代器可以看作是枚举器和可枚举对象的组合
和IComparable<in T> 与 IComparer 类似,able代表能力,表示实现该接口的类具有XX能力。er名词,表示自定义实现具有XX功能的枚举器
【参考】