Dart 105: มันวนซ้ำได้นะ! มาสร้าง Generator และใช้งาน Iterable กันเถอะ
Tags: beginners, programming, dart, flutter
Published: 2020-04-02 17:01:26

---
canonical_url: https://www.tamemo.com/post/176/dart-105-generatot-and-iterable/
---

ในบทก่อนๆ เรารู้จักตัวแปรประเภท **List** กันมาแล้ว แต่ในภาษา Dart (และภาษาสมัยใหม่อื่นๆ ด้วย) จะมีตัวแปรอีกชนิดนึงที่ **"สามารถนำมาวนลูปได้"** หรือ **"สามารถ access ค่าเป็นลำดับเรียงต่อกันได้"**

ตามปกติเราสามารถสร้างลิสต์ได้แบบนี้
```dart
List<int> items = [1, 2, 3, 4];
```
แต่ถ้าเราลองเข้าไปดู source code ของคลาส **List** เราจะพบว่ามัน `extends` มาจากคลาสๆ หนึ่งที่มีชื่อว่า **Iterable**

แปลว่าเราสามารถสร้างลิสต์แบบนี้ได้

```dart
Iterable<int> items = [1, 2, 3, 4];
```

ซึ่งทั้ง 2 แบบสามารถเอามาวน loop ได้ด้วยนะ
```dart
List<int> items1 = [1, 2, 3, 4];
for(var item in items){
    print(item);
}
//output: 1 2 3 4

Iterable<int> items2 = [1, 2, 3, 4];
for(var item in items){
    print(item);
}
//output: 1 2 3 4
```
ได้ผลเหมือนกันเลยนี่นา? แล้วถ้ามันทำได้เหมือนกันแบบนี้มันจะมี 2 คลาสทำไมกัน แปลว่ามันต้องมีความต่างกันล่ะ

และนั่นแหละ คือหัวข้อที่เราจะมาพูดกันในบทความนี้ คือเรื่องของ **Generator** และ **Iterable**

## by Lazy ขี้เกียจไว้ก่อน จะใช้แล้วค่อยทำ

จากบทที่ 3 เราเคยพูดไปแล้วว่าวิธีการสร้าง List มีหลายวิธีมาก หนึ่งในนั้นคือการสร้างด้วย Generator

สิ่งที่เราต้องเตรียมคือฟังก์ชันที่รับ `index` มาแล้วตอบค่าของ item ในตำแหน่งนั้นกลับไป
```dart
List<int> list = List<int>.generate(10, (index){
    return index + 1;
});
// List [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
```
แต่ลองดูโค้ดต่อไปนี้
```dart
var list = List<int>.generate(3, (index){
    print('creating item from List');
    return index + 1;
});

var iter = Iterable<int>.generate(3, (index){
    print('creating item from Iterable');
    return index + 1;
});
```
คำถามคือ...ถ้าเอาโค้ดแค่นี้เลยนะ มารัน -> จะได้ output ออกมาเป็นยังไง?

คำตอบคือ...
```
creating item from List  ━┓
creating item from List   ┣━ 3ครั้งจากListอย่างเดียว
creating item from List  ━┛
```
นั่นคือฟังก์ชันสำหรับสร้างไอเทมของ **List** ถูกรันจนครบ 3 ครั้งเลย แต่กลับกันคือ **Iterable** นั้นไม่ถูกรันเลยซักครั้ง!

แต่ถ้าเราเขียนโค้ดต่อไปอีกหน่อย เป็นแบบนี้
```dart
var list = List<int>.generate(3, (index){
    print('creating item from List');
    return index + 1;
});

var iter = Iterable<int>.generate(3, (index){
    print('creating item from Iterable');
    return index + 1;
});

print('first item of list: ${list[0]}');
//หรือจะเรียกให้เหมือน iterable คือ list.elementAt(0) ก็ได้
print('first item of iter: ${iter.elementAt(0)}');
//iterable ไม่มีคำสั่ง [] นะ
```
output กลับได้เป็น
```
creating item from List      ━┓
creating item from List       ┣━ 3 ครั้ง
creating item from List      ━┛
first item of list: 1
creating item from Iterable  ━━━ 1 ครั้ง
first item of iter: 1
```
เราจะพบว่าฟังก์ชันของ Iterable เริ่มทำงานแล้วล่ะนะ แต่ทำงานแค่ครั้งเดียว!?

ส่งนี้เรียกว่า **"Lazy Evaluation"** นั่นคือเราจะยังไม่สร้างไอเทมในทันทีที่ถูกสั่งให้สร้าง แต่ถ้าไหร่ก็ตามที่ถูกเรียกใช้ค่อยสร้างตอนนั้น

