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

本章主要介绍 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(7);

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

输出:

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# 当中枚举器、可枚举类以及迭代器的介绍。如有收获,记得一键三连呐。

最后,给各位道友介绍一下使用国外虚拟卡开通一些国外服务的渠道,当前我在用的是 wildcard,使用我的注册邀请码 IOQ1YDHH 注册,你能得 2 美刀呢。能抵消一部分的开卡费用。自己买个 Github CopilotChatGPT Plus (不用注册账号即可使用)简直不要太香。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/891453.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

uni-app 打包成app时 限制web-view大小

今天对接一个uni-app的app 内置对方h5 web-view的形式 需要对方在web-view顶部加点东西 对方打的app的web-view始终是全屏的状态&#xff0c;对方表示做不到我要的效果 emmmmmm。。。。。。 于是乎 自己搭了个demo 本地h5跑起来审查了下代码&#xff0c;发现web-view是给绝对定…

IP地址与CDN提升网络速度

视频流媒体、在线游戏、或是电商购物&#xff0c;互联网在我们的工作生活中愈加不可或缺&#xff0c;人们对于网络的加载速度要求也越来越严苛。而IP地址与CDN的协同工作&#xff0c;对于互联网速度增加与稳定起这重大的作用。 一、CDN的工作原理 CDN是由分布在全球各地的服务…

基于JAVA+SpringBoot+Vue的旅游管理系统

基于JAVASpringBootVue的旅游管理系统 前言 ✌全网粉丝20W,csdn特邀作者、博客专家、CSDN[新星计划]导师、java领域优质创作者,博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java技术领域和毕业项目实战✌ &#x1f345;文末附源码下载链接&#x1f345; 哈喽兄…

使用XML实现MyBatis的基础操作

目录 前言 1.准备工作 1.1⽂件配置 1.2添加 mapper 接⼝ 2.增删改查操作 2.1增(Insert) 2.2删(Delete) 2.3改(Update) 2.4查(Select) 前言 接下来我们会使用的数据表如下&#xff1a; 对应的实体类为&#xff1a;UserInfo 所有的准备工作都在如下文章。 MyBatis 操作…

【论文速看】DL最新进展20241015-目标检测、图像超分

目录 【目标检测】【图像超分】 【目标检测】 [ECCV2024] LaMI-DETR: Open-Vocabulary Detection with Language Model Instruction 论文链接&#xff1a;https://arxiv.org/pdf/2407.11335 代码链接&#xff1a;https://github.com/eternaldolphin/LaMI-DETR 现有方法通过利…

Android ImageView scaleType使用

目录 一、src设置图片资源 二、scaleType设置图片缩放类型 三、scaleType具体表现 matrix&#xff1a; fitXY: fitStart&#xff1a; fitCenter&#xff1a; fitEnd: Center&#xff1a; centerCrop: centerInside&#xff1a; 控制ImageView和图片的大小保持一致…

【优选算法】(第四十一篇)

目录 被围绕的区域&#xff08;medium&#xff09; 题目解析 讲解算法原理 编写代码 迷宫中离⼊⼝最近的出⼝&#xff08;medium&#xff09; 题目解析 讲解算法原理 编写代码 被围绕的区域&#xff08;medium&#xff09; 题目解析 1.题目链接&#xff1a;. - 力扣&a…

创建docker虚拟镜像,创建启动服务脚本

进入系统命令服务目录 编辑服务 [Unit] DescriptionDocker Application Container Engine Documentationhttps://docs.docker.com Afternetwork-online.target firewalld.service Wantsnetwork-online.target [Service] Typenotify ExecStart/usr/bin/dockerd ExecReload/bin/…

[旧日谈]关于Qt的刷新事件频率,以及我们在Qt的框架上做实时的绘制操作时我们该关心什么。

[旧日谈]关于Qt的刷新事件频率&#xff0c;以及我们在Qt的框架上做实时的绘制操作时我们该关心什么。 最近在开发的时候&#xff0c;发现一个依赖事件来刷新渲染的控件会导致程序很容易异常和崩溃。 当程序在运行的时候&#xff0c;其实软件本身的负载并不高&#xff0c;所以…

【量化交易】聚宽安装

安装JQData 更换源&#xff1a; 如果使用的是pip默认的PyPI源&#xff0c;可以尝试更换为一个更快的国内镜像源。例如阿里云、豆瓣等提供的PyPI镜像。 更改方法可以通过设置环境变量或者在pip命令中直接指定&#xff1a; PS C:\Users\bilirjs\Documents> pip config set …

