了解 C# foreach 内部语句和使用 yield 实现的自定义迭代器

在本期专栏中,我将介绍我们在编程时经常用到的 C# 核心构造(即 foreach 语句)的内部工作原理。了解 foreach 内部行为后,便可以探索如何使用 yield 语句实现 foreach 集合接口,我将对此进行介绍。


虽然 foreach 语句编码起来很容易,但很少有开发者了解它的内部工作原理,这让我感到非常惊讶。例如,你是否注意到 foreach 对数组的运行方式不同于 IEnumberable<T> 集合吗? 你对 IEnumerable<T> 和 IEnumerator<T> 之间关系的熟悉程度如何? 而且,就算你了解可枚举接口,是否熟练掌握使用 yield 语句实现此类接口呢? 


集合类的关键要素


根据定义,Microsoft .NET Framework 集合是至少可实现 IEnumerable<T>(或非泛型 IEnumerable 类型)的类。此接口至关重要,因为至少必须实现 IEnumerable<T> 的方法,才支持迭代集合。

foreach 语句语法十分简单,开发者无需知道元素数量,避免编码过于复杂。不过,运行时并不直接支持 foreach 语句。C# 编译器会转换代码,接下来的部分会对此进行介绍。

foreach 和数组: 下面展示了简单的 foreach 循环,用于迭代整数数组,然后将每个整数打印输出到控制台中:

int[] array = new int[]{1, 2, 3, 4, 5, 6};foreach (int item in array)
{Console.WriteLine(item);
}


在此代码中,C# 编译器为 for 循环创建了等同的 CIL:

int[] tempArray;int[] array = new int[]{1, 2, 3, 4, 5, 6};
tempArray = array;for (int counter = 0; (counter < tempArray.Length); counter++)
{int item = tempArray[counter];Console.WriteLine(item);
}


在此示例中,请注意,foreach 依赖对 Length 属性和索引运算符 ([]) 的支持。借助 Length 属性,C# 编译器可以使用 for 语句迭代数组中的每个元素。

foreach 和 IEnumerable<T> 集合: 虽然前面的代码适用于长度固定且始终支持索引运算符的数组,但并不是所有类型集合的元素数量都是已知的。此外,许多集合类(包括 Stack<T>、Queue<T> 和 Dictionary<TKey and TValue>)都不支持按索引检索元素。因此,需要使用一种更为通用的方法来迭代元素集合。迭代器模式就派上用场了。假设可以确定第一个、第二个和最后一个元素,那么就没有必要知道元素数量,也没有必要支持按索引检索元素。

System.Collections.Generic.IEnumerator<T> 和非泛型 System.Collections.IEnumerator 接口旨在启用迭代器模式(而不是前面介绍的长度索引模式)来迭代元素集合。它们的关系类图如图 1 所示。

图 1:IEnumerator<T> 和 IEnumerator 接口的类图

IEnumerator<T> 派生自的 IEnumerator 包含三个成员。第一个成员是布尔型 MoveNext。使用这种方法,可以在集合中从一个元素移到下一个元素,同时检测是否已枚举完所有项。第二个成员是只读属性 Current,用于返回当前处理的元素。Current 在 IEnumerator<T> 中进行重载,提供按类型分类的实现代码。借助集合类中的这两个成员,只需使用 while 循环,即可迭代集合:

System.Collections.Generic.Stack<int> stack =new System.Collections.Generic.Stack<int>();int number;// ...// This code is conceptual, not the actual code.while (stack.MoveNext())
{number = stack.Current;Console.WriteLine(number);
}


在此代码中,当移到集合末尾时,MoveNext 方法返回 false。这样一来,便无需在循环的同时计算元素数量。

(Reset 方法通常会抛出 NotImplementedException,因此不得进行调用。如果需要重新开始枚举,只要新建一个枚举器即可。)

前面的示例展示的是 C# 编译器输出要点,但实际上并非按此方式进行编译,因为其中略去了两个重要的实现细节:交错和错误处理。

状态为共享: 前面示例中展示的实现代码存在一个问题,即如果两个此类循环彼此交错(一个 foreach 在另一个循环内,两个循环使用相同的集合),集合必须始终有当前元素的状态指示符,以便在调用 MoveNext 时,可以确定下一个元素。在这种情况下,交错的一个循环可能会影响另一个循环。(对于多个线程执行的循环,也是如此。)

