• C#迭代器的详细用法


    什么是迭代器

    迭代器是一种设计模式,可以让开发人员无需关心容器对象的底层架构,就可以遍访这个容器对象。简单来说,迭代器就是用来遍历一个序列中的所有对象。

    在C#中可以使用foreach关键字就可以枚举一个序列

    1. foreach (var item in collection)
    2. {
    3. Console.WriteLine(item?.ToString());
    4. }

    但foreach语句并非完美无缺,它依赖于.NET Core库中的两个接口:IEnumerableIEnumerator,接下来我们通过这两个接口实现foreach语句

    IEnumerable是可枚举的意思,IEnumerator是枚举器的意思

    IEnumerable接口

    1. public interface IEnumerable
    2. {
    3. IEnumerator GetEnumerator();
    4. }

    继承这个接口需要实现暴露出来的GetEnumerator方法,返回一个IEnumerator对象

    IEnumerator接口

    1. public interface IEnumerator
    2. {
    3. object Current { get; } 
    4. bool MoveNext();
    5. void Reset();
    6. }

    IEnumerator接口有三个东西,Current返回当前序列的元素,方法MoveNext()移动到下一个元素,Reset方法重置,所以继承这个接口需要实现这三个东西

    从这个两个接口对比就可以发现,对于枚举一个容器,起真正作用是IEnumerator

    所以一个对象只要实现IEnumerator接口就能遍历

    下面来看一个实例

    1. using System;
    2. using System.Collections;
    3. using System.Collections.Generic;
    4. using System.Linq;
    5. using System.Text;
    6. using System.Threading.Tasks;
    7. namespace Csharp_study.Day1
    8. {
    9. //遍历对象
    10. public class Anim
    11. {
    12. public string name;//动物的名称
    13. //构造方法,给name赋值
    14. public Anim(string name)
    15. {
    16. this.name = name;
    17. }
    18. }
    19. //枚举器
    20. public class MIEnumerator : IEnumerator
    21. {
    22. Anim[] anim;
    23. int idx = -1;
    24. //构造函数,anim赋值
    25. public MIEnumerator(Anim[] t)
    26. {
    27. anim = t;
    28. }
    29. //实现IEnumerator接口的Current方法,获取当前元素
    30. public object Current
    31. {
    32. get
    33. {
    34. if (idx == -1)
    35. return new IndexOutOfRangeException();
    36. return anim[idx];
    37. }
    38. }
    39. //实现IEnumerator接口的MoveNext方法,向下一个元素移动
    40. public bool MoveNext()
    41. {
    42. idx++;
    43. return anim.Length > idx;
    44. }
    45. //实现IEnumerator接口的Reset方法,重置迭代器状态
    46. public void Reset()
    47. {
    48. idx = -1;
    49. }
    50. }
    51. class Class1
    52. {
    53. static void Main(string[] args)
    54. {
    55. //初始化一个Anim序列,用来遍历
    56. Anim[] anims = new Anim[] { new Anim("老虎"), new Anim("大象"), new Anim("河马") };
    57. MIEnumerator enumerator = new MIEnumerator(anims);
    58. while (enumerator.MoveNext())
    59. {
    60. Anim test = enumerator.Current as Anim;
    61. Console.WriteLine(test.name);
    62. }
    63. Console.ReadLine();
    64. }
    65. }
    66. }

    输出结果

    1. 老虎
    2. 大象
    3. 河马

    这段代码就不细说了,看注释就能明白。从这个例子中就可以看出来,我们通过继承这个IEnumerator接口,然后实现它的Current,MoveNext和Reset方法就可以遍历这个Anim对象了。

    所以不难看出,foreach关键字就是主要依靠IEnumerator接口实现。那IEnumerable接口是干嘛的呢?上面已经说了,它只有一个GetEnumerator方法,并且返回的是一个IEnumerator类型的对象,所以说IEnumerable的作用就是获得可用于循环访问集合的IEnumerator对象

    我们最终是要通过这两个接口来实现foreach,所以我们对上面的代码进行一下改动

    1. using System;
    2. using System.Collections;
    3. using System.Collections.Generic;
    4. using System.Linq;
    5. using System.Text;
    6. using System.Threading.Tasks;
    7. namespace Csharp_study.Day1
    8. {
    9. //IEnumerable接口
    10. public class Anim : IEnumerable
    11. {
    12. public string name;//动物名称
    13. Anim[] t;//anim对象数组
    14. //构造方法重载1-name赋值
    15. public Anim(string name)
    16. {
    17. this.name = name;
    18. }
    19. //构造方法重载2-t赋值
    20. public Anim(Anim[] p)
    21. {
    22. t = p;
    23. }
    24. //实现IEnumerable接口的GetEnumerator方法
    25. public IEnumerator GetEnumerator()
    26. {
    27. Console.WriteLine("调用GetEnumerator方法!");
    28. return new MIEnumerator(t);
    29. }
    30. }
    31. //实现IEnumerator接口
    32. public class MIEnumerator : IEnumerator
    33. {
    34. Anim[] anim;
    35. int idx = -1;
    36. public MIEnumerator(Anim[] t)
    37. {
    38. anim = t;
    39. }
    40. //实现IEnumerator接口的Current方法,获取当前元素
    41. public object Current
    42. {
    43. get
    44. {
    45. Console.WriteLine("调用Current方法!");
    46. if (idx == -1)
    47. return new IndexOutOfRangeException();
    48. return anim[idx];
    49. }
    50. }
    51. //实现IEnumerator接口的MoveNext方法,向下一个元素移动
    52. public bool MoveNext()
    53. {
    54. Console.WriteLine("调用MoveNext方法!");
    55. idx++;
    56. return anim.Length > idx;
    57. }
    58. //实现IEnumerator接口的Reset方法,重置迭代器状态
    59. public void Reset()
    60. {
    61. Console.WriteLine("调用Reset方法!");
    62. idx = -1;
    63. }
    64. }
    65. class Class1
    66. {
    67. static void Main(string[] args)
    68. {
    69. //初始化一个Anim序列,用来遍历
    70. Anim[] anims = new Anim[] { new Anim("老虎"), new Anim("大象"), new Anim("河马") };
    71. Anim anim_t = new Anim(anims);
    72. //foreach遍历
    73. foreach (Anim item in anim_t)
    74. {
    75. Console.WriteLine(item.name);
    76. }
    77. Console.ReadLine();
    78. }
    79. }
    80. }

    输出结果

    1. 调用GetEnumerator方法!
    2. 调用MoveNext方法!
    3. 调用Current方法!
    4. 老虎
    5. 调用MoveNext方法!
    6. 调用Current方法!
    7. 大象
    8. 调用MoveNext方法!
    9. 调用Current方法!
    10. 河马
    11. 调用MoveNext方法!

    通过输出结果可以发现,foreach在运行时会先调用Anim类中的GetEnumerator方法获得一个MIEnumerator,然后通过MIEnumerator的MoveNext方法后移,在调用Current方法获得元素的值,之后一直循环MoveNext和Current方法来遍历整个Anim序列

    不难得出,GetEnumerator方法负责获取枚举器IEnumerator,MoveNext方法负责向下一个元素移动,Current方法负责返回当前的元素的值,Reset方法负责重置枚举器状态

    总结

    IEnumerable接口的作用就是获得枚举器IEnumerator,而IEnumerator接口的作用才是迭代的主体,负责遍历序列

    通过这两段代码就可以看出foreach遍历的原理是先通过IEnumerable获得枚举器IEnumerator,然后通过枚举器的MoveNext和Current方法来遍历序列。

    yield关键字

    此外在迭代器中还有一个关键字需要我们掌握-yield。yield是迭代器中的关键字,用于向枚举对象提供值或发出迭代结束的信号

    yield是一个语法糖,是为了简化迭代器的实现语法才产生的,从上面的讲解不难发现,在枚举器中起实际起作用的就是MoveNext和Current方法。所以C#2提供一个处理方法:yield语句。

    有了yield语句,我们可以在自己的类或者序列中支持foreach迭代而不必实现IEnumerator和IEnumerable接口,我们只要提供一个迭代器,当编辑器检测到迭代器时,会自动生成IEnumerator接口中的Current,MoveNext和Reset方法。

    通过yield return xxx 可以向枚举对象提供值,通过yield break来中止迭代

    下面可以看一个例子

    1. using System;
    2. using System.Collections;
    3. using System.Collections.Generic;
    4. using System.Linq;
    5. using System.Text;
    6. using System.Threading.Tasks;
    7. namespace Csharp_study.Day1
    8. {
    9. class Class1
    10. {
    11. static void Main(string[] args)
    12. {
    13. foreach(int item in GetSingleDigitNumbers())
    14. {
    15. Console.WriteLine(item);
    16. }
    17. IEnumerable<int> GetSingleDigitNumbers()
    18. {
    19. yield return 0;
    20. yield return 1;
    21. yield return 2;
    22. yield return 3;
    23. yield return 4;
    24. yield return 5;
    25. yield return 6;
    26. yield return 7;
    27. yield return 8;
    28. yield return 9;
    29. }
    30. Console.ReadKey();
    31. }
    32. }
    33. }

    输出结果

    1. 0
    2. 1
    3. 2
    4. 3
    5. 4
    6. 5
    7. 6
    8. 7
    9. 8
    10. 9

    我们使用IEnumerable来表示一个int序列,通过yield return 来向这个序列中添加元素,然后直接在foreach就可以遍历这个int序列了,完全不需要MoveNext方法和Current方法就可以实现遍历。通过yield系统会自动生成这两种方法。

    注意,yield只能用于迭代器器,也就是说yield只能用于返回值是IEnumertor和IEnumerable接口及其泛型版本

    如有错漏之处,敬请指正!

  • 相关阅读:
    Linux内核有什么之内存管理子系统有什么第三回 —— 小内存分配(1)
    golang解析excel、csv编码格式
    小程序隐私保护授权处理方式之弹窗组件
    IMX6ULL移植篇-Linux内核源码文件表
    云主机内网通信ping不通问题处理过程
    暑假加餐|有钱人和你想的不一样(第16天)+基于蒙特卡罗的电力系统可靠地评估(Matlab代码实现)
    TPO69 01|Why Snakes Have Forked Tongues|阅读真题精读|10:40-11:40+15:30-16:57
    javascript二维数组(19)不要键名只保留值的算法
    【LeetCode-中等题】78. 子集
    Prometheus-Alertmanager 警报管理器-部署和设置
  • 原文地址:https://blog.csdn.net/qq_52905520/article/details/126441713