图解C#高级教程(五):枚举器和迭代器

时间:2024-10-16 07:24:54

本章主要介绍 C# 当中枚举器、可枚举类型以及迭代器相关的知识。

文章目录

  • 1. 枚举器和可枚举类型
  • 2. IEnumerator 和 IEnumerable 接口
    • 2.1 IEnumerator 接口
    • 2.2 `IEnumerable` 接口
  • 3. 泛型枚举接口
  • 4. 迭代器
    • 4.1 使用迭代器创建枚举器
    • 4.2 使用迭代器创建可枚举类
    • 4.3 迭代器作为属性
    • 4.4 迭代器的使用场景
    • 4.4 迭代器的工作原理

1. 枚举器和可枚举类型

枚举器为我们提供了一种统一的方式来顺序访问集合中的元素,却不必了解集合内部的具体实现。例如,使用 foreach 语句遍历数组中的元素:

int[] arr = {10, 11, 12, 23};
foreach (int item in arr1)
{
	Console.WriteLine("Item value: {0}", item);
}

可枚举类型允许对象可以被枚举器逐一遍历,也就是说一个类型必须是可枚举的才能使用 foreach 循环进行遍历。

2. IEnumerator 和 IEnumerable 接口

2.1 IEnumerator 接口

一个枚举器必须实现 IEnumerator 接口的 3 个函数成员:

  • Current:返回序列中当前位置的属性。这个属性是只读的,返回的是 object 类型的引用,所以可以返回任何类型。
  • MoveNext 方法把枚举器位置移动到集合中的下一项。返回值为 bool 值,如果新的位置有效,返回 true;否则,返回 fasle。由于枚举器的原始位置在序列中的第一项之前,因此第一次使用 Current 之前必须先调用 MoveNext
  • Reset 把位置重置为原始状态。

设计的思想就是需要一个标志物(具体可以是引用、指针或者其它方式)能够获得被遍历集合的当前项,能够移动到集合中下一项的方法,以及重置当前位置的方法。

举一个例子,下图的左边是自定义的可枚举类型,右边是枚举器。
在这里插入图片描述

2.2 IEnumerable 接口

可枚举类是指实现了 IEnumerable 接口的类,该接口只有 GetEnumrator 一个成员方法,它返回用于枚举可枚举类型的枚举器对象。

下面是一个结合使用枚举器和自定义可枚举类的例子:

using System;
using System.Collections;

// 自定义整数范围类,实现 IEnumerable 接口
public class IntegerRange : IEnumerable
{
    private int _start;
    private int _end;

    public IntegerRange(int start, int end)
    {
        _start = start;
        _end = end;
    }

    // 实现 IEnumerable 接口的 GetEnumerator 方法
    public IEnumerator GetEnumerator()
    {
        return new IntegerRangeEnumerator(_start, _end);
    }

    // 自定义枚举器类,实现 IEnumerator 接口
    private class IntegerRangeEnumerator : IEnumerator
    {
        private int _start;
        private int _end;
        private int _current;
        private bool _hasStarted;

        public IntegerRangeEnumerator(int start, int end)
        {
            _start = start;
            _end = end;
            _hasStarted = false;
        }

        // 实现 MoveNext(),移动到下一个元素
        public bool MoveNext()
        {
            if (!_hasStarted)
            {
                _current = _start;
                _hasStarted = true;
            }
            else
            {
                _current++;
            }

            return _current <= _end;
        }

        // 实现 Current 属性,返回当前元素
        public object Current
        {
            get
            {
                if (!_hasStarted || _current > _end)
                    throw new InvalidOperationException();
                return _current;
            }
        }

        // 实现 Reset(),重置枚举器状态
        public void Reset()
        {
            _hasStarted = false;
        }
    }
}

class Program
{
    static void Main()
    {
        IntegerRange range = new IntegerRange(1, 5);

        // 使用 foreach 遍历自定义的可枚举类型
        foreach (int number in range)
        {
            Console.WriteLine(number);
        }
    }
}

输出:

1
2
3
4
5

3. 泛型枚举接口

在 C# 中,泛型枚举接口主要包括两个接口:IEnumerable<T>IEnumerator<T>。这两个接口分别用于实现泛型集合的枚举功能,允许集合中的元素在类型安全的情况下被逐个遍历。与非泛型版本相比,泛型接口提供了更好的类型安全性和性能,因为它避免了装箱/拆箱操作以及对 object 的转换。

对于泛型接口形式:

  • IEnumerable<T> 接口的 GetEnumerator 方法返回实现 IEnumerator 枚举器类的实例;
  • 实现 IEnumerator<T> 的类实现了 Current 属性,它返回实际类型的对象,而不是 object 基类的引用。

下面是一个实现 IEnumerable<T>IEnumerator<T> 的简单示例,定义了一个泛型范围类 Range<T>,可以生成从某个起始值到终止值的泛型集合。

using System;
using System.Collections;
using System.Collections.Generic;

// 定义一个泛型范围类,实现 IEnumerable<T>
public class Range<T> : IEnumerable<T> where T : IComparable<T>
{
    private T _start;
    private T _end;
    private Func<T, T> _incrementFunc;