为了解决此问题,集合类不直接支持 IEnumerator<T> 和 IEnumerator 接口。而是直接支持另一种接口 IEnumerable<T>,其唯一方法是 GetEnumerator。此方法用于返回支持 IEnumerator<T> 的对象。不必使用始终指示状态的集合类,而是可以使用另一种类,通常为嵌套类,这样便有权访问集合内部,从而支持 IEnumerator<T> 接口,并始终指示迭代循环的状态。枚举器就像是序列中的“游标”或“书签”。可以有多个“书签”,移动其中任何一个都可以枚举集合,与其他枚举器互不影响。使用此模式,foreach 循环的 C# 等同代码如图 2 所示。

图 2:迭代期间始终指示状态的独立枚举器

System.Collections.Generic.Stack<int> stack =new System.Collections.Generic.Stack<int>();int number;
System.Collections.Generic.Stack<int>.Enumeratorenumerator;// ...// If IEnumerable<T> is implemented explicitly,// then a cast is required.// ((IEnumerable<int>)stack).GetEnumerator();enumerator = stack.GetEnumerator();while (enumerator.MoveNext())
{number = enumerator.Current;Console.WriteLine(number);
}


迭代后清除状态: 由于实现 IEnumerator<T> 接口的类始终指示状态,因此有时需要在退出循环后清除状态(因为要么所有迭代均已完成,要么抛出异常)。为此,从 IDisposable 派生 IEnumerator<T> 接口。实现 IEnumerator 的枚举器不一定实现 IDisposable,­但如果实现了,同样也会调用 Dispose。这样可以在退出 foreach 循环后调用 Dispose。因此,最终 CIL 的 C# 等同代码如图 3 所示。

图 3:对集合执行 foreach 的编译结果