fastadmin 多商户模式下侧边栏跳转路径BUG

记录&#xff1a;仅作自己项目记录&#xff0c;在一个域名下部署多套项目时&#xff0c;若是多商户模式项目会出现跳转路径问题。 修改 \manystore\library\Auth.php 文件的 getSidebar 方法 // 1 改为&#xff1a; $v[url] isset($v[url]) && $v[url] ? $v[url] :…

一键快捷回复软件助力客服高效沟通

双十一临近&#xff0c;电商大战一触即发&#xff01;在这个购物狂欢的热潮中&#xff0c;客服团队的效率至关重要。今天我要和大家分享一个非常实用的快捷回复软件&#xff0c;特别是为电商客服小伙伴们准备的。这款软件能够极大地提高你的工作效率&#xff0c;让你在处理客户…

前端布局与响应式设计综合指南(二)

​&#x1f308;个人主页&#xff1a;前端青山 &#x1f525;系列专栏&#xff1a;Css篇 &#x1f516;人终将被年少不可得之物困其一生 依旧青山,本期给大家带来Css篇专栏内容:前端布局与响应式设计综合指南(二) 目录 23、行内元素和块级元素&#xff1f;img算什么&…

音视频入门基础:FLV专题(15)——Video Tag简介

一、引言 根据《video_file_format_spec_v10_1.pdf》第75页&#xff0c;如果某个Tag的Tag header中的TagType值为9&#xff0c;表示该Tag为Video Tag&#xff1a; 这时StreamID之后紧接着的就是VideoTagHeader&#xff0c;也就是说这时Tag header之后的就是VideoTagHeader&…

热成像人像算法呈现方式!

一、热红外成像技术 热红外成像技术利用物体发出的红外辐射进行成像&#xff0c;这种辐射与物体的温度有关。因此&#xff0c;热红外成像可以不受光照条件的影响&#xff0c;且在图像中&#xff0c;人体由于温度较高&#xff0c;通常会比背景显得更亮。 二、图像处理算法 阈…

远翔原厂芯片设计开发软件:降压恒流共阳极无频闪调光芯片FP7126/7127/7128,舞台灯磁吸轨道灯智能家居应用方案

FP7126 FP7127 FP7128是平均电流模式控制的 LED 驱动 IC&#xff0c;具有稳定输出恒流的能力&#xff0c;优秀的负载调整率与高精度的电流控制。不用额外增加外部补偿元件&#xff0c;简化 PCB 板设计。FP7126 FP7127 FP7128可接受 PWM 数位调光&#xff0c;建议调光频率 0.1kH…

[C++ 核心编程]笔记 4.1.4 类和对象 - 案例1

类和对象: 案例1: 设计立方体类(Cube) 求出立方体的面积和体积分别用全局函数和成员函数判断两个立方体是否相等。 设计方法: 创建立方体类设计属性设计行为 求立方体面积和体积分别用全局和成员函数 判断立方体是否相等 #include<iostream> using namespace std;clas…

音频剪辑在线工具 —— 让声音更精彩

你是否曾梦想过拥有自己的声音创作空间&#xff0c;却苦于复杂的音频编辑软件&#xff1f;接下来&#xff0c;让我们一同揭开这些音频剪辑在线工具的神秘面纱&#xff0c;看看它们如何帮助你实现从录音到发布的无缝衔接。 1.福昕音频剪辑 链接直达>>https://www.foxits…

勇攀保研高峰:解锁环节与要点,更容易上岸成功

在大学的逐梦之旅中&#xff0c;保研宛如一座令人向往的学术高峰&#xff0c;吸引着无数优秀学子奋力攀登。对于那些渴望在学术道路上更进一步的同学来说&#xff0c;了解保研的各个环节和考察要点至关重要。那么&#xff0c;保研究竟有着怎样的神秘路径呢&#xff1f;让我们一…

ArcGIS中分区统计栅格值前需要进行投影吗(在投影坐标系下进行吗),为什么?

最近&#xff0c;我接到了一个分区统计栅格数值前需要进行投影&#xff0c;或者说是必须需要在投影坐标系下进行吗的咨询。 答案是不需要刻意去变。 但是他又说他把地理坐标系下分区统计结果与投影坐标系下的分区统计结果分别做了一遍&#xff0c;并进行了对比&#xff0c;两个…