    public Range(T start, T end, Func<T, T> incrementFunc)
    {
        _start = start;
        _end = end;
        _incrementFunc = incrementFunc;
    }

    // 实现 IEnumerable<T> 接口
    public IEnumerator<T> GetEnumerator()
    {
        return new RangeEnumerator(_start, _end, _incrementFunc);
    }

    // 显式实现非泛型的 IEnumerable 接口
    IEnumerator IEnumerable.GetEnumerator()
    {
        return GetEnumerator();
    }

    // 自定义泛型枚举器
    private class RangeEnumerator : IEnumerator<T>
    {
        private T _current;
        private T _start;
        private T _end;
        private Func<T, T> _incrementFunc;
        private bool _hasStarted;

        public RangeEnumerator(T start, T end, Func<T, T> incrementFunc)
        {
            _start = start;
            _end = end;
            _incrementFunc = incrementFunc;
            _hasStarted = false;
        }

        // 实现 IEnumerator<T> 的 Current 属性
        public T Current
        {
            get
            {
                if (!_hasStarted)
                    throw new InvalidOperationException();
                return _current;
            }
        }

        // 实现非泛型的 Current 属性
        object IEnumerator.Current => Current;

        // 实现 MoveNext(),移动到下一个元素
        public bool MoveNext()
        {
            if (!_hasStarted)
            {
                _current = _start;
                _hasStarted = true;
            }
            else
            {
                _current = _incrementFunc(_current);
            }

            return _current.CompareTo(_end) <= 0;
        }

        // 实现 Reset(),重置枚举器
        public void Reset()
        {
            _hasStarted = false;
        }

        // 实现 Dispose() 方法
        public void Dispose() { }
    }
}

class Program
{
    static void Main()
    {
        // 定义一个整数范围的泛型集合
        var range = new Range<int>(1, 5, x => x + 1);

        // 使用 foreach 遍历集合
        foreach (var number in range)
        {
            Console.WriteLine(number);
        }
    }
}

Range<T>代码解释:

  1. Range<T> 是一个泛型类,允许生成从 _start_end 范围的集合。它实现了 IEnumerable<T> 接口,使该集合可以被 foreach 遍历。
  2. incrementFunc 是一个 Func<T, T> 类型的委托,指定如何生成下一个值。这使得 Range<T> 可以用于不同类型的泛型数据。

输出:

1
2
3
4
5

4. 迭代器

C# 从 2.0 版本开始提供了创建枚举器和可枚举类型的简单方式——迭代器。迭代器可为我们自动创建枚举器和可枚举类型。通过迭代器,程序可以按需逐步生成和返回集合中的元素,而无需一次性加载所有元素。

C# 的迭代器是通过使用两个关键字实现的:

  • yield return:用于按需返回集合中的下一个元素。
  • yield break:用于立即终止迭代。

当你实现一个迭代器方法时,C# 编译器会自动生成一个实现 IEnumerableIEnumerable<T> 接口的类,并生成适当的 IEnumeratorIEnumerator<T> 枚举器,因此不需要手动编写这些接口的实现代码。迭代器方法可以像常规方法一样被调用,并与 foreach 语句兼容。

4.1 使用迭代器创建枚举器

下面的实例展示了使用迭代器创建枚举器:

class MyClass
{
    public IEnumerator<string> GetEnumerator()
    {
        return BlackAndWhite();     // 返回枚举器
    }

	// 直接产生一个枚举器
    public IEnumerator<string> BlackAndWhite()
    {
        yield return "Black";
        yield return "White";
        yield return "gray";
    }

    class Program
    {
        static void Main(string[] args)
        {
            MyClass myClass = new MyClass();
            foreach (string color in myClass)
            {
                Console.WriteLine(color);
            }
        }
    }
}

输出如下:

Black
White
gray

编译器自动帮助我们做的工作如下图所示。编译器会为我们添加一个实现一个枚举器必须包含的方法。
在这里插入图片描述

4.2 使用迭代器创建可枚举类

下面的示例展示了使用迭代器创建可枚举类,这里的 BlackAndWhite 方法返回可枚举类型而不是枚举器,因此在 GetEnumrator 方法中需要调用可枚举类型对象的 GetEnumerator 方法来获取枚举器。

class MyClass1
{
    public IEnumerator<string> GetEnumerator()
    {
        IEnumerable<string> myEnum = BlackAndWhite();       // 获取可枚举类型
        return myEnum.GetEnumerator();          // 获取枚举器
    }

    public IEnumerable<string> BlackAndWhite()
    {
        yield return "Black";
        yield return "White";
        yield return "gray";
    }

}

class Program
{
    static void Main(string[] args)
    {
        MyClass1 myClass1 = new MyClass1();

        // 使用类对象
        foreach (string color in myClass1)
        {
            Console.WriteLine(color);
        }

        // 使用类枚举方法
        foreach (string color in myClass1.BlackAndWhite())
        {
            Console.WriteLine(color);
        }
    }
}

在这里插入图片描述

4.3 迭代器作为属性

