概要
在前端和后端代码开发中,迭代器模式在数据遍历过程中被大量使用,以简化我们遍历代码。本文主要从实现和原理两个方面,比较JS和C#中迭代器,从而加深我们对迭代器这种编程模式的理解。
代码实现
本文通过迭代一个学生属性的实例来自定义JS和C#迭代器,从而比较双方在各个方面的异同点。
JS迭代器的实现
var student = {
0: "XC-001",
1: "Tom",
2: "Room-01",
length : 3
}
student.__proto__[Symbol.iterator] = function(){
var curIdx = 0;
var self = this;
return {
next(){
return curIdx < self.length ?
{value: self[curIdx++], done: false }:
{value: undefined, done: true };
}
};
}
for(var item of student){
console.log(item);
}
- 定义一个JS对象,JS对象没有默认的迭代器,从而保证系统必须使用我们自定义的迭代器。
- 为对象添加length属性,该属性是必须添加的。
- 在student对象的原型链上定义一个迭代器,每次比较当前指针和数组长度,如果指针指向没有超出数组范围,返回数组元素;否则返回undefined
- 返回对象格式必须包含value和done两个属性。
- 通过for of 可以遍历改对象中的元素。 for of 循环会将迭代器属性和length属性自动略去。
JS迭代器的ES6 语法糖版
var student = {
0: "XC-001",
1: "Tom",
2: "Room-01",
length : 3
}
student.__proto__[Symbol.iterator] = function *(){
for(var i =0; i<this.length; i++){
yield this[i];
}
}
- 使用ES6的生成器函数实现迭代器
- 用yield代替next()方法
- 使用for of迭代,生成的结果和普通迭代器是一样的。
C# 迭代器的实现
using System;
using System.Collections;
public class Program
{
public static void Main()
{
Student s = new Student(){
Id = "XC-001",
Name = "Tom",
Classroom = "Room-01"
};
foreach(string prop in s){
Console.WriteLine(prop);
}
}
}
public class Student : IEnumerator, IEnumerable {
public string Id {get; set;}
public string Name {get; set;}
public string Classroom {get; set;}
private int curIdx = -1;
private const int PropertyCount = 3;
public string this[int index]{
get {return GetPropByIndex(index);}
}
object IEnumerator.Current{
get {
return GetPropByIndex(curIdx);
}
}
public string Current{
get {
return GetPropByIndex(curIdx);
}
}
private string GetPropByIndex(int index){
switch (index){
case 0:
return this.Id;
case 1:
return this.Name;
case 2:
return this.Classroom;
default:
throw new IndexOutOfRangeException();
}
}
public bool MoveNext(){
++ curIdx;
return (curIdx < PropertyCount);
}
public void Reset(){
curIdx = -1;
}
IEnumerator IEnumerable.GetEnumerator(){
this.Reset();
return this as IEnumerator;
}
}
- C#中我们需要实现IEnumerator, IEnumerable两个接口来实现迭代器
- 为了实现和JS一样的类数组,使用所引器来代替,以实现对学生的Id等属性通过索引访问。
- 实现IEnumerator接口,实现MoveNext和Reset方法,以及Current属性
- 实现IEnumerable接口,重写IEnumerable.GetEnumerator方法,因为当前Student类已经实现了IEnumerator接口,所以该函数直接返回当前对象即可。
C# 迭代器的语法糖实现
using System;
using System.Collections;
public class Program
{
public static void Main()
{
Student s = new Student(){
Id = "XC-001",
Name = "Tom",
Classroom = "Room-01"
};
foreach(string prop in s){
Console.WriteLine(prop);
}
}
}
public class Student : IEnumerable {
public string Id {get; set;}
public string Name {get; set;}
public string Classroom {get; set;}
private const int PropertyCount = 3;
public string this[int index]{
get {return GetPropByIndex(index);}
}
private string GetPropByIndex(int index){
switch (index){
case 0:
return this.Id;
case 1:
return this.Name;
case 2:
return this.Classroom;
default:
throw new IndexOutOfRangeException();
}
}
IEnumerator IEnumerable.GetEnumerator(){
for(int i=0; i < PropertyCount; ++i){
yield return this[i];
}
}
}
- 使用yield关键字代替实现IEnumerator方法和属性
- 使用foreach迭代,生成的结果和普通迭代器是一样的。
原理比较
无论是JS的迭代器还是C#的迭代器,都是在已有框架上,已经定义好了实现规范,我们在自定义迭代器中,按照规范的要求,实现具体的迭代功能。在具体实现手段上,二者显然是不用的。
JS是基于函数式编程的思想,通过定义一个方法,该方法返回一个包含next方法的对象,在每次for of循环中:
- 调用next方法,进行迭代。
- 如果next方法的返回值中done属性被标记为ture,则判定迭代结束。
C#的迭代器实现是基于面向对象的思想。通过实现IEnumator和IEnumerable接口,定义迭代中需要的数据,包括MoveNext和Reset方法,已经一个Current属性。在foreach循环中,
- 通过调用MoveNext,判定是否迭代完成。
- 通过使用Current属性,来获取当前迭代值
语法糖比较
JS和C#都是使用yield关键字来代替原有的迭代细节,从而达到语法糖简化开发的效果。
JS中我们不再需要next方法。C#中我们不再需要实现IEnumerator接口。
迭代元素不能在迭代中修改的问题
JS迭代器
primitive 类型
我们在迭代器中修改迭代元素,代码如下:
var student = {
0: "XC-001",
1: "Tom",
2: "Room-01",
length : 3
}
student.__proto__[Symbol.iterator] = function *(){
for(var i =0; i<this.length; i++){
yield this[i];
}
}
for(var item of student){
item = "ABC";
}
for(var item of student){
console.log(item);
}
执行结果:
从测试结果来看,primitive类型在迭代器中无法修改。
对象类型
var student = {
0: {name: "XC-001"},
1: {name:"Tom"},
2: {name:"Room-01"},
length : 3
}
student.__proto__[Symbol.iterator] = function *(){
for(var i =0; i<this.length; i++){
yield this[i];
}
}
for(var item of student){
item.name = "ABC";
}
for(var item of student){
console.log(item.name);
}
执行结果:
对象类型的数据可以在迭代器中进行修改。
原因分析
JS的迭代器主要通过函数实现,函数参数的传值,对于primitive类型的参数,是进行复制的,而Object类型的参数是不复制,直接传值的。所以对于JS的迭代器,从设计上看,是没有打算进行限制的。
C#迭代器
C#迭代器是禁止在迭代中修改迭代元素的,主要包括软限制和应限制两种。
软限制
IEnumerator接口中,Current元素并没有set方法,在接口实现类中,如果只是简单实现,也是不需要增加set方法的。
如下面MSDN所示:
硬限制
foreach循环在调用迭代器方法时候,会再进行限制,即使我们在IEnumerator的实现类中为Current定义了set方法,也是不可以修改的。
测试代码如下:
using System;
using System.Collections;
public class Program
{
public static void Main()
{
Student s = new Student(){
Id = "XC-001",
Name = "Tom",
Classroom = "Room-01"
};
s[1] = "Mary";
foreach(string prop in s){
Console.WriteLine(prop);
}
foreach(string prop in s){
prop = "Mary";
}
}
}
public class Student : IEnumerator, IEnumerable {
public string Id {get; set;}
public string Name {get; set;}
public string Classroom {get; set;}
private int curIdx = -1;
private const int PropertyCount = 3;
public string this[int index]{
get {return GetPropByIndex(index);}
set {
SetPropByIndex(index, value);
}
}
object IEnumerator.Current{
get {
return GetPropByIndex(curIdx);
}
}
public string Current{
get {
return GetPropByIndex(curIdx);
}
set {
SetPropByIndex(curIdx, value);
}
}
private string GetPropByIndex(int index){
switch (index){
case 0:
return this.Id;
case 1:
return this.Name;
case 2:
return this.Classroom;
default:
throw new IndexOutOfRangeException();
}
}
private void SetPropByIndex(int index, string val){
switch (index){
case 0:
this.Id = val;
break;
case 1:
this.Name = val;
break;
case 2:
this.Classroom = val;
break;
default:
throw new IndexOutOfRangeException();
}
}
public bool MoveNext(){
++ curIdx;
return (curIdx < PropertyCount);
}
public void Reset(){
curIdx = -1;
}
IEnumerator IEnumerable.GetEnumerator(){
this.Reset();
return this as IEnumerator;
}
}
执行结果:
如果删掉Main方法的最后一个foreach循环,执行结果如下:
从执行结果来看:
- 我们可以在索引器中修改元素值
- 在foreach循环中,禁止修改迭代元素,即使我们给Current增加了set方法。
迭代器复用使用问题
JS迭代器调用机制
我们看如下代码,我们在迭代器初始化时候增加了一行日志:
var student = {
0: "XC-001",
1: "Tom",
2: "Room-01",
length : 3
}
student.__proto__[Symbol.iterator] = function *(){
console.log("The iterator is created!");
for(var i =0; i<this.length; i++){
yield this[i];
}
}
console.log("Loop 1");
for(var item of student){
console.log(item);
}
console.log("Loop 2");
for(var item of student){
console.log(item);
}
执行结果:
我们可以看到,JS的迭代器机制是每次使用for of循环时候,会重新调用生成器函数,由于函数的作用域是独立的,所以循环中的循环变量i每次是从0开始。
为了进一步验证该结论,我们进行如下实验, 我们强制让一个迭代器使用两次。
var student = {
0: "XC-001",
1: "Tom",
2: "Room-01",
length : 3
}
function * generaor(arr){
for(var i =0; i < arr.length; ++i){
yield arr[i];
}
}
var iterator = generaor(student);
console.log("Loop 1");
for(var item of iterator){
console.log(item);
}
console.log("Loop 2");
for(var item of iterator){
console.log(item);
}
执行结果:
从结果可以看出,迭代器只能在第一个for of循环中正常使用,在第二个for of中,没有迭代出结果。
原因不难理解,在第一个for of执行完成后,当前对象的迭代器的迭代变量i的值是arr.length,所以第二个for of循环,根本无法执行。
我们在JS迭代器的语法糖版中加入类似的日志代码:
var student = {
0: "XC-001",
1: "Tom",
2: "Room-01",
length : 3
}
student.__proto__[Symbol.iterator] = function *(){
console.log("The iterator is created!");
for(var i =0; i<this.length; i++){
yield this[i];
}
}
console.log("Loop 1");
for(var item of student){
console.log(item);
}
console.log("Loop 2");
for(var item of student){
console.log(item);
}
执行结果:
执行结果和非语法糖版本一致。
JS迭代器无论是语法糖版本还是非语法糖版本,都不支持迭代器复用,每次for of 循环时,都生成新的迭代器。
C# 迭代器调用机制
我们还是将日志加入代码中,代码如下:
using System;
using System.Collections;
public class Program
{
public static void Main()
{
Student s = new Student(){
Id = "XC-001",
Name = "Tom",
Classroom = "Room-01"
};
Console.WriteLine("Loop 1");
foreach(string prop in s){
Console.WriteLine(prop);
}
Console.WriteLine("Loop 2");
foreach(string prop in s){
Console.WriteLine(prop);
}
}
}
public class Student : IEnumerator, IEnumerable {
public string Id {get; set;}
public string Name {get; set;}
public string Classroom {get; set;}
private int curIdx = -1;
private const int PropertyCount = 3;
public string this[int index]{
get {return GetPropByIndex(index);}
}
object IEnumerator.Current{
get {
return GetPropByIndex(curIdx);
}
}
public string Current{
get {
return GetPropByIndex(curIdx);
}
}
private string GetPropByIndex(int index){
switch (index){
case 0:
return this.Id;
case 1:
return this.Name;
case 2:
return this.Classroom;
default:
throw new IndexOutOfRangeException();
}
}
public bool MoveNext(){
++ curIdx;
return (curIdx < PropertyCount);
}
public void Reset(){
curIdx = -1;
}
IEnumerator IEnumerable.GetEnumerator(){
Console.WriteLine("The has code is " + this.GetHashCode());
this.Reset();
return this as IEnumerator;
}
}
执行结果:
从执行结果来看:
- 因为我们只实例化了一次Student类,所以两次foreach 使用的是同一个迭代器。
- Hashcode从另一个方法证明了我们的推论。
- 我们每次调用迭代器的时候,先将迭代器重置,保证每次迭代都可以正常进行。
我们在语法糖版C#中加入日志代码:
using System;
using System.Collections;
public class Program
{
public static void Main()
{
Student s = new Student(){
Id = "XC-001",
Name = "Tom",
Classroom = "Room-01"
};
Console.WriteLine("Loop 1");
foreach(string prop in s){
Console.WriteLine(prop);
}
Console.WriteLine("Loop 2");
foreach(string prop in s){
Console.WriteLine(prop);
}
}
}
public class Student : IEnumerable {
public string Id {get; set;}
public string Name {get; set;}
public string Classroom {get; set;}
private const int PropertyCount = 3;
public string this[int index]{
get {return GetPropByIndex(index);}
}
private string GetPropByIndex(int index){
switch (index){
case 0:
return this.Id;
case 1:
return this.Name;
case 2:
return this.Classroom;
default:
throw new IndexOutOfRangeException();
}
}
IEnumerator IEnumerable.GetEnumerator(){
Console.WriteLine("The has code is " + this.GetHashCode());
for(int i=0; i < PropertyCount; ++i){
yield return this[i];
}
}
}
执行结果:
从执行结果中我们可以看出,C#的语法糖版本和非语法糖版本是一致的。语法糖版本更加简单易用,我们并不需要像非语法糖版本那样,显示调用Reset函数。
结论
JS和C#迭代器比较如下:
JS迭代器 | C#迭代器 | 补充说明 | |
---|---|---|---|
OOP实现 | NO | YES | |
函数式编程实现 | YES | NO | |
支持迭代器复用 | NO | YES | |
可以修改迭代元素 | YES | NO | JS迭代器中, 迭代元素是对象类型可以进行修改 |