那一次的邂逅:
第一次见到yield的时候,内心中充满了各种声音,这是个啥子鬼扯扯的东西?C#有这个破玩意吗?这是一个关键字?按捺不住内心的疑惑,熟练的打开了宇宙第一IDE ------ VS2015.
卧槽,还真有这个关键字.. 看一下解释 "yield 关键字" ,可以,不和我多逼逼! 微软大佬不愧是微软大佬,就是这么高冷.
魂牵梦绕:
面对大佬如此爱答不理的态度,勾引起了我单纯内心的无限遐想,先从字面意思yield理解:产量,产出。难道这个是一个集合应当具有的属性? 找到我们的老朋友List<T>:
从yield的字面意思上入手,我们把关注点放在IEnumerable接口上,微软给出的解释是:公开的枚举数,该枚举数支持在非泛型集合上进行简单的迭代.
在C#要对一个集合进行遍历有两种方式:
1.For循环
2.Foreach循环
对于Foreach循环,我们要为这个集合定义一个迭代器才能使用微软大佬给我们提供的福利(Foreach).
眉目传情:
控制不住自己的情绪,事不宜迟,先建立一个Test<T>类来实现IEnumerable来试一试:
class Test<T> : IEnumerable { public IEnumerator GetEnumerator() { throw new NotImplementedException(); } }
从代码中可以看出,要实现IEnumerable接口,必须要实现GetEnumerator()方法, 并且返回的类型是IEnumerator, 所以现在我们要先实现IEnumerator这个接口
class Test<T> : IEnumerable { public IEnumerator GetEnumerator() { throw new NotImplementedException(); } class TestEnumerator : IEnumerator { public object Current { get { throw new NotImplementedException(); } } public bool MoveNext() { throw new NotImplementedException(); } public void Reset() { throw new NotImplementedException(); } } }
实现了IEnumerator接口,必须要实现 Current属性,以及MoveNext() 和 Reset()方法。 并且使用了嵌套类TestEnumerator,为什么要使用嵌套类呢,客官莫急莫急,且听臣娓娓道来.
Current : 简单的说就是保存集合当前的值,并且只有get权限.
MoveNext():返回的是个bool类型的值,用来判断集合是否还有下一个元素。 如果集合还有元素则返回true,反之亦然。
Rest():对集合进行重置操作.
为什么要使用嵌套类呢? 因为我们要对Test<T>类的实例进行操作,如果我们使用了一个顶级类,那么会得不到相应的数据(没有权限查看).
添加实现得到的代码如下:
class Test<T> : IEnumerable { private T[] values; //用于接收传递的数组 public Test() { } public Test(T[] values) //构造函数初始化 { this.values = values; } public IEnumerator GetEnumerator() { return new TestEnumerator(this); // 因为需要得到Test 实列的values.(所以使用嵌套类 有权限能访问到values的值) } class TestEnumerator : IEnumerator { private Test<T> parent; private int index;//定义索引 public TestEnumerator(Test<T> parent) { this.parent = parent; index = -1;// 在foreach的时候 会先去调用 GetEnumerator()方法, 所以先给定一个index=-1 初始化。 } public object Current //得到当前集合的值 { get { if (index == -1 || index == parent.values.Length) { throw new InvalidOperationException(); } return this.parent.values[index]; } } public bool MoveNext() //是否还有剩余元素 { if (index != parent.values.Length) { index++; } return index < parent.values.Length; } public void Reset() { index = -1; // 初始化索引值; } } }
在下比较懒,对于Test<T>的内部values只用了数组来存,有时间 我会更新成用链表的数据结构来实现这个方法,但是这个不是今天的重点..
测试下代码:
static void Main(string[] args) { int[] values = { 1, 2, 3, 4, 5, 6 }; Test<int> t = new Test<int>(values); // 在遍历实列的时候 // 1.会先去找到GetEnumerator()方法 // 2.在去初始化 实现IEnumerator接口的类,并且初始化 // 3. 在去MoveNext()判断是否还有下一项 // 4.输出Current属性, 此时代表的就是item foreach (var item in t) { Console.WriteLine(item); // 1,2,3,4,5,6 } Console.ReadKey(); }
初次相识:
说了半天,怎么看不到yield的影子,故弄玄虚,博主你是不是个睿智啊??? 不好意思,让各位大爷久等了,现在闪亮的请出我们今天的主角 ”yield“。
在C#1.0要需要手动实现迭代器,洋洋洒洒几十行的代码,微软大佬你就不能对小弟好一些,使用写短小干练的方式吗? 于是乎,大佬决定相应小弟们的号召,使用关键字”yield“ 对迭代器进行封装.. (微软大佬就是好,没事就给小弟们吃糖(语法糖),牙齿都给我吃的曲黑!!)
不多逼逼,简单粗暴点,直接上代码:
重新实现GetEnumerator()方法:
public IEnumerator GetEnumerator() { for (var index = 0; index < values.Length; index++) { yield return values[index]; } }
简单几行代码就能够完全实现Test<T>类所需要的功能。方法看起来很普通,除了使用了yield return。这条语句告诉编译器这不是一个普通的方法,而是一个需要执行的迭代块(yield block),他返回一个IEnumerator对象,你能够使用迭代块来执行迭代方法并返回一个IEnumerable需要实现的类型,IEnumerator或者对应的泛型。如果实现的是非泛型版本的接口,迭代块返的yield type是Object类型,否则返回的是相应的泛型类型。例如,如果方法实现IEnumerable<String>接口,那么yield返回的类型就是String类型。 在迭代块中除了yield return外,不允许出现普通的return语句。块中的所有yield return 语句必须返回和块的最后返回类型兼容的类型。举个例子,如果方法定义需要返回IEnumeratble<String>类型的话,不能yield return 1 。 需要强调的一点是,对于迭代块,虽然我们写的方法看起来像是在顺序执行,实际上我们是让编译器来为我们创建了一个状态机。这就是在C#1中我们书写的那部分代码---调用者每次调用只需要返回一个值,因此我们需要记住最后一次返回值时,在集合中位置。 当编译器遇到迭代块是,它创建了一个实现了状态机的内部类。这个类记住了我们迭代器的准确当前位置以及本地变量,包括参数。这个类有点类似与我们之前手写的那段代码,他将所有需要记录的状态保存为实例变量。
春宵一刻:
利用yield,可以简单的实现Linq中的where条件过滤(惰性过滤)。
在Test<T>中添加where方法:
public IEnumerable<T> where(Predicate<T> predicate) { foreach (T item in values) { if (predicate(item)) { yield return item; } } }
测试:
static void Main(string[] args) { int[] values = { 1, 2, 3, 4, 5, 6 }; Test<int> t = new Test<int>(values); foreach (var item in t.where(x => x < 5)) { Console.WriteLine(item); // 1,2,3,4 } Console.ReadKey(); }
贤者时间:
C#对许多设计模式进行了间接的实现,使得实现这些模式变得很容易。相对来针对某一特定的设计模式直接实现的的特性比较少。从foreach代码中看出,C#1对迭代器模式进行了直接的支持,但是没有对进行迭代的集合进行有效的支持。对集合实现一个正确的IEnumerable很耗时,容易出错也很枯燥。在C#2中,编译器为我们做了很多工作,为我们实现了一个状态机来实现迭代。
代码中还有些瑕疵的地方,比如没有处理异常,没有使用泛型的接口等。还有yield break的时候,需要释放资源的问题,具体的细节,有时间在写把!
微软大佬,法力无边,使我螺旋升天。--------- by 没有对象的野指针