ถ้าจำลองสถาณการณ์ของโค้ดเมื่อกี้:
- สำหรับ **List**: เปิดมา ไม่ต้องพูดพร่ำทำเพลง สร้างมันรวดเดียวเลย 3 ไอเทม (ยังไม่มีคนเรียกใช้เลยนะ สร้างไว้ก่อน)
- สำหรับ **Iterable**: เปิดมา ยังไม่ทำอะไรทั้งนั้นแหละ (Lazy) แต่เมื่อมีการเรียกไอเทม elementAt(0) มันพบว่าไอเทมตัวที่ 0 น่ะยังไม่ถูกสร้างขึ้นมาเลย แต่เขาจะใช้งานแล้วนี่นา -> งั้นก็สร้างซะตอนนี้เลย!

![](https://1.bp.blogspot.com/-3hnsst7Tfv8/XoX45376zcI/AAAAAAAASOA/T_PCIyLwVUsCzvvDxYvrK0v5i-48Ed4tgCNcBGAsYHQ/s1600/dart6-1.png)

แต่อ่านมาถึงตรงนี้ เราก็อาจจะงงๆ สงสัยกันว่า แล้วจะทำแบบนี้เพื่ออะไรกัน?

ลองดูตัวอย่างต่อไปครับ...

## สร้าง Iterable ด้วยฟังก์ชัน Generator

สมมุติว่าเราจะสร้างฟังก์ชันสำหรับสร้างเลข 1-1,000,000 (หนึ่งถึงหนึ่งล้าน) ขึ้นมาหนึ่งตัวเพื่อเอาไปวนลูปอะไรสักอย่างนึง
```dart
for(var i in getNumbers()){
    print(i);
}
```
ทั่วๆ ไปเราก็อาจจะนึกถึงฟังก์ชันที่สร้าง List คืนมา แบบนี้
```dart
List<int> getNumbers(){
    return [for(var i=1; i<=1000000; i++) i];
}
```
ดังนั้นเวลาโค้ดทำงาน
1. ฟังก์ชันสร้างตัวเลขทั้ง 1 ล้านตัวขึ้นมาก่อน
2. ส่งกลับไปให้ลูปทำการวนปริ๊นค่าทั้ง 1 ล้านตัวนั่นออกมา

แต่ปัญหาจะเกิดขึ้น ถ้าลูปของเรามันสามารถหยุดกลางคันได้
```dart
for(var i in getNumbers()){
    print(i);
    if(i >= 10) break; //ถึง 10 เมื่อไหร่ก็จบลูปได้
}
```
ทีนี้ โค้ดก็จะทำงานเปลี่ยนไป
1. ฟังก์ชันสร้างตัวเลขทั้ง 1 ล้านตัวขึ้นมาก่อน
2. ส่งกลับไปให้ลูปทำการวนปริ๊นค่า
3. แต่คราวนี้ ปริ๊นไปแค่ 10 ตัวก็จบลูปแล้ว (ตัวเลขที่เหลืออีก 999,900 ก็ไม่ได้ใช้ แต่สร้างมาแล้วนะ)

ถ้าถามว่าโลจิคแบบนี้มันโอเคมัน มันก็ได้อยู่นะ โลจิคไม่ได้เปลี่ยนไป **แต่ในแง่ประสิทธิภาพ performance ของโปรแกรมนี่ไม่ผ่านแน่นอน**

> เพราะแบบนี้ ถ้าเราสร้างฟังก์ชัน `getNumbers()` ด้วย Iterable จะทำให้ประหยัด performance มากๆ เพราะถึงจะกำหนดว่าต้องสร้างเลข 1-1,000,000 แต่เวลาเรียกใช้ ใช้แค่10ตัว มันก็จะสร้างไอเทมขึ้นมาแค่10เท่าที่ต้องใช้ ไม่ต้องสร้างจริงทั้งหนึ่งล้านตัว

## sync* ฟังก์ชันที่หยุดกลางคันได้

นอกจากวิธีใช้ factory function `Iterable.generate` สร้าง Iterable ขึ้นมา จริงๆ ยังมีอีกวิธีในการสร้างมัน นั่นคือการใช้ **"Generator Function"**

ถ้าเราสร้างฟังก์ชันที่ตอบ `int` ธรรมดาก็จะได้โค้ดหน้าตาแบบนี้
```dart
int getNumber(){
    return 1;
}
```
แต่ถ้าเราจะตอบกลับเป็น Iterable เราจะต้องเขียนฟังก์ชันแบบนี้

```dart
Iterable<int> getNumbers() sync* {
    yield 1;
}
```
จุดสังเกตคือมี 2 อย่างที่เปลี่ยนไป นั่นคือ
- เราไม่ได้ใช้คีย์เวิร์ด `return` อีกต่อไป แต่ใช้ `yield` แทน
- มีการกำหนดฟังก์ชันเป็น `sync*` ด้วยนะ (จะมาพร้อม yield เสมอ คือถ้าต้องการใช้คำสั่ง `yield` ในฟังก์ชันจะต้องประกาศให้ฟังก์ชันเป็น `sync*` เสมอ)

ความแตกต่างระหว่าง `yield` กับ `return` คือทันทีที่ return ทำงาน ฟังก์ชันจะจบการทำงานทันที แต่สำหรับ yield นั้นจะยังไม่หยุดจนกว่าจะจบฟังก์ชัน เช่น

```dart
Iterable<int> getNumbers() sync* {
    yield 1;
    yield 2;
    yield 3;
}

for(var number in getNumbers()){
    print(number);
}
//output: 1 2 3
```
> ฟังก์ชันทั่วๆ ไปจะเป็นฟังก์ชันประเภท `sync` แต่เราจะถือว่าไม่ต้องเติมคีย์เวิร์ดนี้ลงไปนะ

> ภาษาทั่วไป การประกาศฟังก์ชันเป็น Generator จะใช้ `*` เติมไม่ก่อนก็หลังฟังก์ชัน เช่น `function* f()` แต่ใน Dart จะต้องระบุคีย์เวิร์ด `sync*` ลงไปด้วย เพราะเดี๋ยวในบทต่อๆ ไปเราจะเจอกับ `async*` ด้วย!!
> แต่เดี๋ยวขอเก็บไว้ก่อนนะ ไว้ค่อยกลับมาพูดกัน

ข้อดีอีกอย่างของ sync* function คือเราสามารถเขียนโค้ดแบบธรรมดาได้เลย เช่น

**ex.** ฟังก์ชันสำหรับสร้างลำดับเลขฟีโบนัคชี (อ่านเพิ่มเติมใน [Fibonacci Number](https://en.wikipedia.org/wiki/Fibonacci_number))

```dart
Iterable<int> fibonacci() sync* {
    var first = 1;
    var second = 1;
    yield first;
    yield second;
    while(true){
        var next = first + second;
        yield next;
        first = second;
        second = next;
    }
}
```
จะเห็นว่าเราจะเขียนโค้ดได้แบบไม่ต้องแคร์เลยว่าการวนลูปของเราจะต้องจบเมื่อไหร่ สามารถวนลูปไปได้เรื่อยๆ เลย แล้วอยากจะตอบค่ากลับก็ `yield` ได้เลย 

แต่ถ้าเขียน infinity iterable แบบนี้ เวลาใช้งาน**ต้องระวัง!!** เพราะจะต้องมีการกำหนดขนาดก่อนใช้เสมอ เช่น
```dart
for(var number in fibonacci().take(10)){
    print(number);
}
```
แบบนี้เราใช้ฟังก์ชัน `take` ในการดึงค่าฟีโบนัคชี 10 ตัวแรกเท่านั้นพอ

## Nested sync* function ซ้อนกันก็ยังได้นะ
ถ้าเรามีฟังก์ชัน Generator 2 ตัว

```dart
Iterable<int> threeTime(int x) sync* {
    for(var i=0; i<3; i++){
        yield x;
    }
}

Iterable<int> oneToThree() sync* {
    for(var i=1; i<=3; i++){
        for(var item in threeTime(i)){
            yield item;
        }
    }
}
```
เราสร้างฟังก์ชันมา 2 ตัว **threeTime**จะรับเลขไปหนึ่งตัว แล้วปริ๊นเลขตัวนั้น 3 ครั้ง, **oneToThree** วนลูปเรียกฟังก์ชันแรกอีก 3 ครั้ง

ถ้าเรารัน **oneToThree** ก็จะได้ output แบบนี้
```
10, 11, 12, 20, 21, 22, 30, 31, 32
```
แต่เราสามารถเขียนย่อได้อีก โดยใช้คำสั่ง `yield*` 
```dart
Iterable<int> oneToThree() sync* {
    for(var i=1; i<=3; i++){
       yield* threeTime(i);
    }
}
```
ก็คือ ถ้าจะใช้ sync* เรียก sync* จะต้องสั่งด้วย `yield*` นั่นเอง





---

ก็จบลงไปแล้วกับซีรีส์แนะนำภาษา Dart เบื้องต้น

ต่อไปจะเป็นบทความเกี่ยวกับการใช้ Async ในภาษา Dart กัน ... แล้วหลังจากนั้นก็จะเป็นซีรีส์การนำภาษา Dart มาเขียน Application แบบ cross-platform ด้วยเฟรมเวิร์คที่ชื่อว่า Flutter