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


TypeScript 提供了 IterableIterator<T>IteratorResult<T> 2 個 interface,class 只要 implement IterableIterator<T> interface,並由 next() 回傳 IteratorResult<T> interface,就可以實做出 iterator,提供 for of 功能。

Architecture


ts000

Schoool implement 了 IterableIterator<T> interface,且 next() 回傳了 IteratorResult<T> interface。

Implementation


Iterator Interface
ts000

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

定義 Iterator 該有的 method

一個 iterator 一定要有 next(),而 return()throw() 可視需求決定是否 implement。

IterableIterator Interface
ts000

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

定義 Iterator 該有的 property

由於 IterableIterable<T> 繼承了 Iterator<T>,也就是 next()return()throw() 3 個 method 都會存在於 IterableIterator<T>,另外還多了自己的 Symbol.iterator property。

簡單的說,一個 class 要成為 iterator,必須 implement IterableIterator<T> :

  • Symbol.iterator property : 回傳的型別為 IterableIterator<T>
  • next()return()throw() : 3 個 method

才可稱為 iterator 被 for of 所使用。

Iterator<T>IterableIterator<T> 可視為因為 界面隔離原則,所以拆成 2 個 interface,但因為最後由 IterableIterator<T> 繼承了 Iterator<T>,所以對於使用端而言,只要知道 IterableIterator<T> 即可,符合 最小知識原則

IteratorResult Interface
ts000

1
2
3
4
interface IteratorResult<T> {
done: boolean;
value: T;
}

定義 next() 的回傳結果

next() 回傳的 object,一定要有 donevalue 兩個 property。

  • done : false 不是最後一筆資料,true 為最後一筆資料
  • value : 要傳回的 itertator 資料

Student.ts
ts000

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

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

School.ts
ts000

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
import { Student } from './student';

export class School<T extends Student> implements IterableIterator<T> {
private students: T[];
private index = 0;

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

[Symbol.iterator](): IterableIterator<T> {
return this;
}

next(): IteratorResult<T> {
if (this.index < this.students.length) {
return {
done: false,
value: this.students[this.index++]
};
}

return {
done: true,
value: undefined
};
}
}

第 3 行

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

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

第 4 行

1
private students: T[];

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

第 5 行

1
private index = 0;

儲存目前 students 的 index 位置。

第 7 行

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

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

15 行

1
2
3
[Symbol.iterator](): IterableIterator<T> {
return this;
}

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

根據 IterableIterator<T> 規定,一定要提供 Symbol.iterator property,且回傳型別 IterableIterator<T>,而 School 正是 implement IterableIterator<T>,因此可直接回傳。

19 行

1
2
3
4
5
6
7
8
9
10
11
12
13
next(): IteratorResult<T> {
if (this.index < this.students.length) {
return {
done: false,
value: this.students[this.index++]
};
}

return {
done: true,
value: undefined
};
}

next() method 回傳 IteratorResult<T> 物件供使用端取值

根據 Iterator<T> 規定,一定要提供 next(),且回傳值型態為 IteratorResult<T>

若目前的 index 還在範圍內,則 donefalsevaluestudents 目前 index 的資料。

若 index 已經到最後一筆,則 donetruevalueundefined

FAQ

Q : 為什麼要使用 for of 就要 implement IterableIterator<T> interface ?

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

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

經過編譯後會變成

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

let iterator = school.next();
while (!iterator.done) {
console.log(iterator.value.Name);
iterator = school.next();
}

這也是為什麼我們必須 implement IterableIterator<T> interface 的原因,因為真的會用到 IterableIterator<T>next()IteratorResult<T>donevalue

Conclusion

  • Iterator Pattern 讓我們不用暴露 object 內部資料,卻又可以讓使用端以 for of 方式訪問內部資料。
  • Iterator Pattern 在實務上可以讓我們避免回傳一個大陣列影響效能,大陣列需要浪費時間建立,也浪費記憶體。