下面的代码示例主要用来演示两个方面的内容:

  1. 使用迭代器产生具有两个枚举器的类;
  2. 演示迭代器作为属性而不是方法
    这段代码声明了两个属性来定义两个不同的枚举器。GetEnumerator 方法根据 _listFromUVtoIR 布尔变量的值返回两个枚举器中的一个。如果 _listFromUVtoIRtrue ,则返回 UVtoIR 枚举器;否则,返回 IRtoUV 枚举器。
class Spectrum
{
    bool _listFromUVtoIR;

    string[] colors = { "Red", "Green", "Blue", "Yellow", "Purple", "Orange" };

    public Spectrum(bool isFromUVtoIR)
    {
        _listFromUVtoIR = isFromUVtoIR;
    }

    public IEnumerator<string> GetEnumerator()
    {
        return _listFromUVtoIR? UVtoIR: IRtoUV;
    }

    public IEnumerator<string> UVtoIR
    {
        get
        {
            for (int i = 0; i < colors.Length; i++)
            {
                yield return colors[i];
            }
        }
    }

    public IEnumerator<string> IRtoUV
    {
        get 
        {
            for (nint i = colors.Length - 1; i >= 0; i--)
                yield return colors[i];
        }
    }
}

class Program
{
    static void Main()
    {
        Spectrum startUV = new Spectrum(true);
        Spectrum startIR = new Spectrum(false);

        foreach (string color in startUV)
        {
            Console.Write("{0} ", color);
        }
        Console.WriteLine();
        foreach (string color in startIR)
        {
            Console.Write("{0} ", color);
        }
    }
}

输出:

Red Green Blue Yellow Purple Orange
Orange Purple Yellow Blue Green Red

下面代码的说明。

public IEnumerator<string> IRtoUV
{
    get 
    {
        for (nint i = colors.Length - 1; i >= 0; i--)
            yield return colors[i];
    }
}

IRtoUV 是一个只读属性,它的类型是 IEnumerator<string>。当访问该属性时,get 访问器中的代码会被执行,返回一个 IEnumerator<string> 对象。这个 get 访问器使用了 yield return,因此它定义了一个迭代器,用于逐步返回 colors 数组中的元素,按从后往前的顺序。

4.4 迭代器的使用场景

迭代器的使用场景如下:

  1. 延迟执行
    迭代器提供了按需生成元素的能力,这意味着元素只有在被请求时才会生成。这对于处理大数据集合、流数据或计算开销较大的数据非常有用。举个例子,如果你有一个需要大量计算才能得到的值,使用迭代器可以避免一次性计算所有值,而是每次只计算一个值。
static IEnumerable<int> GenerateLargeNumbers()
{
    for (int i = 0; i < int.MaxValue; i++)
    {
        yield return i;
    }
}
  1. 自定义集合的遍历
    当你创建自定义的数据结构时,可以使用迭代器来定义集合的遍历规则,而不需要手动实现 IEnumerableIEnumerator 接口。例如,使用迭代器遍历二叉树:
using System;
using System.Collections.Generic;

// 定义二叉树节点类
public class TreeNode
{
    public int Value;  // 节点的值
    public TreeNode Left;  // 左子节点
    public TreeNode Right;  // 右子节点

    public TreeNode(int value)
    {
        Value = value;
    }

    // 使用迭代器实现深度优先遍历(前序遍历:根 -> 左 -> 右)
    public IEnumerable<int> PreOrderTraversal()
    {
        // 返回当前节点的值
        yield return Value;

        // 如果左子节点存在,递归遍历左子树
        if (Left != null)
        {
            foreach (var leftValue in Left.DepthFirstTraversal())
            {
                yield return leftValue;
            }
        }

        // 如果右子节点存在,递归遍历右子树
        if (Right != null)
        {
            foreach (var rightValue in Right.DepthFirstTraversal())
            {
                yield return rightValue;
            }
        }
    }
	// 中序遍历:左 -> 根 -> 右)
	public IEnumerable<int> InOrderTraversal()
	{
	    if (Left != null)
	    {
	        foreach (var leftValue in Left.InOrderTraversal())
	        {
	            yield return leftValue;
	        }
	    }
	
	    yield return Value;
	
	    if (Right != null)
	    {
	        foreach (var rightValue in Right.InOrderTraversal())
	        {
	            yield return rightValue;
	        }
	    }
	}

	// 后序遍历:右 -> 根 -> 左
	public IEnumerable<int> PostOrderTraversal()
{
    if (Left != null)
    {
        foreach (var leftValue in Left.PostOrderTraversal())
        {
            yield return leftValue;
        }
    }

    if (Right != null)
    {
        foreach (var rightValue in Right.PostOrderTraversal())
        {
            yield return rightValue;
        }
    }

    yield return Value;
}

}

class Program
{
    static void Main()
    {
        // 构建一棵二叉树
        TreeNode root = new TreeNode(1);
        root.Left = new TreeNode(2);
        root.Right = new TreeNode(3);
        root.Left.Left = new TreeNode(4);
        root.Left.Right = new TreeNode(5);
        root.Right.Left = new TreeNode(6);
        root.Right.Right = new TreeNode