System.Collections.Generic.Stack<int> stack =new System.Collections.Generic.Stack<int>();
System.Collections.Generic.Stack<int>.Enumeratorenumerator;
IDisposable disposable;
enumerator = stack.GetEnumerator();try{int number;while (enumerator.MoveNext()){number = enumerator.Current;Console.WriteLine(number);}
}finally{// Explicit cast used for IEnumerator<T>.  disposable = (IDisposable) enumerator;disposable.Dispose();// IEnumerator will use the as operator unless IDisposable  // support is known at compile time.  // disposable = (enumerator as IDisposable);  // if (disposable != null)  // {  //   disposable.Dispose();  // }}


请注意,由于 IEnumerator<T> 支持 IDisposable 接口,因此 using 语句可以将图 3 中的代码简化为图 4 中的代码。

图 4:使用 using 执行错误处理和资源清除

System.Collections.Generic.Stack<int> stack =new System.Collections.Generic.Stack<int>();int number;using(System.Collections.Generic.Stack<int>.Enumeratorenumerator = stack.GetEnumerator())
{while (enumerator.MoveNext()){number = enumerator.Current;Console.WriteLine(number);}
}


然而,重新调用 CIL 并不直接支持 using 关键字。因此,图 3 中的代码实际上是用 C# 更精准表示的 foreach CIL 代码。

在不实现 IEnumerable 的情况下使用 foreach: C# 不要求必须实现 IEnumerable/IEnumerable<T> 才能使用 foreach 迭代数据类型。编译器改用鸭子类型这一概念;它使用 Current 属性和 MoveNext 方法查找可返回类型的 GetEnumerator 方法。鸭子类型涉及按名称搜索,而不依赖接口或显式方法调用。(“鸭子类型”一词源自将像鸭子一样的鸟视为鸭子的怪诞想法,对象必须仅实现 Quack 方法,无需实现 IDuck 接口。) 如果鸭子类型找不到实现的合适可枚举模式,编译器便会检查集合是否实现接口。


迭代器简介


至此,你已了解 foreach 的内部实现代码,是时候了解如何使用迭代器创建 IEnumerator<T>、IEnumerable<T> 和自定义集合对应的非泛型接口的自定义实现代码了。迭代器提供明确的语法,用于指定如何迭代集合类中的数据,尤其是使用 foreach 循环。这样一来,集合的最终用户就可以浏览其内部结构,而无需知道相应结构。

枚举模式存在的问题是,手动实现起来不方便,因为必须始终指示描述集合中的当前位置所需的全部状态。对于列表集合类型类,指示这种内部状态可能比较简单;当前位置的索引就足够了。相比之下,对于需要递归遍历的数据结构(如二叉树),指示状态可能就会变得相当复杂。为了减少实现此模式所带来的挑战,C# 2.0 新增了 yield 上下文关键字,这样类就可以更轻松地决定 foreach 循环如何迭代其内容。

定义迭代器:迭代器是更为复杂的枚举器模式的快捷语法,用于实现类的方法。如果 C# 编译器遇到迭代器,它会将其内容扩展到实现枚举器模式的 CIL代码中。因此,实现迭代器时没有运行时依赖项。由于 C# 编译器通过生成 CIL 代码处理实现代码,因此使用迭代器无法获得真正的运行时性能优势。不过,使用迭代器取代手动实现枚举器模式可以大大提高程序员的工作效率。为了理解这一优势,我将先思考一下,如何在代码中定义迭代器。

迭代器语法: 迭代器提供迭代器接口(IEnumerable<T> 和 IEnumerator<T> 接口的组合)的简单实现代码。图 5 通过创建 GetEnumerator 方法,声明了泛型 BinaryTree<T> 类型的迭代器(尽管还没有实现代码)。

图 5:迭代器接口模式

using System;using System.Collections.Generic;public class BinaryTree<T>:IEnumerable<T>
{public BinaryTree ( T value){Value = value;}#region IEnumerable<T>public IEnumerator<T> GetEnumerator(){// ...  }#endregion IEnumerable<T>public T Value { get; }  // C# 6.0 Getter-only Autoproperty  public Pair<BinaryTree<T>> SubItems { get; set; }
}public struct Pair<T>: IEnumerable<T>
{public Pair(T first, T second) : this(){First = first;Second = second;}public T First { get; }public T Second { get; }#region IEnumerable<T>public IEnumerator<T> GetEnumerator(){yield return First;yield return Second;}#endregion IEnumerable<T>#region IEnumerable MembersSystem.Collections.IEnumeratorSystem.Collections.IEnumerable.GetEnumerator(){return GetEnumerator();}#endregion  // ...}


通过迭代器生成值: 迭代器接口类似于函数,不同之处在于一次生成一系列值,而不是返回一个值。如果为 BinaryTree<T>,迭代器会生成一系列为 T 提供的类型参数值。如果使用非泛型版本 IEnumerator,生成的值将改为类型对象。

为了正确实现迭代器模式,必须始终指示某内部状态,以便在枚举集合的同时跟踪当前位置。如果为 BinaryTree<T>,跟踪树中哪些元素已枚举,以及哪些元素尚未枚举。编译器将迭代器转换成“状态机”,用于跟踪当前位置,并确定如何“将自身移”到下一个位置。

每当迭代器遇到 yield return 语句,都会生成值;控制权会立即重归请求获取此项的调用方。当调用方请求获取下一项时,之前执行的 yield return 语句后面紧接着的代码便会开始执行。在图 6 中,C# 内置数据类型关键字依序返回。

图 6:依序生成一些 C# 关键字

using System;using System.Collections.Generic;public class CSharpBuiltInTypes: IEnumerable<string>
{public IEnumerator<string> GetEnumerator(){yield return "object";yield return "byte";yield return "uint";yield return "ulong";yield return "float";yield return "char";yield return "bool";yield return "ushort";yield return "decimal";yield return "int";yield return "sbyte";yield return "short";yield return "long";yield return "void";yield return "double";yield return "string";}// The IEnumerable.GetEnumerator method is also required    // because IEnumerable<T> derives from IEnumerable.  System.Collections.IEnumeratorSystem.Collections.IEnumerable.GetEnumerator(){// Invoke IEnumerator<string> GetEnumerator() above.    return GetEnumerator();}
}public class Program
{static void Main(){var keywords = new CSharpBuiltInTypes();foreach (string keyword in keywords){Console.WriteLine(keyword);}}
}


图 6 的结果如图 7 所示,即 C# 内置类型的列表。

图 7:图 6 中代码输出的一些 C# 关键字的列表

object
byte
uint
ulong
float
char
bool
ushort
decimal
int
sbyte
short
long
void
double
string


很显然,这需要有更多说明,但由于本期专栏的空间有限,我将在下一期专栏中对此进行说明,给大家留个悬念。我只想说,借助迭代器,可以神奇般地将集合创建为属性,如图图 8 所示。在此示例中,依赖 C# 7.0 元组只是因为这样做比较有趣。若要进一步了解,可以查看源代码,也可以参阅我的“C# 本质论”一书的第 16 章。

图 8:使用 yield return 实现 IEnumerable<T> 属性

IEnumerable<(string City, string Country)> CountryCapitals
{get  {yield return ("Abu Dhabi","United Arab Emirates");yield return ("Abuja", "Nigeria");yield return ("Accra", "Ghana");yield return ("Adamstown", "Pitcairn");yield return ("Addis Ababa", "Ethiopia");yield return ("Algiers", "Algeria");yield return ("Amman", "Jordan");yield return ("Amsterdam", "Netherlands");// ...  }
}


总结


在本期专栏中,我回顾了 C# 版本 1.0 及更高版本中的一项功能,此功能在 C# 2.0 中引入泛型后没有改变太多。虽然此功能使用频繁,但许多人都不了解它的内部工作原理。然后,我通过举例泛泛地介绍了利用 yield return 构造的迭代器模式。

本期专栏的大部分内容截取自我的“C# 本质论”一书 (IntelliTect.com/EssentialCSharp),目前我正在修改“C# 7.0 本质论”。 有关详细信息,请参阅此书的第 14 和 16 章。




Mark Michaelis 是 IntelliTect 的创始人,担任首席技术架构师和培训师。在近二十年的时间里,他一直是 Microsoft MVP,并且自 2007 年以来一直担任 Microsoft 区域总监。Michaelis 还是多个 Microsoft 软件设计评审团队(包括 C#、Microsoft Azure、SharePoint 和 Visual Studio ALM)的成员。他在开发者会议上发表了演讲,并撰写了大量书籍,包括最新的“必备 C# 6.0(第 5 版)”(itl.tc/EssentialCSharp)。可通过他的 Facebook facebook.com/Mark.Michaelis、博客 IntelliTect.com/Mark、Twitter @markmichaelis 或电子邮件 mark@IntelliTect.com 与他取得联系。

感谢以下 IntelliTect 技术专家对本文的审阅: Kevin Bost、Grant Erickson、Chris Finlayson、Phil Spokas 和 Michael Stokesbary

原文地址:https://msdn.microsoft.com/en-us/magazine/mt797654


.NET社区新闻,深度好文,微信中搜索dotNET跨平台或扫描二维码关注

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

JS中的原型

<!DOCTYPE html> <html><head><meta charset"UTF-8"><title></title><script type"text/javascript">/** 原型 prototype* * 我们所创建的每一个函数&#xff0c;解析器都会向函数中添加一个属性prototype* …

扫盲,为什么分布式一定要有Redis?

转载自 扫盲&#xff0c;为什么分布式一定要有Redis? 考虑到绝大部分写业务的程序员&#xff0c;在实际开发中使用 Redis 的时候&#xff0c;只会 Set Value 和 Get Value 两个操作&#xff0c;对 Redis 整体缺乏一个认知。所以我斗胆以 Redis 为题材&#xff0c;对 Redis …

Mybatis insert操作细节【ID】

默认情况下映射文件中插入数据&#xff1a; <insert id"saveUser" parameterType"com.itheima.domain.User">INSERT INTO user (username,address,sex,birthday) VALUES (#{username},#{address},#{sex},#{birthday})</insert>单元测试 Testp…

关于人脸识别最近浏览器打不开摄像头的解决方案

好久没有发公众号啦&#xff0c;因为最近没有在技术方面有更高的提升&#xff0c;关于人脸识别浏览器兼容问题一直很头疼&#xff0c;时至今日&#xff0c;随着浏览器的更新&#xff0c;代码也不得不更新一下了&#xff0c;今天主要是给大家解决一个谷歌浏览器里面的错&#xf…

C# 7 中的模范和实践

原文地址:https://www.infoq.com/articles/Patterns-Practices-CSharp-7 关键点 遵循 .NET Framework 设计指南&#xff0c;时至今日&#xff0c;仍像十年前首次出版一样适用。API 设计至关重要&#xff0c;设计不当的API大大增加错误&#xff0c;同时降低可重用性。始终保持&q…

JS重写toString(),打印想要的值

<!DOCTYPE html> <html> <head><meta charset"UTF-8"><title></title><script type"text/javascript">function Person(name , age , gender){this.name name;this.age age;this.gender gender;}//修改Perso…

Mybatis实体类属性名与数据库类名不对应的两种解决方法

在Mybatis开发时&#xff0c;如果 Bean的属性名与数据库的类名不一致时&#xff0c;CRUD将出现问题。 数据库类名 Bean的属性名&#xff1a;&#xff08;默认&#xff09; 调整Bean中的属性名&#xff1a;&#xff08;测试不一致&#xff09; 此时原有代码将会报错&#xff…

揭开Java 泛型类型擦除神秘面纱

转载自 揭开Java 泛型类型擦除神秘面纱 泛型&#xff0c;一个孤独的守门者。 大家可能会有疑问&#xff0c;我为什么叫做泛型是一个守门者。这其实是我个人的看法而已&#xff0c;我的意思是说泛型没有其看起来那么深不可测&#xff0c;它并不神秘与神奇。泛型是 Java 中一…

ASP.Net防范XSS漏洞攻击的利器HtmlSanitizer

项目名称:HtmlSanitizer NuGet安装指令:Install-Package HtmlSanitizer 官方网站:https://github.com/mganss/HtmlSanitizer 开源协议:MIT 可靠程度:更新活跃,目前已经是3.x版,成熟靠谱。 1、 什么是XSS漏洞? XSS漏洞又称为“跨站脚本”漏洞,指的是网站对于用户输入的内…

阿里巴巴制定了这 16 条

转载自 阿里巴巴制定了这 16 条 本文内容整理自《阿里巴巴Java开发手册 1.4.0》&#xff0c;获取完整版请在公众号后台回复关键字&#xff1a;手册。 1、【强制】存储方案和底层数据结构的设计获得评审一致通过&#xff0c;并沉淀成为文档。 说明&#xff1a;有缺陷的底层数…

使用 Docker 让传统 .NET 应用程序现代化

15 年来&#xff0c;Microsoft .NET Framework 一直都是成功的应用程序平台&#xff0c;在旧版 Framework 和旧版 Windows Server 上运行的业务关键应用程序不计其数。这些传统应用程序仍具有很大的业务价值&#xff0c;但其维护、升级、扩展和管理难度可能很大。同样&#xff…

Mybatis中properties标签的使用

作用域&#xff1a;主配置文件SqlMapConfig.xml中 第一种写法&#xff01; value值使用${properties中property中name} 第二种写法&#xff1a; 创建文件&#xff1a;jdbcConfig.properties jdbc.drivercom.mysql.jdbc.Driver jdbc.urljdbc:mysql://localhost:3306/ee42 jd…

.NET的一点历史故事:作者的一些感想

最近几天通过微博的头条文章平台公开连载了《.NET的一点历史故事》一书的部分草稿。不论是书名还是章节内容&#xff0c;目前真的是仅仅草稿阶段。所以这么早就以连载的方式发布出来&#xff0c;一方面是正在准备在蒙特利尔这边微软技术圈的两场演讲&#xff0c;需要自己尽快恢…

Mybatis中typeAliases标签和package标签

1、typeAliases 主配置文件&#xff1a; <typeAliases><typeAlias type"com.itheima.domain.User" alias"user"></typeAlias></typeAliases>映射配置文件&#xff1a; 2、package 主配置文件<typeAliases><!--<t…

PPT 2010实现使用自定义主题付下载

直接入主题&#xff0c;首先我们打开PPT2010&#xff0c;如下图所示&#xff1a; 点击设计&#xff0c;找到浏览主题&#xff1a; 然后找到我们需要的主题&#xff0c;我已经整理了常用的40套&#xff1a; 最后完美更改

【深圳】.NET 技术分享交流会

随着微软Build 2017的召开&#xff0c;预期将发布.NET Core 2.0 Preview, 邀请深圳地区.NET技术专家和从业人员&#xff0c;一起分享与交流.NET 技术的发展方向,提高.NET技术氛围&#xff0c;发掘.NET高级人才&#xff0c;为改善.NET生态贡献一份力&#xff0c;使.NET技术在深圳…

分布式作业 Elastic Job 如何动态调整

转载自 分布式作业 Elastic Job 如何动态调整 前面分享了两篇分布式作业调度框架 Elastic Job 的介绍及应用实战。 ElasticJob&#xff0d;分布式作业调度神器 分布式作业 Elastic Job 快速上手指南 Elastic Job 提供了简单易用的运维平台&#xff0c;方便用户监控、动态修…

Visual Studio 2017 - Update 2预览版已发布

微软在继续通过Visual Studio Preview项目测试各类新功能&#xff0c;同时会通过公开发布的正式版测试这些新功能在现实世界中的表现情况。通过这种方式&#xff0c;开发者有机会及时了解正在开发的新功能&#xff0c;在开发的早期阶段向微软提供宝贵的反馈&#xff0c;借此为产…

ASP.NET Core开发之HttpContext

ASP.NET Core中的HttpContext开发&#xff0c;在ASP.NET开发中我们总是会经常用到HttpContext。 那么在ASP.NET Core中要如何使用HttpContext呢&#xff0c;下面就来具体学习ASP.NET Core HttpContext。 注入HttpContextAccessor ASP.NET Core中提供了一个IHttpContextAcces…