outline
In front-end and back-end code development, iterator pattern is widely used in the process of data traversal to simplify our traversal code. This paper mainly compares the iterators in JS and C # from two aspects of implementation and principle, so as to deepen our understanding of the programming mode of iterators.
code implementation
This paper defines JS and C# iterators by iterating an example of student attributes, so as to compare the similarities and differences between the two sides in various aspects.
Implementation of JS iterator
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); }
- Define a JS object, which has no default iterator, so as to ensure that the system must use our custom iterator.
- Add a length attribute to the object, which must be added.
- Define an iterator on the prototype chain of the student object to compare the current pointer and array length each time. If the pointer does not exceed the array range, return the array element; Otherwise, undefined is returned
- The format of the returned object must contain two attributes: value and done.
- Through for of, you can traverse the elements in the modified object. The for of loop automatically omits the iterator attribute and the length attribute.
ES6 syntax sugar version of JS iterator
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]; } }
- The iterator is implemented using the generator function of ES6
- Replace the next() method with yield
- Using the for of iteration, the generated result is the same as that of the ordinary iterator.
Implementation of C# iterator
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; } }
- In C #, we need to implement ienumerator and IEnumerable interfaces to implement iterators
- In order to implement the same class array as JS, the indexer is used instead to access the student's Id and other attributes through index.
- Implement the IEnumerator interface, the MoveNext and Reset methods, and the Current attribute
- Implement IEnumerable interface and rewrite IEnumerable GetEnumerator method. Because the current Student class has implemented the IEnumerator interface, this function can directly return the current object.
Syntax implementation of C# iterator
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]; } } }
- Use the yield keyword instead of implementing IEnumerator methods and properties
- Using foreach iteration, the result is the same as that of a normal iterator.
Principle Comparison
Both JS iterators and C # iterators have defined the implementation specification on the existing framework. We implement the specific iterative function in the custom iterator according to the requirements of the specification. In terms of specific implementation means, they are obviously not used.
JS is based on the idea of functional programming. By defining a method, the method returns an object containing the next method. In each for of loop:
- Call the next method to iterate.
- If the done attribute in the return value of the next method is marked true, the iteration is determined to be over.
C #'s iterator implementation is based on the idea of object-oriented. By implementing the ienumer and IEnumerable interfaces, define the data required in the iteration, including the MoveNext and Reset methods, and have a Current attribute. In the foreach loop,
- Determine whether the iteration is completed by calling MoveNext.
- Get the Current iteration value by using the Current property
Grammatical sugar comparison
JS and C # both use the yield keyword to replace the original iteration details, so as to simplify the development of syntax sugar.
In JS, we no longer need the next method. In C #, we no longer need to implement IEnumerator interface.
The iteration element cannot be modified in the iteration
JS iterator
primitive type
We modify the iteration element in the iterator. The code is as follows:
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); }
Execution results:
From the test results, the primitive type cannot be modified in the iterator.
object type
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); }
Execution results:
The data of the object type can be modified in the iterator.
Cause analysis
JS iterators are mainly implemented through functions. The value of function parameters is copied for parameters of primitive type, while the value of parameters of Object type is directly transferred without copying. Therefore, JS iterators are not intended to be limited in design.
C # iterator
C # iterators are forbidden to modify iteration elements during iteration, mainly including soft limit and should limit.
Soft limit
In the IEnumerator interface, the Current element does not have a set method. In the interface implementation class, if it is simply implemented, there is no need to add a set method.
As shown in MSDN below:
Hard limit
The foreach loop will be restricted when calling the iterator method. Even if we define the set method for Current in the implementation class of ienumeror, it cannot be modified.
The test code is as follows:
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; } }
Execution results:
If the last foreach loop of the Main method is deleted, the execution result is as follows:
From the implementation results:
- We can modify the element value in the indexer
- In the foreach loop, it is forbidden to modify the iteration element, even if we add the set method to Current.
Iterator reuse problem
JS iterator calling mechanism
Let's look at the following code. We added a line of log during iterator initialization:
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); }
Execution results:
We can see that the iterator mechanism of JS is that the generator function will be called again every time the for of loop is used. Since the scope of the function is independent, the loop variable i in the loop starts from 0 every time.
In order to further verify this conclusion, we conduct the following experiments. We force an iterator to be used twice.
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); }
Execution result:
It can be seen from the results that the iterator can only be used normally in the first for of loop, and there is no iteration result in the second for of loop.
The reason is not difficult to understand. After the first for of execution is completed, the value of the iteration variable i of the iterator of the current object is arr.length, so the second for of loop cannot be executed at all.
We add similar log code to the syntax sugar version of JS iterator:
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); }
Execution results:
The execution result is consistent with the non Syntax version.
JS iterators, whether syntactic sugar version or non syntactic sugar version, do not support iterator reuse. Each time a for of loop, a new iterator is generated.
C# iterator calling mechanism
Let's add the log to the code as follows:
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; } }
Execution results:
From the implementation results:
- Because we instantiate the Student class only once, the same iterator is used for both foreach.
- Hashcode proves our inference from another method.
- Every time we call the iterator, we first reset the iterator to ensure that each iteration can proceed normally.
We add the log code in the syntax sugar version 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]; } } }
Execution results:
From the execution results, we can see that the syntax sugar version of C # is consistent with the non syntax sugar version. The syntax sugar version is easier to use. We don't need to display and call the Reset function like the non syntax sugar version.
conclusion
JS and C# iterators are compared as follows:
JS iterator | C # iterator | Supplementary notes | |
---|---|---|---|
OOP implementation | NO | YES | |
Functional programming | YES | NO | |
Support iterator reuse | NO | YES | |
You can modify iteration elements | YES | NO | In JS iterators, the iteration element is an object type that can be modified |