0
点赞
收藏
分享

微信扫一扫

Javascript和C# Linq迭代器比较

Sophia的玲珑阁 2022-01-24 阅读 135

概要

在前端和后端代码开发中,迭代器模式在数据遍历过程中被大量使用,以简化我们遍历代码。本文主要从实现和原理两个方面,比较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);
}
  1. 定义一个JS对象,JS对象没有默认的迭代器,从而保证系统必须使用我们自定义的迭代器。
  2. 为对象添加length属性,该属性是必须添加的。
  3. 在student对象的原型链上定义一个迭代器,每次比较当前指针和数组长度,如果指针指向没有超出数组范围,返回数组元素;否则返回undefined
  4. 返回对象格式必须包含value和done两个属性。
  5. 通过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];
    }
}
  1. 使用ES6的生成器函数实现迭代器
  2. 用yield代替next()方法
  3. 使用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;
	}
	
} 
  1. C#中我们需要实现IEnumerator, IEnumerable两个接口来实现迭代器
  2. 为了实现和JS一样的类数组,使用所引器来代替,以实现对学生的Id等属性通过索引访问。
  3. 实现IEnumerator接口,实现MoveNext和Reset方法,以及Current属性
  4. 实现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];
		}
	}	
} 
  1. 使用yield关键字代替实现IEnumerator方法和属性
  2. 使用foreach迭代,生成的结果和普通迭代器是一样的。

原理比较

无论是JS的迭代器还是C#的迭代器,都是在已有框架上,已经定义好了实现规范,我们在自定义迭代器中,按照规范的要求,实现具体的迭代功能。在具体实现手段上,二者显然是不用的。

JS是基于函数式编程的思想,通过定义一个方法,该方法返回一个包含next方法的对象,在每次for of循环中:

  1. 调用next方法,进行迭代。
  2. 如果next方法的返回值中done属性被标记为ture,则判定迭代结束。

C#的迭代器实现是基于面向对象的思想。通过实现IEnumator和IEnumerable接口,定义迭代中需要的数据,包括MoveNext和Reset方法,已经一个Current属性。在foreach循环中,

  1. 通过调用MoveNext,判定是否迭代完成。
  2. 通过使用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循环,执行结果如下:

在这里插入图片描述
从执行结果来看:

  1. 我们可以在索引器中修改元素值
  2. 在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;
	}
} 

执行结果:
在这里插入图片描述
从执行结果来看:

  1. 因为我们只实例化了一次Student类,所以两次foreach 使用的是同一个迭代器。
  2. Hashcode从另一个方法证明了我们的推论。
  3. 我们每次调用迭代器的时候,先将迭代器重置,保证每次迭代都可以正常进行。

我们在语法糖版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实现NOYES
函数式编程实现YESNO
支持迭代器复用NOYES
可以修改迭代元素YESNOJS迭代器中, 迭代元素是对象类型可以进行修改
举报

相关推荐

C#迭代器

[C#] LINQ之SelectMany和GroupJoin

C# Linq介绍

C# LINQ,SQL

JavaScript迭代器

Linq和C# Lambda表达式

LINQ详解二(C#)

[C#] LINQ之GroupBy

0 条评论