.NET面试题系列目录html
IEnumerable及IEnumerable的泛型版本IEnumerable<T>是一个接口,它只含有一个方法GetEnumerator。Enumerable这个静态类型含有不少扩展方法,其扩展的目标是IEnumerable<T>。面试
实现了这个接口的类可使用Foreach关键字进行迭代(迭代的意思是对于一个集合,能够逐一取出元素并遍历之)。实现这个接口必须实现方法GetEnumerator。数组
实现一个继承IEnumerable的类型等同于实现方法GetEnumerator。想知道如何实现方法GetEnumerator,不妨思考下实现了GetEnumerator以后的类型在Foreach之下的行为:安全
假设咱们有一个很简单的Person类(例子来自MSDN):ide
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 = 0; i < pArray.Length; i++) { _people[i] = pArray[i]; } } //实现IEnumerable须要实现GetEnumerator方法 public IEnumerator GetEnumerator() { throw new NotImplementedException(); } }
咱们的主函数应当是:this
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方法。spa
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元数据,其解释十分清楚:
经过上面的文字,咱们能够理解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 = 0; } }
这看上去好像没问题,但一执行以后却发现:
经过不断的调试,最后完整的实现应当是:
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 = -1; public PeopleEnumerator(Person[] people) { People = people; } //当程序运行到foreach循环中的in时,就调用这个方法得到下一个person对象 public bool MoveNext() { _position++; //返回一个布尔值,若是为真,则说明枚举没有结束。 //若是为假,说明已经到集合的结尾,就结束这次枚举 return (_position < People.Length); } public void Reset() => _position = -1; }
为何当程序运行到in时,会呼叫方法MoveNext呢?咱们并无直接调用这个方法啊?当你试图查询IL时,就会获得答案。实际上下面两段代码的做用是相同的:
foreach (T item in collection) { ... }
IEnumerator<T> enumerator = collection.GetEnumerator(); while (enumerator.MoveNext()) { T item = enumerator.Current; ... }
注意:第二段代码中,没有呼叫Reset方法,也不须要呼叫。当你呼叫时,你会获得一个异常,这是由于编译器没有实现该方法。
若是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("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是一个语法糖,它的本质是为咱们实现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 "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<string> 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(); }
这部分的文章还能够参考http://www.alloyteam.com/2016/02/generators-in-depth/。
下面这个例子来自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 = 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<Vector> 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); }