TypeScript 也可以實做出 Iterator

在 C# 有著名的 IEnumerableIEnumerator interface,只要 implement 這兩個 interface,就可以實踐 Iterator Pattern,但 TypeScript 該如何實做呢 ?

Version


TypeScript 2.6

User Story


1
2
3
4
5
const school = new School<Student>();

for (const student of school) {
console.log(student.Name);
}

School 內包含很多 Student,我們希望直接使用 for ofSchool 抓到所有 Student

for of 為 ES6 的新語法,可以想像成是 C# 的 foreach()

Task


yield 本身回傳就是 iterator,因此若使用 yield 將更精簡。

Architecture


generator000

Schoool implement 了 Iterable<T> interface,且必須回傳 Iterator<T> interface。

Implementation


generator001

Iterable Interface

1
2
3
interface Iterable<T> {
[Symbol.iterator](): Iterator<T>;
}

定義 Iterator 該有的 method

一個 iterator 一定要有 [Symbol.iterator](),並且回傳 Iterator<T>,其中 yield 可直接回傳 Iterator<T>

Iterator003

Iterator Interface

1
2
3
4
5
interface Iterator<T> {
next(value?: any): IteratorResult<T>;
return?(value?: any): IteratorResult<T>;
throw?(e?: any): IteratorResult<T>;
}

定義 [Symbol.iterator]() 回傳結果的 interface

不用擔心要實作這個 interface 很困難,yield 回傳的正是 Iterator<T>,只要簡單使用 yield 即可。

Iterator004

Student.ts

1
2
3
export interface Student {
Name: string;
}

定義資料類型為 Student interface,只有 Name public field。

Iterator005

School.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import { Student } from './student';

export class School<T extends Student> implements Iterable<T> {
private students: T[];

constructor() {
this.students = [
<T>{ Name: 'Sam' },
<T>{ Name: 'Kevin' },
<T>{ Name: 'Jessie' }
];
}

*[Symbol.iterator]() {
for (const iter of this.students)
{
yield iter;
}
}
}

第 3 行

1
export class School<T extends Student> implements Iterable<T> {

為了 School 實踐 iterator,使其 implement IterableIterator<T>,但這個 T 並不是任意的 T,被限定只能繼承於 Student interface。

第 4 行

1
private students: T[];

School 的內部資料 Student[],其中 StudentT 取代。

第 6 行

1
2
3
4
5
6
7
constructor() {
this.students = [
<T>{ Name: 'Sam' },
<T>{ Name: 'Kevin' },
<T>{ Name: 'Jessie' }
];
}

在 constructor 將目前的 students 資料建立起來。

14 行

1
2
3
4
5
6
*[Symbol.iterator]() {
for (const iter of this.students)
{
yield iter;
}
}

Symbol.iterator property 回傳一個 Iterator<T> 物件供使用端操作

根據 Iterable<T> 規定,一定要提供 Symbol.iterator property,且回傳型別 Iterator<T>,而 yield 回傳的正是 Iterator<T>

yieldreturn 不同的是 : 執行到 yield 會立即中止,當一個 for...of 執行時,會繼續下一個 yield,因此不像 return 是回傳一個陣列,然後使用端又不須再一次 for..of

使用 yield 前後只有一次 for...of

另外 ES6 規定,只要回傳的是 iterator,則 function 之前要加 *

除此之外,ES6 提供更精簡的寫法

1
2
3
*[Symbol.iterator]() {
yield* this.students;
}

只要 yield*,後面加上 array,編譯展開相當於

1
2
3
4
for (const iter of this.students)
{
yield iter;
}

Conclusion


  • Iterator Pattern 讓我們不用暴露 object 內部資料,卻又可以讓使用端以 for of 方式訪問內部資料。
  • Iterator Pattern 在實務上可以讓我們避免回傳一個大陣列影響效能,大陣列需要浪費時間建立,也浪費記憶體。
  • 若使用 generator,則程式碼更短,意圖也更明顯,還可以使用更精簡的 yield* 寫法。