IEnumerable罗列器接口的重要性,说一万句话都不太甚。险些一切鸠合都完成了这个接口,Linq的中心也依靠于这个全能的接口。C言语的for轮回写得心烦,foreach就顺畅了许多。
我很喜欢这个接口,但在运用中也碰到不少的疑问,你是不是是也有与我一样的疑心:
(1) IEnumerable 与 IEnumerator究竟有什么区分
(2) 罗列可否越界接见,越界接见是什么效果?为何在罗列中不能转变鸠合的值?
(3) Linq的细致完成究竟是怎样的,比方Skip,它跳过了一些元素,那末这些元素被接见到了么?
(4) IEnumerable 的本质是什么?
(5) IEnumerable 罗列中是不是会构成闭包?多个罗列历程会不会相互滋扰?可否在罗列中动态转变罗列的元素?
….
假如感兴趣,我们接着下面的内容。
最先之前,我们的文章划定,罗列就是IEnumerable,迭代就是IEnumerator,已被实例化(比方ToList())就是鸠合。
1. IEnumerable 与 IEnumerator
IEnumerable只需一个笼统要领:GetEnumerator(),而IEnumerator又是一个迭代器,真正完成了接见鸠合的功用。 IEnumerator只需一个Current属性,MoveNext和Reset两个要领。
有个小题目,只搞一个接见器接口不就得了?为何要两个看起来很轻易殽杂的接口呢?一个叫罗列器,另一个叫迭代器。因为
(1) 完成IEnumerator是个脏活累活,白白加了两个要领一个属性,而且这两个要领实在并不好完成(后面会提到)。
(2) 它须要保护初始状况,晓得怎样MoveNext ,怎样完毕,同时返回迭代的上一个状况,这些并不轻易。
(3)迭代明显黑白线程平安的,每次IEnumerable都邑生成新的IEnumerator,从而构成多个相互不影响的迭代历程。在迭代历程当中,不能修正迭代鸠合,不然不平安。
所以只需你完成了IEnumerable,编译器就会帮我们完成IEnumerator。况且绝大多数状况都是从现有鸠合继续,平常不须要重写MoveNext和Reset要领。 IEnumerable固然另有泛型完成,这个不影响题目的议论。
IEnumerable让我们想起了单向链表,C中须要一个指针域保留下一个节点的信息,那末在IEnumerable中,谁协助保留了这个信息?这个历程占用内存么? 是占在顺序区,照样堆区?
然则,IEnumerable也有它的瑕玷,它没法退却,没法腾跃(只能一个一个的跳过去),而且完成Reset并不轻易,没法完成索引接见。想一想看, 假如是一个实例鸠合的罗列历程,直接返回到第0个元素就可以够了,然则假如这个IEnumerable是冗长的接见链条,想找到最初的根是很难题的!所 以CLR via C#的作者通知你,实在许多Reset的完成根本就是假话,晓得有这个东西就好了,不要太甚依靠它。
2. foreach和MoveNext有区分吗
IEnumerable最大的特点是将接见的历程,交给了被接见者自身控制。在C言语中数组控制权是外部完整控制的。这个接口却在内部封装接见了的历程,进一步提拔了封装性。比方下面:
public class People //定义一个简朴的实体类 { public string Name { get; set; } public int Age { get; set; } } public class PersonList { private readonly List<People> peoples; public PersonList() //为了轻易,组织历程当中插进去元素 { peoples = new List<People>(); for (int i = 0; i < 5; i++) { peoples.Add(new People {Name = "P" + i, Age = 30 + i}); } } public int OldAge = 31; public IEnumerable<People> OlderPeoples { get { foreach (People people in _people) { if (people.Age > OldAge) yield return people; } yield break; } } }
IEnumerable的本质是状况机,它有点相似事宜的观点,将完成丢到表面,完成代码间的穿越(想一想星际穿越),这是Linq的基本。酷炫的迭代器,真的有我们设想的那末简朴么?
在C言语中,数组就是数组,实实在在的内存空间,那末IEnumerable究竟是什么意思呢?假如它由一个真正的鸠合(比方List)完成,那末没题目,也是实实在在的内存,但是假如是上述的例子呢?挑选返回的yield return 只返回了元素,但可以并不存在这个现实的鸠合,假如你将简朴的罗列器的yield return 反编译后看,会发明现实上是一组switch-case, 编译器在背景为我们做了大批的事情。
生成的新迭代器,假如不MoveNext,实在Current是空的,这是为何呢?为何一个迭代器不直接指向头元素呢?
(谢谢回覆:就像C言语的单向链表的头指针一样,如许可以指定一个不包含任何元素的罗列,顺序设计起来更轻易)
foreach每次往前挪动一格,到头了就住手。 等等,你一定它到头了就会住手么?我们来做个实验:
public IEnumerable<People> Peoples1 //直接返回鸠合 { get { return peoples; } }public IEnumerable<People> Peoples2 //包含yield break; { get { foreach (var people in peoples) { yield return people; } yield break; //实在这个用不用都可以 } }
以上两种,是我们罕见的体式格局,注重第二种完成,ReSharper把yield break标成灰色(反复)。
我们再写下以下的测试代码,peopleList鸠合只需五个元素,但尝试去MoveNext 8次。可以把peopleList.Peoples1换成2,3,离别测试。
var peopleList = new PeopleList(); //内部组织函数插进去了五个元素 IEnumerator<People> e1 = peopleList.Peoples1.GetEnumerator(); if (e1.Current == null) { Console.WriteLine("迭代器生成后Current为空"); } int i = 0; while (i<8) //统共只需五个元素,看看一向迭代会发作什么效果 { e1.MoveNext(); if (e1.Current == null) { Console.WriteLine("迭代第{0}次后为空",i); } else { Console.WriteLine("迭代第{0}次后为{1}",i,e1.Current.Name); } i++; }
//PeopleEnumerable1 (直接返回鸠合) 迭代器生成后Current为空 迭代第0次后为P0 迭代第1次后为P1 迭代第2次后为P2 迭代第3次后为P3 迭代第4次后为P4 迭代第5次后为空 迭代第6次后为空 迭代第7次后为空 //PeopleEnumerable2 (不加yield break) 迭代器生成后Current为空 迭代第0次后为P0 迭代第1次后为P1 迭代第2次后为P2 迭代第3次后为P3 迭代第4次后为P4 迭代第5次后为P4 迭代第6次后为P4 迭代第7次后为P4 //PeopleEnumerable2 (加上yield break) 迭代器生成后Current为空 迭代第0次后为P0 迭代第1次后为P1 迭代第2次后为P2 迭代第3次后为P3 迭代第4次后为P4 迭代第5次后为P4 迭代第6次后为P4 迭代第7次后为P4 越界罗列测试效果
真让人受惊,返回原始鸠合,越界以后就返回null了,但如果是MoveNext,不管有无加yield break, 越界迭代后照样返回末了一个元素! 或许就是我们在第1节里提到的,迭代器只返回上一次的状况,因为没法后移,所以就反复返回,那为何List鸠合就不会如许呢?题目留给人人。
(谢谢回覆:越界罗列究竟是null照样末了一个元素的题目,实在没有明确划定,细致看.NET的完成,在.NET Framework中,越界后依然是末了一个元素)。
不过列位看官只管宁神,在foreach的规范罗列历程下,罗列是一定能罗列完的,这就说清楚明了MoveNext和foreach两种在完成上的差别,明显foreach更平安。同时还注重,不能在yield历程当中完成try-catch代码块,为何呢?因为yield形式组合了来自差别位置的代码和逻辑,怎样可以靠编译给每一个援用的代码块加上try-catch?这太庞杂了。
罗列的特征在处置惩罚大数据的时刻很有协助,就是因为它的状况性,一个超大的文件,我只需每次读一部分,就可以够依次的读取下去,直到文件完毕,因为不须要实例化鸠合,内存占用是很低的。对数据库也是云云,每次读取一部分,就可以应对许多难以敷衍的状况。
3.在罗列中修正罗列器参数?
在罗列历程当中,鸠合是不能被修正的,比方在foreach轮回中,假如插进去或许删除一个元素,一定会报运行时非常。有履历的顺序员通知 你,此时用for轮回。for和foreach的本质区分是什么呢?
在MoveNext中,我倏忽转变了罗列的参数,使得它的数据量变多或许变少了,又会发作什么?
Console.WriteLine("不修正OldAge参数"); foreach (var olderPeople in peopleList.OlderPeoples) { Console.WriteLine(olderPeople); } Console.WriteLine("修正了OldAge参数"); i = 0; foreach (var olderPeople in peopleList.OlderPeoples) { Console.WriteLine(olderPeople); i++; if (i ==1) peopleList.OldAge = 33; //只罗列一次后,修正OldAge 的值 }
测试效果是:
不修正OldAge参数 ID:2,NameP2,Age32 ID:3,NameP3,Age33 ID:4,NameP4,Age34 修正了OldAge参数 ID:2,NameP2,Age32 ID:4,NameP4,Age34
可以看到,在罗列历程当中修正了控制罗列的值,能动态转变罗列的行动。上面是在一个yield构造中转变变量的状况,我们再尝尝在迭代器和Lambda表达式的状况(代码略), 获得效果是:
在迭代中修转变量值 ID:2,NameP2,Age32 ID:4,NameP4,Age34 在Lambda表达式中修转变量值 ID:2,NameP2,Age32 ID:4,NameP4,Age34
可以看出,外部修转变量可以控制内部的迭代历程,动态转变了“鸠合的元素”。 这是一个功德,因为它的行动确实是对的;也是坏事:在迭代历程当中,修正了变量的值,上下文语境变化,但是假如还按之前的语境举行处置惩罚,明显就会变成大错。 这里和闭包没紧要。
因而,假如一个罗列须要在上下文会发作变化的状况下坚持原有的行动,就须要手动保留变量的副本。
假如你把两个鸠合A,B用Concat函数依次拼接起来,也就是A-B, 而且不实例化,那末在罗列A的阶段中,修正鸠合B的元素,会报错么? 为何?
比方以下的测试代码:
List<People> peoples=new List<People>(){new People(){Name = "PA"}}; Console.WriteLine("将一个假造罗列A连接到鸠合B,并在罗列A阶段修正鸠合B的元素"); var e8 = peopleList.PeopleEnumerable1.Concat(peoples); i = 0; foreach (var people in e8) { Console.WriteLine(people); i++; if (i == 1) peoples.Add(new People(){Name = "PB"}); //此时还在罗列PeopleEnumerable1阶段
}
假如你想晓得,可以本身做个实验(在我附件里也有这个例子)。留给人人议论。
4. 更多LINQ的议论
你可以在yield中插进去任何代码,这就是耽误(Lazy)的表现,只是须要实行的时刻才实行。 我们不难设想Linq许多函数的完成体式格局,比较有意思的包含Concat,它将两个鸠合连在了一同,就像下面如许:
public static IEnumerable<T> Concat<T>(this IEnumerable<T> source, IEnumerable<T> source2) { foreach (var r in source) { yield return r; } foreach (var r in source2) { yield return r; } }
另有Select, Where都好完成,就不议论了。
Skip怎样完成的呢? 它跳过了鸠合中的一部分元素,我猜是如许的:
public static IEnumerable<T> Skip<T>(this IEnumerable<T> source, int count) { int t = 0; foreach (var r in source) { t++; if(t<=count) continue; yield return r; } }
那末,被跳过的元素,究竟被接见过没有?它的代码被实行了么?
Console.WriteLine("Skip的元素是不是会被接见到?"); IEnumerable<People> e6 = peopleList.PeopleEnumerable1.Select(d => { Console.WriteLine(d); return d; }).Skip(3); Console.WriteLine("只罗列,什么都不做:"); foreach (var r in e6){} Console.WriteLine("转换为实体鸠合,再次罗列"); IEnumerable<People> e7 = e6.ToList(); foreach (var r in e7){}
测试效果以下:
只罗列,什么都不做: ID:0,NameP0,Age30 ID:1,NameP1,Age31 ID:2,NameP2,Age32 ID:3,NameP3,Age33 ID:4,NameP4,Age34 转换为实体鸠合,再次罗列 ID:0,NameP0,Age30 ID:1,NameP1,Age31 ID:2,NameP2,Age32 ID:3,NameP3,Age33 ID:4,NameP4,Age34
可以看出,Skip虽然是跳过,但照样会“接见”元素的,因而会实行分外的操纵,比方lambda表达式,这不管是罗列器照样实体鸠合都是云云。这个角度说,要优化表达式,应该尽量在linq中早的Skip和Take,以削减分外的副作用。
但关于Linq to SQL的完成中,明显Skip是做过分外优化的。我们是不是也能优化Skip的完成,使得上层尽量提拔海量数据下的Skip机能呢?
5. 有关IEnumerable罗列的更多题目
(1) 罗列历程怎样停息?有停息这一说么? 怎样作废?
(2) PLinq的完成道理是什么?它转变的究竟是IEnumerable接口的哪一种特征?是不是产生了乱序罗列?这类乱序罗列究竟是怎样完成?
(3) IEnumerable完成了链条构造,这是Linq的基本,但这个链条的本质是什么?
(4) 因为IEnumerable代表了状况和耽误,因而就不难理解许多异步操纵的本质就是IEnumerable。我有一次口试时刻,问到了异步的本质,你说异步的本质是什么?异步不是多线程!异步的出色,本质上是代码的重新组合,因为长时间的异步操纵就是状况机。。。比方CCR库。此处不准备展开说,因为临时超过了作者的学问贮备,下次再说。
(5) 假如用C言语来完成一样的罗列器,一样酷炫的Linq,不靠编译器能完成么?先不提Lambda的梗,我们用函数指针。
(6) IEnumerable写MapReduce? Linq for MapReduce?
(7) IEnumerable怎样Sort? 实例化为一个鸠合再排序么?假如是一个超大的假造鸠合,怎样优化?
以上就是C#你可以不晓得的圈套, IEnumerable接口的示例代码详解的细致内容,更多请关注ki4网别的相干文章!