.NET教程:.NET 面试题之IEnumerable(二)

  .NET教程,这篇文章还是接着上文介绍的第二部分!多的不说,直接献上内容!

使用yield关键字实现方法GetEnumerator

  如果iterator本身有实现IEnumerator接口(本例就是一个数组),则可以有更容易的方法:

  public IEnumerator GetEnumerator()

  {

  return _people.GetEnumerator();

  }

  注意,这个方法没有Foreach的存在,所以如果你改用for循环去迭代这个集合,你得自己去呼叫MoveNext,然后获得集合的下一个成员。而且会出现一个问题,就是你无法知道集合的大小(IEnumerable没有Count方法,只有IEnumerable才有)。

  此时,可以做个试验,如果我们知道一个集合有3个成员,故意迭代多几次,比如迭代10次,那么当集合已经到达尾部时,将会抛出InvalidOperationException异常。

  class Program

  {

  static void Main(string[] args)

  {

  Person p1 = new Person("1");

  Person p2 = new Person("2");

  Person p3 = new Person("3");

  People p = new People(new Person[3]{p1, p2, p3});

  var enumerator = p.GetEnumerator();

  //Will throw InvalidOperationException

  for (int i = 0; i < 5; i++)

  {

  enumerator.MoveNext();

  if (enumerator.Current != null)

  {

  var currentP = (Person) enumerator.Current;

  Console.WriteLine("current is {0}", currentP.Name);

  }

  }

  Console.ReadKey();

  }

  }

  public class Person

  {

  public string Name { get; set; }

  public Person(string name)

  {

  Name = name;

  }

  }

  public class People : IEnumerable

  {

  private readonly Person[] _persons;

  public People(Person[] persons)

  {

  _persons = persons;

  }

  public IEnumerator GetEnumerator()

  {

  return _persons.GetEnumerator();

  }

  }

  使用yield关键字配合return,编译器将会自动实现继承IEnumerator接口的类和上面的三个方法。而且,当for循环遍历超过集合大小时,不会抛出异常,Current会一直停留在集合的最后一个元素。

  public IEnumerator GetEnumerator()

  {

  foreach (Person p in _people)

  yield return p;

  }

  如果我们在yield的上面加一句:

  public IEnumerator GetEnumerator()

  {

  foreach (var p in _persons)

  {

  Console.WriteLine("test");

  yield return p;

  }

  }

  我们会发现test只会打印三次。后面因为已经没有新的元素了,yield也就不执行了,整个Foreach循环将什么都不做。

  yield的延迟执行特性 – 本质上是一个状态机

  关键字yield只有当真正需要迭代并取到元素时才会执行。yield是一个语法糖,它的本质是为我们实现IEnumerator接口。

  static void Main(string[] args)

  {

  IEnumerable items = GetItems();

  Console.WriteLine("Begin to iterate the collection.");

  var ret = items.ToList();

  Console.ReadKey();

  }

  static IEnumerable GetItems()

  {

  Console.WriteLine("Begin to invoke GetItems()");

  yield return "1";

  yield return "2";

  yield return "3";

  }

  在上面的例子中,尽管我们呼叫了GetItems方法,先打印出来的句子却是主函数中的句子。这是因为只有在ToList时,才真正开始进行迭代,获得迭代的成员。我们可以使用ILSpy察看编译后的程序集的内容,并在View -> Option的Decompiler中,关闭所有的功能对勾(否则你将仍然只看到一些yield),然后检查Program类型,我们会发现编译器帮我们实现的MoveNext函数,实际上是一个switch。第一个yield之前的所有代码,统统被放在了第一个case中。

  bool IEnumerator.MoveNext()

  {

  bool result;

  switch (this.<>1__state)

  {

  case 0:

  this.<>1__state = -1;

  Console.WriteLine("Begin to invoke GetItems()");

  this.<>2__current = "1";

  this.<>1__state = 1;

  result = true;

  return result;

  case 1:

  this.<>1__state = -1;

  this.<>2__current = "2";

  this.<>1__state = 2;

  result = true;

  return result;

  case 2:

  this.<>1__state = -1;

  this.<>2__current = "3";

  this.<>1__state = 3;

  result = true;

  return result;

  case 3:

  this.<>1__state = -1;

  break;

  }

  result = false;

  return result;

  }

  如果某个yield之前有其他代码,它会自动包容到它最近的后续的yield的“统治范围”:

  static IEnumerable GetItems()

  {

  Console.WriteLine("Begin to invoke GetItems()");

  Console.WriteLine("Begin to invoke GetItems()");

  yield return "1";

  Console.WriteLine("Begin to invoke GetItems()");

  yield return "2";

  Console.WriteLine("Begin to invoke GetItems()");

  Console.WriteLine("Begin to invoke GetItems()");

  Console.WriteLine("Begin to invoke GetItems()");

  yield return "3";

  }

  它的编译结果也是可以预测的:

  case 0:

  this.<>1__state = -1;

  Console.WriteLine("Begin to invoke GetItems()");

  Console.WriteLine("Begin to invoke GetItems()");

  this.<>2__current = "1";

  this.<>1__state = 1;

  result = true;

  return result;

  case 1:

  this.<>1__state = -1;

  Console.WriteLine("Begin to invoke GetItems()");

  this.<>2__current = "2";

  this.<>1__state = 2;

  result = true;

  return result;

  case 2:

  this.<>1__state = -1;

  Console.WriteLine("Begin to invoke GetItems()");

  Console.WriteLine("Begin to invoke GetItems()");

  Console.WriteLine("Begin to invoke GetItems()");

  this.<>2__current = "3";

  this.<>1__state = 3;

  result = true;

  return result;

  case 3:

  this.<>1__state = -1;

  break;

  这也就解释了为什么第一个打印出来的句子在主函数中,因为所有不是yield的代码统统都被yield吃掉了,并成为状态机的一部分。而在迭代开始之前,代码是无法运行到switch分支的。

  令人瞩目的是,编译器没有实现reset方法,这意味着不支持多次迭代:

  void IEnumerator.Reset()

  {

  throw new NotSupportedException();

  }

  yield只返回,不赋值

  下面这个例子。不过我认为Artech大大分析的不是很好,我给出自己的解释。

  class Program

  {

  static void Main(string[] args)

  {

  IEnumerable vectors = GetVectors();

  //Begin to call GetVectors

  foreach (var vector in vectors)

  {

  vector.X = 4;

  vector.Y = 4;

  }

  //Before this iterate, there are 3 members in vectors, all with X and Y = 4

  foreach (var vector in vectors)

  {

  //But this iterate will change the value of X and Y BACK to 1/2/3

  Console.WriteLine(vector);

  }

  }

  static IEnumerable GetVectors()

  {

  yield return new Vector(1, 1);

  yield return new Vector(2, 3);

  yield return new Vector(3, 3);

  }

  }

  public class Vector

  {

  public double X { get; set; }

  public double Y { get; set; }

  public Vector(double x, double y)

  {

  this.X = x;

  this.Y = y;

  }

  public override string ToString()

  {

  return string.Format("X = {0}, Y = {1}", this.X, this.Y);

  }

  }

  我们进行调试,并将断点设置在第二次迭代之前,此时,我们发现vector的值确实变成4了,但第二次迭代之后,值又回去了,好像被改回来了一样。但实际上,并没有改任何值,yield只是老老实实的吐出了新的三个vector而已。Yield就像一个血汗工厂,不停的制造新值,不会修改任何值。

  从编译后的代码我们发现,只要我们通过foreach迭代一个IEnumerable,我们就会跑到GetVectors方法中,而每次运行GetVectors方法,yield都只会返回全新的三个值为(1,1),(2,2)和(3,3)的vector,仿佛第一次迭代完全没有运行过一样。原文中,也有实验证明了vector创建了六次,实际上每次迭代都会创建三个新的vector。

  解决这个问题的方法是将IEnumerable转为其子类型例如List或数组。

  在迭代的过程中改变集合的状态

  foreach迭代时不能直接更改集合成员的值,但如果集合成员是类或者结构,则可以更改其属性或字段的值。不能在为集合删除或者增加成员,这会出现运行时异常。For循环则可以。

  var vectors = GetVectors().ToList();

  foreach (var vector in vectors)

  {

  if (vector.X == 1)

  //Error

  //vectors.Remove(vector);

  //This is OK

  vector.X = 99;

  Console.WriteLine(vector);

  }

  IEnumerable的缺点

  IEnumerable功能有限,不能插入和删除。

  访问IEnumerable只能通过迭代,不能使用索引器。迭代显然是非线程安全的,每次IEnumerable都会生成新的IEnumerator,从而形成多个互相不影响的迭代过程。

  在迭代时,只能前进不能后退。新的迭代不会记得之前迭代后值的任何变化。

