.NET面试题系列[9] - IEnumerable
什么是IEnumerable?
IEnumerable及IEnumerable的泛型版本IEnumerable<T>是一个接口,它只含有一个方法GetEnumerator。Enumerable这个静态类型含有很多扩展方法,其扩展的目标是IEnumerable<T>。
实现了这个接口的类可以使用Foreach关键字进行迭代(迭代的意思是对于一个集合,可以逐一取出元素并遍历之)。实现这个接口必须实现方法GetEnumerator。
如何实现一个继承IEnumerable的类型?
实现一个继承IEnumerable的类型等同于实现方法GetEnumerator。想知道如何实现方法GetEnumerator,不妨思考下实现了GetEnumerator之后的类型在Foreach之下的行为:
- 可以获得第一个或当前成员
- 可以移动到下一个成员
- 可以在集合没有下一个成员时退出循环。
假设我们有一个很简单的Person类(例子来自MSDN):
public class Person
{
public Person(string fName, string lName)
{
FirstName = fName;
LastName = lName;
} public string FirstName;
public string LastName;
}
然后我们想构造一个没有实现IEnumerable的类型,其储存多个Person,然后再对这个类型实现IEnumerable。这个类型实际上的作用就相当于Person[]或List<Person>,但我们不能使用它们,因为它们已经实现了IEnumerable,故我们构造一个People类,模拟很多人(People是Person的复数形式)。这个类型允许我们传入一组Person的数组。所以它应当有一个Person[]类型的成员,和一个构造函数,其可以接受一个Person[],然后将Person[]类型的成员填充进去作为初始化。
//People类就是Person类的集合
//但我们不能用List<Person>或者Person[],因为他们都实现了IEnumerable
//我们要自己实现一个IEnumerable
//所以请将People类想象成List<Person>或者类似物
public class People : IEnumerable
{
private readonly Person[] _people;
public People(Person[] pArray)
{
//构造一个Person的集合
_people = new Person[pArray.Length]; for (var i = ; i < pArray.Length; i++)
{
_people[i] = pArray[i];
}
} //实现IEnumerable需要实现GetEnumerator方法
public IEnumerator GetEnumerator()
{
throw new NotImplementedException();
}
}
我们的主函数应当是:
public static void Main(string[] args)
{
//新的Person数组
Person[] peopleArray =
{
new Person("John", "Smith"),
new Person("Jim", "Johnson"),
new Person("Sue", "Rabon"),
}; //People类实现了IEnumerable
var peopleList = new People(peopleArray); //枚举时先访问MoveNext方法
//如果返回真,则获得当前对象,返回假,就退出此次枚举
foreach (Person p in peopleList)
Console.WriteLine(p.FirstName + " " + p.LastName);
}
但现在我们的程序不能运行,因为我们还没实现GetEnumerator方法。
实现方法GetEnumerator
GetEnumerator方法需要一个IEnumerator类型的返回值,这个类型是一个接口,所以我们不能这样写:
return new IEnumerator();
因为我们不能实例化一个接口。我们必须再写一个类PeopleEnumerator,它继承这个接口,实现这个接口所有的成员:Current属性,两个方法MoveNext和Reset。于是我们的代码又变成了这样:
//实现IEnumerable需要实现GetEnumerator方法
public IEnumerator GetEnumerator()
{
return new PeopleEnumerator();
}
在类型中:
public class PeopleEnumerator : IEnumerator
{
public bool MoveNext()
{
throw new NotImplementedException();
} public void Reset()
{
throw new NotImplementedException();
} public object Current { get; }
}
现在问题转移为实现两个方法,它们的功能看上去一目了然:一个负责将集合中Current向后移动一位,一个则将Current初始化为0。我们可以查看IEnumerator元数据,其解释十分清楚:
- Enumerator代表一个类似箭头的东西,它指向这个集合当前迭代指向的成员
- IEnumerator接口类型对非泛型集合实现迭代
- Current表示集合当前的元素,我们需要用它仅有的get方法取得当前元素
- MoveNext方法根据Enumerator是否可以继续向后移动返回真或假
- Reset方法将Enumerator移到集合的开头
通过上面的文字,我们可以理解GetEnumerator方法,就是获得当前Enumerator指向的成员。我们引入一个整型变量position来记录当前的位置,并且先试着写下:
public class PeopleEnumerator : IEnumerator
{
public Person[] _peoples;
public object Current { get; } //当前位置
public int position; //构造函数接受外部一个集合并初始化自己内部的属性_peoples
public PeopleEnumerator(Person[] peoples)
{
_peoples = peoples;
} //如果没到集合的尾部就移动position,返回一个bool
public bool MoveNext()
{
if (position < _peoples.Length)
{
position++;
return true;
}
return false;
} public void Reset()
{
position = ;
}
}
这看上去好像没问题,但一执行之后却发现:
- 当执行到MoveNext方法时,position会先增加1,这导致第一个元素(在位置0)会被遗漏,故position的初始值应当为-1而不是0
- 当前位置变量position显然应该是私有的
- 需要编写Current属性的get方法取出当前位置(position)上的集合成员
通过不断的调试,最后完整的实现应当是:
public class PeopleEnumerator : IEnumerator
{
public Person[] People; //每次运行到MoveNext或Reset时,利用get方法自动更新当前位置指向的对象
object IEnumerator.Current
{
get
{
try
{
//当前位置的对象
return People[_position];
}
catch (IndexOutOfRangeException)
{
throw new InvalidOperationException();
}
}
} //当前位置
private int _position = -; public PeopleEnumerator(Person[] people)
{
People = people;
} //当程序运行到foreach循环中的in时,就调用这个方法获得下一个person对象
public bool MoveNext()
{
_position++;
//返回一个布尔值,如果为真,则说明枚举没有结束。
//如果为假,说明已经到集合的结尾,就结束此次枚举
return (_position < People.Length);
} public void Reset() => _position = -;
}
为什么当程序运行到in时,会呼叫方法MoveNext呢?我们并没有直接调用这个方法啊?当你试图查询IL时,就会得到答案。实际上下面两段代码的作用是相同的:
foreach (T item in collection)
{
...
}
IEnumerator<T> enumerator = collection.GetEnumerator();
while (enumerator.MoveNext())
{
T item = enumerator.Current;
...
}
注意:第二段代码中,没有呼叫Reset方法,也不需要呼叫。当你呼叫时,你会得到一个异常,这是因为编译器没有实现该方法。
使用yield关键字实现方法GetEnumerator
如果iterator本身有实现IEnumerator接口(本例就是一个数组),则可以有更容易的方法:
public IEnumerator GetEnumerator()
{
return _people.GetEnumerator();
}
注意,这个方法没有Foreach的存在,所以如果你改用for循环去迭代这个集合,你得自己去呼叫MoveNext,然后获得集合的下一个成员。而且会出现一个问题,就是你无法知道集合的大小(IEnumerable没有Count方法,只有IEnumerable<T>才有)。此时,可以做个试验,如果我们知道一个集合有3个成员,故意迭代多几次,比如迭代10次,那么当集合已经到达尾部时,将会抛出InvalidOperationException异常。
class Program
{
static void Main(string[] args)
{
Person p1 = new Person("");
Person p2 = new Person("");
Person p3 = new Person(""); People p = new People(new Person[]{p1, p2, p3});
var enumerator = p.GetEnumerator(); //Will throw InvalidOperationException
for (int i = ; i < ; 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<string> items = GetItems();
Console.WriteLine("Begin to iterate the collection.");
var ret = items.ToList();
Console.ReadKey();
} static IEnumerable<string> GetItems()
{
Console.WriteLine("Begin to invoke GetItems()");
yield return "";
yield return "";
yield return "";
}
在上面的例子中,尽管我们呼叫了GetItems方法,先打印出来的句子却是主函数中的句子。这是因为只有在ToList时,才真正开始进行迭代,获得迭代的成员。我们可以使用ILSpy察看编译后的程序集的内容,并在View -> Option的Decompiler中,关闭所有的功能对勾(否则你将仍然只看到一些yield),然后检查Program类型,我们会发现编译器帮我们实现的MoveNext函数,实际上是一个switch。第一个yield之前的所有代码,统统被放在了第一个case中。
bool IEnumerator.MoveNext()
{
bool result;
switch (this.<>1__state)
{
case :
this.<>1__state = -;
Console.WriteLine("Begin to invoke GetItems()");
this.<>2__current = "";
this.<>1__state = ;
result = true;
return result;
case :
this.<>1__state = -;
this.<>2__current = "";
this.<>1__state = ;
result = true;
return result;
case :
this.<>1__state = -;
this.<>2__current = "";
this.<>1__state = ;
result = true;
return result;
case :
this.<>1__state = -;
break;
}
result = false;
return result;
}
如果某个yield之前有其他代码,它会自动包容到它最近的后续的yield的“统治范围”:
static IEnumerable<string> GetItems()
{
Console.WriteLine("Begin to invoke GetItems()");
Console.WriteLine("Begin to invoke GetItems()");
yield return "";
Console.WriteLine("Begin to invoke GetItems()");
yield return "";
Console.WriteLine("Begin to invoke GetItems()");
Console.WriteLine("Begin to invoke GetItems()");
Console.WriteLine("Begin to invoke GetItems()");
yield return "";
}
它的编译结果也是可以预测的:
case :
this.<>1__state = -;
Console.WriteLine("Begin to invoke GetItems()");
Console.WriteLine("Begin to invoke GetItems()");
this.<>2__current = "";
this.<>1__state = ;
result = true;
return result;
case :
this.<>1__state = -;
Console.WriteLine("Begin to invoke GetItems()");
this.<>2__current = "";
this.<>1__state = ;
result = true;
return result;
case :
this.<>1__state = -;
Console.WriteLine("Begin to invoke GetItems()");
Console.WriteLine("Begin to invoke GetItems()");
Console.WriteLine("Begin to invoke GetItems()");
this.<>2__current = "";
this.<>1__state = ;
result = true;
return result;
case :
this.<>1__state = -;
break;
这也就解释了为什么第一个打印出来的句子在主函数中,因为所有不是yield的代码统统都被yield吃掉了,并成为状态机的一部分。而在迭代开始之前,代码是无法运行到switch分支的。
令人瞩目的是,编译器没有实现reset方法,这意味着不支持多次迭代:
void IEnumerator.Reset()
{
throw new NotSupportedException();
}
这部分的文章还可以参考http://www.alloyteam.com/2016/02/generators-in-depth/。
yield只返回,不赋值
下面这个例子来自http://www.cnblogs.com/artech/archive/2010/10/28/yield.html#!comments。不过我认为Artech大大分析的不是很好,我给出自己的解释。
class Program
{
static void Main(string[] args)
{
IEnumerable<Vector> vectors = GetVectors(); //Begin to call GetVectors
foreach (var vector in vectors)
{
vector.X = ;
vector.Y = ;
} //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<Vector> GetVectors()
{
yield return new Vector(, );
yield return new Vector(, );
yield return new Vector(, );
}
}
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 == )
//Error
//vectors.Remove(vector);
//This is OK
vector.X = ;
Console.WriteLine(vector);
}
IEnumerable的缺点
- IEnumerable功能有限,不能插入和删除。
- 访问IEnumerable只能通过迭代,不能使用索引器。迭代显然是非线程安全的,每次IEnumerable都会生成新的IEnumerator,从而形成多个互相不影响的迭代过程。
- 在迭代时,只能前进不能后退。新的迭代不会记得之前迭代后值的任何变化。
.NET面试题系列[9] - IEnumerable的更多相关文章
- .NET面试题系列[10] - IEnumerable的派生类
.NET面试题系列目录 IEnumerable分为两个版本:泛型的和非泛型的.IEnumerable只有一个方法GetEnumerator.如果你只需要数据而不打算修改它,不打算为集合插入或删除任何成 ...
- .NET面试题系列[11] - IEnumerable<T>的派生类
“你每次都选择合适的数据结构了吗?” - Jeffery Zhao .NET面试题系列目录 ICollection<T>继承IEnumerable<T>.在其基础上,增加了Ad ...
- .NET面试题系列[0] - 写在前面
.NET面试题系列目录 .NET面试题系列[1] - .NET框架基础知识(1) .NET面试题系列[2] - .NET框架基础知识(2) .NET面试题系列[3] - C# 基础知识(1) .NET ...
- .NET面试题系列[8] - 泛型
“可变性是以一种类型安全的方式,将一个对象作为另一个对象来使用.“ - Jon Skeet .NET面试题系列目录 .NET面试题系列[1] - .NET框架基础知识(1) .NET面试题系列[2] ...
- .NET面试题系列[15] - LINQ:性能
.NET面试题系列目录 当你使用LINQ to SQL时,请使用工具(比如LINQPad)查看系统生成的SQL语句,这会帮你发现问题可能发生在何处. 提升性能的小技巧 避免遍历整个序列 当我们仅需要一 ...
- .NET面试题系列[14] - LINQ to SQL与IQueryable
.NET面试题系列目录 名言警句 "理解IQueryable的最简单方式就是,把它看作一个查询,在执行的时候,将会生成结果序列." - Jon Skeet LINQ to Obje ...
- .NET面试题系列[13] - LINQ to Object
.NET面试题系列目录 名言警句 "C# 3.0所有特性的提出都是更好地为LINQ服务的" - Learning Hard LINQ是Language Integrated Que ...
- .NET面试题系列[12] - C# 3.0 LINQ的准备工作
"为了使LINQ能够正常工作,代码必须简化到它要求的程度." - Jon Skeet 为了提高园子中诸位兄弟的英语水平,我将重要的术语后面配备了对应的英文. .NET面试题系列目录 ...
- 【转载】.NET面试题系列[0] - 写在前面
原文:.NET面试题系列[0] - 写在前面 索引: .NET框架基础知识[1] - .NET框架基础知识(1) http://www.cnblogs.com/haoyifei/p/5643689.h ...
随机推荐
- C++ 系列:多线程资源收集
Copyright © 1900-2016, NORYES, All Rights Reserved. http://www.cnblogs.com/noryes/ 欢迎转载,请保留此版权声明. -- ...
- ElasticSearch的基本用法与集群搭建
一.简介 ElasticSearch和Solr都是基于Lucene的搜索引擎,不过ElasticSearch天生支持分布式,而Solr是4.0版本后的SolrCloud才是分布式版本,Solr的分布式 ...
- DDD建模案例----“视频课程”场景
接触领域驱动设计DDD有一年多的时间了,中间看过不少书,参与过一些讨论(ENode QQ群).目前对DDD的认知还停留在理论阶段,所以对领域建模非常感兴趣,这里说的建模是指以DDD的思想为指导再加上D ...
- grafana
metrics+grafana elk 这两套系统居家旅游必备啊
- 私有无线传感网 PWSN HLINK
私有无线传感网,我把其叫做 Personal Wireless Sensor Network.此种网络最另众人所知的就是ZIGBEE了.由于在用户不同的使用场景中,对传感网络有许多不同的要求,例如:通 ...
- Mac/IOS/linux获取当前时间包含微秒毫秒的代码
#include <sys/time.h> 1 struct UnityLocalTimeStat { int Year; int Month; int DayOfWeek; int Da ...
- webpack初试
前言: 知道这完儿,没用过.关于webpack有很多介绍了,就不多说了.放几个链接,方便新手理解.这是给纯没用过的人了解的.这里只是简单介绍一下webpack的基本用法.大多内容都是来自webpack ...
- 使用dom4j读取xml连接数据库与之单例模式
使用dom4j读取xml ,加入jar包 dom4j-1.6.1.jar jaxen-1.1-beta-6.jar public class XmlConfigReader { //懒汉式,延迟加载 ...
- HTML基础知识
一个完美的web前端攻城狮,所具备的专业素养有:HTML5.XHTML.CSS3.JavaScript.JQuery.PS.PHP等.所以说,我要学的东西还有很多... 没别得,我也是一个H5的初学者 ...
- 【APICloud】利用sublimetext3编写apicloud
下载sublime text 3 安装插件 使用模糊搜索apicloud有三个插件全部下载下来 安装海马玩模拟器,这是一个安卓的模拟器,进入官网下载后直接安装就可以了. 打开sublime text ...