C#高级教程(五):枚举器、迭代器以及使用场景

本章主要通过大量的代码示例介绍了 C# 当中枚举器和可枚举类型的基本概念,IEnumerator 和 IEnumerable 接口,以及迭代器的详细用法和使用场景。

1. 枚举器和可枚举类型

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

csharp 复制代码
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 一个成员方法,它返回用于枚举可枚举类型的枚举器对象。

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

csharp 复制代码
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);
        }
    }
}

输出:

bash 复制代码
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>,可以生成从某个起始值到终止值的泛型集合。

csharp 复制代码
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> 可以用于不同类型的泛型数据。

输出:

bash 复制代码
1
2
3
4
5

4. 迭代器

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

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

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

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

4.1 使用迭代器创建枚举器

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

csharp 复制代码
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);
            }
        }
    }
}

输出如下:

bash 复制代码
Black
White
gray

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

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

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

csharp 复制代码
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 枚举器。
csharp 复制代码
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);
        }
    }
}

输出:

bash 复制代码
Red Green Blue Yellow Purple Orange
Orange Purple Yellow Blue Green Red

下面代码的说明。

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

        // 使用深度优先遍历二叉树
        Console.WriteLine("DFS Traversal (Pre-order):");
        foreach (int value in root.PreOrderTraversal())
        {
            Console.WriteLine(value);
        }
    }
}

输出:

bash 复制代码
DFS Traversal (Pre-order):
1 2 4 5 3 6 7
DFS Traversal (In-order):
4 2 5 1 6 3 7
DFS Traversal (Post-order):
4 5 2 6 7 3 1

二叉树的形状:

4.4 迭代器的工作原理

在后台,由编译器生成的枚举器类是包含4个状态的状态机。

  • Before 首次调用 MoveNext 的初始状态。
  • Running 调用MoveNext后进入这个状态。在这个状态中,枚举器检测并设置下一项的位置。在遇到yield returnyield break 或在迭代器体结束时,退出状态。
  • Suspended 状态机等待下次调用 MoveNext 的状态。
  • After 没有更多项可以枚举。

好了,以上就是对 C# 当中枚举器、可枚举类以及迭代器的介绍。如有收获,记得一键三连呐。

相关推荐
bobz9652 分钟前
ovs patch port 对比 veth pair
后端
Asthenia041212 分钟前
Java受检异常与非受检异常分析
后端
uhakadotcom26 分钟前
快速开始使用 n8n
后端·面试·github
JavaGuide33 分钟前
公司来的新人用字符串存储日期,被组长怒怼了...
后端·mysql
bobz96543 分钟前
qemu 网络使用基础
后端
Asthenia04121 小时前
面试攻略:如何应对 Spring 启动流程的层层追问
后端
Asthenia04121 小时前
Spring 启动流程:比喻表达
后端
Asthenia04122 小时前
Spring 启动流程分析-含时序图
后端
ONE_Gua2 小时前
chromium魔改——CDP(Chrome DevTools Protocol)检测01
前端·后端·爬虫
致心2 小时前
记一次debian安装mariadb(带有迁移数据)
后端