Async in Dart (2) เดี๋ยวจะตอบให้ในอนาคต กับการใช้ Future
Tags: beginners, programming, dart, flutter
Published: 2020-05-01 17:17:06

---
canonical_url: https://www.centrilliontech.co.th/blog/2809/async-in-dart-2-future/
---

ในบทที่แล้ว เราเกริ่นนำเกี่ยวกับการเขียนโปรแกรมแบบไม่ประสานเวลาหรือ **Asynchronous Programming** และระบบผู้อยู่เบื้องหลังการรันโค้ดของภาษา Dart อย่าง **Event Loop** กันไปแล้ว

ในบทนี้ เราจะมาสอนวิธีการสร้าง Async แบบง่ายที่สุด นั่นคือการใช้งานคลาสที่ชื่อว่า **"Future"**

## Future ฉันจะตอบให้แน่นอน..ในอนาคต

ในการเขียนโปรแกรม ปกติเราจะทำงานกับตัวแปรตรงๆ แต่มีเทคนิคหนึ่งที่สามารถสร้างตัวห่อหุ้มตัวแปรเพิ่มอีกชั้นหนึ่ง เรียกว่า **"Wrapper"** หรือ **"Container"** (หรือถ้าคุณเป็นสาย Functional Programming จะมองว่ามันเป็น Functor ตัวนึงก็ยังได้)

ถ้านึกไม่ออกให้นึกถึง List (หรือจะเป็น Map ก็ยังได้นะ แนวคิดเดียวกัน)
```dart
var wrapper = [123];
```
นั่นคือค่า `123` ถูกหุ้มอยู่ในตัวแปร wrapper ... ถ้าเราเรียกใช้ค่าผ่าน `wrapper` ตรงๆ แน่นอนว่าเราจะไม่ได้ค่า 123 ออกมา

ถ้าจะดึงค่าออกมาใช้ ก็ต้องเรียกผ่าน `wrapper[0]` ถึงจะได้ค่า **123** ออกมา

Future ก็คือ wrapper ชนิดหนึ่งที่หุ้มตัวแปรเอาไว้ แต่มีความสามารถเพิ่มเติมนั่นคือมันจะไม่ส่งค่ากลับมาในทันที แต่สามารถหน่วงเวลาเอาไว้ได้ (เลยเป็นที่มาของชื่อ future ยังไงล่ะ)