时间: 2024-12-09 11:03:50

.NET教程:.NET 面试题之IEnumerable(二)的相关文章

.NET教程:.NET 面试题之IEnumerable

.NET教程,今天给大家介绍的是:.NET 面试题之IEnumerable ,这是在面试的时候可能会碰到的一道题目,这道题的注解分为了两个部分,这一篇是第一部分! 什么是IEnumerable? IEnumerable及IEnumerable的泛型版本IEnumerable是一个接口,它只含有一个方法GetEnumerator.Enumerable这个静态类型含有很多扩展方法,其扩展的目标是IEnumerable. 实现了这个接口的类可以使用Foreach关键字进行迭代(迭代的意思是对于一个集合

微信公众平台开发教程(六)获取个性二维码

微信公众平台开发教程(六)获取个性二维码 一.功能介绍 在进行推广时,我们可以告诉对方,我们的微信公众账号是什么,客户可以去搜索,然后关注.二维码给我们提供了极大的便捷,只要简单一扫描,即可关注. 如果已经关注过,立刻跳入对话画面.在我们进行推广时,不再是简陋的文字,可以是一个有个性的二维码,想必会很生动. 微信对二维码提供了很好的支持,而且还可以根据需要生成不同场景的二维码.下面我们将介绍如何获取和使用二维码. 注意:限服务号,且进行了微信认证,费用300 二.相关接口 为了满足用户渠道推广分

嵌入式linux面试题解析(二)——C语言部分二

嵌入式linux面试题解析(二)--C语言部分二 1..h头文件中的ifndef/define/endif 的作用?    答:防止该头文件被重复引用. 2.#include 与 #include "file.h"的区别?    答:前者是从Standard Library的路径寻找和引用file.h,而后者是从当前工作路径搜寻并引用file.h. 3.描述实时系统的基本特性    答 :在特定时间内完成特定的任务,实时性与可靠性. 4.全局变量和局部变量在内存中是否有区别?如果有,是

SeaJS入门教程系列之使用SeaJS(二)

SeaJS入门教程系列之使用SeaJS(二) 作者: 字体:[增加 减小] 类型:转载 时间:2014-03-03我要评论 这篇文章主要介绍了SeaJS入门教程系列之使用SeaJS,着重介绍了SeaJS的使用方法.关键方法的使用等,需要的朋友可以参考下 下载及安装 要在项目中使用SeaJS,你所有需要做的准备工作就是下载sea.js然后放到你项目的某个位置.SeaJS项目目前托管在GitHub上,主页为 https://github.com/seajs/seajs/ .可以到其git库的buil

Touch事件or手机卫士面试题整理回答(二)

Touch事件or手机卫士面试题整理回答(二) 自定义控件 1. Touch事件的传递机制 顶级View->父View->子View,不处理逆向返回 OnInterceptTouchEvent(),返回值控制Touch是否向下传递. true中断事件,false不中断事件. onTouchEvent() 返回值决定否处理事件.True消耗事件,False不处理事件向上传递. 2. 轮播动画的实现原理 ViewPage和TextView组合实现图片和标题的效果 ViewPage设置setOnPa

嵌入式linux面试题解析(二)——C语言部分三

嵌入式linux面试题解析(二)--C语言部分三 1.下面的程序会出现什么结果#include <stdio.h>#include <stdlib.h> #include <string.h>void getmemory(char *p){    p=(char *) malloc(100);    strcpy(p,"hello world");}int main( ){    char *str=NULL;    getmemory(str); 

面试题3(二):不修改数组找出重复的数字

// 面试题3(二):不修改数组找出重复的数字// 题目:在一个长度为n+1的数组里的所有数字都在1到n的范围内,所以数组中至// 少有一个数字是重复的.请找出数组中任意一个重复的数字,但不能修改输入的// 数组.例如,如果输入长度为8的数组{2, 3, 5, 4, 3, 2, 6, 7},那么对应的// 输出是重复的数字2或者3. 解题思路: 不能修改数组,可以创建一个长度为n+1的辅助数组,空间复杂度为O(n). 如果用时间换空间的话,可以使用二分查找的思想. 元素范围为1~n,但是有n+1

Python爬虫教程-24-数据提取-BeautifulSoup4(二)

Python爬虫教程-24-数据提取-BeautifulSoup4(二) 本篇介绍 bs 如何遍历一个文档对象 遍历文档对象 contents:tag 的子节点以列表的方式输出 children:子节点以迭代器形式返回 descendants:所有子孙节点 string:用string打印出标签的具体内容,不带有标签,只有内容 案例代码27bs3.py文件:https://xpwi.github.io/py/py%E7%88%AC%E8%99%AB/py27bs3.py # BeautifulS

Docker最全教程——从理论到实战(二)

原文:Docker最全教程--从理论到实战(二) 上篇内容链接: https://www.cnblogs.com/codelove/p/10030439.html Docker和ASP.NET Core Docker 正在逐渐成为容器行业的事实标准,受到 Windows 和 Linux 生态系统领域最重要供应商的支持. (Microsoft 是支持 Docker 的主要云供应商之一.)现在,Docker 基本上已经在各大云或本地的任何数据中心普及了. 如何将.NET程序托管到Docker之中,相