### Future State
![](https://4.bp.blogspot.com/-sQ8ociKNFeU/XqxHtmikL2I/AAAAAAAASSo/THeyss3l38g4VjE_yqPrDNCG_HJQ8c9sACNcBGAsYHQ/s1600/dart-async-2-01.png)
Future นั้นมีสเตจหรือสถานะอยู่ 3 แบบ
- **Uncomplete**: ยังไม่เสร็จ กำลังรอข้อมูลส่งกลับมา
- **Completed with a value**: เสร็จแล้ว! พร้อมค่าที่ได้กลับมาด้วย
- **Completed with an error**: เสร็จแล้ว! แต่ Error นะจ๊ะ!!

ส่วนใหญ่นั้น Future จะใช้กับการ
- หน่วงเวลา (delay)
- โหลดข้อมูล เช่นเชื่อมต่อ API หรืออ่านข้อมูลจากไฟล์
- หรือเหตุการณ์อะไรก็ตามที่โปรเซสนานมากๆ

## วิธีสร้าง Future

ในภาษา Dart มีวิธีการสร้าง Future หลายวิธีมากๆ โดยแบบที่เบสิกที่สุดคือสร้างเหมือน Object ธรรมดาตัวหนึ่ง เพราะจริงๆ Future เองก็เป็นคลาสๆ หนึ่งเช่นกัน

```dart
var f = Future<int>(...)
```
โดยอ็อบเจค Future นั้นต้องการฟังก์ชันสำหรับสร้าง value กลับมา แบบนี้

```dart
var f = Future<int>((){
    return 100;
})

//หรือละ type ก็ได้
var f = Future((){
    return 100;
})
```
### value
นอกจากการสร้างอ็อบเจคแล้ว เราสามารถสร้าง Future ด้วยกาใช้ factory `value`
```dart
var f = Future.value(100);
```
### delayed
ถ้าไม่อยากตอบค่ากลับในทันที ก็สามารถหน่วงเวลาตอบกลับด้วย delay ซึ่งวิธีการสร้างก็คล้ายๆ กับการสร้างอ็อบเจค แต่ต้องกำหนดพารามิเตอร์เพื่อบอกระยะเวลาที่จะให้ดีเลย์ด้วย `Duration`
```dart
var f = Future.delayed(Duration(minutes: 1), (){
    return 100;
});
```

### error
ในบางครั้ง การตอบค่ากลับก็มีข้อผิดพลาดขึ้นได้ (ถ้าเขียนแบบปกติก็คือการ `throw Exception()` นั่นแหละ)
```dart
var f = Future.error(Exception('เกิดข้อผิดพลาด'));
```

## วิธีการดึงค่าออกมาจาก Future
อย่างที่บอกไปว่า Future นั้นเป็น wrapper จะใช้ค่าได้ต้องดึงค่าออกมาก่อน โดยมี 2 วิธี

### ดึงด้วยเมธอด then()
ตามปกติการใช้ Future จะใช้เมื่อเราไม่รู้ว่าค่าจะถูกส่งกลับมาเมื่อไหร่ ดั้งนั้นเราจะต้องสร้างฟังก์ชันที่เรียกว่า **"Callback"** ซึ่งจะถูกรันเมื่อค่าถูกส่งกลับมา
```dart
var f = Future.value(100);
f.then((value){
    print(value);
});
```
โค้ดข้างบนเวลารันแล้วก็จะได้ค่า `100` ออกมา ซึ่งดูเผินๆ แล้วก็เหมือนกับการเขียนโค้ดแบบปกติ

แต่ถ้าเราเพิ่มโค้ดเข้าไป แบบนี้

```dart
print('start');

var f = Future.value(100);
f.then((value){
    print(value);
});

print('end');
```
คำถามคือเมื่อเราเอาไปรัน แล้วจะได้ผลอะไรออกมา?

ตามปกติเราก็มักจะคิดว่ามันจะรันตามบรรทัด แล้วรันออกมาได้แบบนี้
```
start
100
end
```
แต่ถ้าใครอ่านเรื่อง Isolates และ Event Loop ในบทที่แล้วแล้ว น่าจะเข้าไปได้ว่าฟังก์ชัน callback นั้นจะถูกแยกออกมาเข้าคิวรอรันอยู่ใน Event Loop โดยต้องรอรันโค้ดใน thread หลักให้เสร็จก่อน

ดังนั้น เวลาเอาไปรันแล้วจะได้ผลแบบนี้
```
start ─┬─ run in main thread
end   ─┘
100   ─── run in second thread
```
แล้วถ้าเราใช้ delayed ก็จะได้ผลเหมือนกัน แต่ถูกหน่วงเวลาเอาไว้
```dart
print('start');

var f = Future.delayed(Duration(minutes: 1), (){
    return 100;
});
f.then((value){
    print(value);
})

print('end');
```
ก็จะได้ผลแบบนี้
```
start ─┬─ รันในทันที
end   ─┘
100   ─── รันเมื่อผ่านไป 1 นาที
```

### ลดรูป Future ด้วย await
ในบางกรณีเรามีการโหลดข้อมูลจาก API หลายๆ ต่อ เช่น
```dart
Future<Data1> loadData1(){
    ...
}

Future<Data2> loadData2(int id){
    ...
}
```
แต่มีปัญหาคือ การจะโหลด Data2 จะต้องใช้ค่าจาก Data1 ซะก่อน การโหลดข้อมูลเลยต้องเขียนแบบนี้
```dart
// โหลด data1 ให้เสร็จก่อน
loadData1().then((data1){
    var id = data1.id;
    // จากนั้นดึง id ออกมาแล้วเอาไปใช้โหลด data2 ต่อ
    loadData2().then((data2){
        //TODO
    })
})
```
แน่นอนว่าการเขียนอะไรแบบนี้ทำให้อ่านยากมาก ยิ่งถ้าต้องมีการโหลด data ต่อๆ กันหลายๆๆๆ ทอด โค้ดก็จะซ้อนๆ กันเข้าไปเรื่อยๆ

แต่ถ้าเราใช้คีย์เวิร์ด `await` (และ `async`) มาช่วย ก็จะทำให้เขียนโค้ดง่ายขึ้น วิธีการใช้งานก็คือวางคีย์เวิร์ด `await` ไว้หน้า Future โค้ดทั้งหมดจะถูกแปลงเป็นสไตล์ sync ได้ง่ายๆ **โดยมีข้อแม้คือการใช้ `await` จะต้องอยู่ในฟังก์ชัน async เท่านั้น**
```dart
void main() async {
    var data1 = await loadData1();
    var id = data1.id;
    var data2 = await loadData2(id);
    //TODO
}
```
จะเห็นว่าการใช้ `await` และ `async` นั้นทำให้เราใช้งาน Future ได้ง่ายขึ้นมากๆ แต่ๆๆๆ มีเรื่องสำคัญที่ต้องจำไว้เวลาใช้ คือ...
> การใช้ await ไม่ใช่การแปลงให้โค้ดที่เคยทำงานแบบ Asynchronous กลับมาทำงานแบบ Synchronous นะ!! มันยังทำงานแบบ Asynchronous เหมือนเดิมนั่นแหละ<br>
> แค่รูปแบบการเขียนมันถูกแปลงให้มาอยู่ในรูป sync เท่านั้นเอง เบื้องหลังเวลารัน มันทำงานเหมือนตอนเขียนด้วย `then()` ทั้งหมดนะ คอนเซ็ป Isolates และ Event Loop ก็ยังอยู่ทั้งหมด เวลาใช้งาน ต้องจำไว้ดีๆ ล่ะ

## Error Handlering
นอกจากการตอบค่าคืนแบบปกติ เราสามารถสร้าง Error กลับมาได้ด้วย เช่น
```dart
var f = Future((){
    if(...){
        throw Exception('อุ๊ปส์ มีข้อผิดพลาดเกิดขึ้น');
    }
    return 100;
});
```
ทีนี้ เมื่อเราสร้าง Error ได้ เราก็ต้องมีการดัก/รับมือ Error ที่เกิดขึ้น แบบนี้
- ถ้าใช้ `then` เราสามารถกำหนด `catchError` ต่อได้
- ถ้าใช้ `await` ไม่มีคำสั่งแบบ `catchError` แต่เราใช้ try-catch แบบธรรมดาคลุมโค้ดตรงนั้นได้เลย
```dart
var future = Future(...)

//แบบ then-catch
future.then((data){
    print('ได้รับข้อมูลเรียบร้อย $data');
}).catchError((error){
    print('ไม่สำเร็จล่ะ เพราะ $error');
});

//แบบ await
try{
    var data = await future;
    print('ได้รับข้อมูลเรียบร้อย $data');
} catch(error) {
    print('ไม่สำเร็จล่ะ เพราะ $error');
}
```

### Future Chaining
การใช้ Future นั้น คำสั่ง `then` จริงๆ มีการรีเทิร์นค่ากลับมาเรื่อยๆ เราสามารถสั่ง `then` ต่อกันได้หลายๆ ชั้น แบบนี้
```dart
var future = Future.value(10);

future.then((value1){   //value1 is 10
    return value1 + 20;
}).then((value2){       //value2 is 30
    return value2 * 2;
}).then((value3){       //value3 is 60
    return value3 + 100;
}).then((value4){       //value4 is 160
    print(value4);
})
```
output
```
160
```
ข้อควรระวังคือเวลาเขียนต้อง `then` ต่อจากตัวเดิมเสมอ ถ้าไปเริ่ม `then` จาก Future ตัวเดิมก็จะได้ค่าตัวนั้นนะ
```dart
var f1 = Future.value(1);
//f1 is 1

var f2 = f1.then((x) => x + 1);
//f2 is 2

f1.then((value){
    print(value);
});

f2.then((value){
    print(value);
});

f1.then((x) => x * 100).then((value){
    print(value);
});

f2.then((x) => x * 100).then((value){
    print(value);
});
```
output
```
1
2
100
200
```
## unawaited
ถ้าเราเขียนโค้ดใน IDE บางตัวที่มีการเช็กโค้ดให้เราดีๆ การใช้คำสั่ง `then` อาจจะทำให้เราเจอ **Warning** แบบนี้ (Warning นะ ไม่ใช่ Error)

![](https://2.bp.blogspot.com/-mLOrH16lHWI/XqxNmC10HlI/AAAAAAAASS0/ABLniID9PHYBIJ7BNSHD7STHiEbFbhOngCNcBGAsYHQ/s1600/dart-async-2-02.png)

แล้วส่วนใหญ่ที่ IDE มันแนะนำให้แก้ก็คือการเติม `await` ลงไปข้างหน้าแบบนี้
```dart
await future().then(...);
```
ซึ่งพอเติมลงไป Warning ก็จะหายไป ว้าว! งั้นวิธีแก้แบบนี้ก็ถูกแล้วสินะ

ถูกอะไรล่ะ! มันไม่ Warning แล้วก็จริง แต่การทำงานของโค้ดเปลี่ยนไปโดยสิ้นเชิงเลยนะ (แบบที่บอกไปแล้วไงว่า await คือคำสั่งที่ทำให้โค้ด async กลายเป็น sync)

เหตุผลที่ IDE มันแจ้ง Warning ก็เพราะจริงๆ แล้ว `then` รีเทิร์นค่า Future ตัวใหม่กลับมาเสมอ เมื่อมีค่ารีเทิร์นกลับมาแต่ไม่ใช้งาน มันเลยแจ้ง Warning ไงล่ะ

วิธีแก้คือการใช้ฟังก์ชัน `unawaited` มาครอบเอาไว้ แล้ว Warning ก็จะหายไป

```dart
import 'package:pedantic/pedantic.dart';

void main() {
    unawaited(
        future().then(...)
    );
}
```
แล้วทำไมการเติม `await` ลงไปถึงทำให้มันทำงานเปลี่ยนไป (ใครลองเราไปเขียนโค้ดเทส อาจจะพบว่าจะเติม await หรือไม่เติมก็ได้ผลเหมือนเดิมไม่ใช่เหรอไง)

ลองดูโค้ดนี้
```dart
Future
    .delayed(Duration(seconds: 2), ()=> 'A')
    .then(print);

Future
    .delayed(Duration(seconds: 4), ()=> 'B')
    .then(print);

Future
    .delayed(Duration(seconds: 1), ()=> 'C')
    .then(print);
```
output (ตัวเลขข้างหน้าเป็นหลักเวลา)
```
0|    1sec.  │    │
1| C  ─┘    2sec. │
2| A  ───────┘    │
3|               4sec.
4| B  ────────────┘
5|
```

สังเกตว่าจุดเริ่มต้นของ Future จะเริ่มพร้อมๆ กันหมด ตั้งแต่รันโปรแกรมเลย

แต่ถ้าเราเติม `await` ลงไป จะทำให้ Future แต่ละตัวต้องหยุดรอกันจนกว่าตัวเดิมจะรันเสร็จ แบบนี้

```dart
await Future
    .delayed(Duration(seconds: 2), ()=> 'A')
    .then(print);

await Future
    .delayed(Duration(seconds: 4), ()=> 'B')
    .then(print);

await Future
    .delayed(Duration(seconds: 1), ()=> 'C')
    .then(print);
```
output (ตัวเลขข้างหน้าเป็นหลักเวลา)
```
0|        │
1|        1sec.
2| A  ────┤
3|        │
4|        4sec.
5|        │
6| B  ────┤
7| C  ────1sec.  
8|
```
## Next - ในตอนต่อไป
ในบทนี้เราสอนวิธีการสร้าง Future ไป ก็เหมือนกับการใช้งานตัวแปรแบบธรรมดา**แต่ตอบค่าในอนาคต**

ในบทต่อไปเราจะสอนคลาสอีกชนิดหนึ่งที่ใช้ตอบค่าในอนาคตเช่นเดียวกัน แต่สามารถตอบกลับได้หลายค่าและหลายครั้งมากๆ ไม่เหมือน Future ที่ตอบได้แค่ 1 ค่าเท่านั้น นั่นคือตัวแปรประเภท Stream