Async in Dart (2) เดี๋ยวจะตอบให้ในอนาคต กับการใช้ Future
Ta
Posted on May 1, 2020
ในบทที่แล้ว เราเกริ่นนำเกี่ยวกับการเขียนโปรแกรมแบบไม่ประสานเวลาหรือ Asynchronous Programming และระบบผู้อยู่เบื้องหลังการรันโค้ดของภาษา Dart อย่าง Event Loop กันไปแล้ว
ในบทนี้ เราจะมาสอนวิธีการสร้าง Async แบบง่ายที่สุด นั่นคือการใช้งานคลาสที่ชื่อว่า "Future"
Future ฉันจะตอบให้แน่นอน..ในอนาคต
ในการเขียนโปรแกรม ปกติเราจะทำงานกับตัวแปรตรงๆ แต่มีเทคนิคหนึ่งที่สามารถสร้างตัวห่อหุ้มตัวแปรเพิ่มอีกชั้นหนึ่ง เรียกว่า "Wrapper" หรือ "Container" (หรือถ้าคุณเป็นสาย Functional Programming จะมองว่ามันเป็น Functor ตัวนึงก็ยังได้)
ถ้านึกไม่ออกให้นึกถึง List (หรือจะเป็น Map ก็ยังได้นะ แนวคิดเดียวกัน)
var wrapper = [123];
นั่นคือค่า 123
ถูกหุ้มอยู่ในตัวแปร wrapper ... ถ้าเราเรียกใช้ค่าผ่าน wrapper
ตรงๆ แน่นอนว่าเราจะไม่ได้ค่า 123 ออกมา
ถ้าจะดึงค่าออกมาใช้ ก็ต้องเรียกผ่าน wrapper[0]
ถึงจะได้ค่า 123 ออกมา
Future ก็คือ wrapper ชนิดหนึ่งที่หุ้มตัวแปรเอาไว้ แต่มีความสามารถเพิ่มเติมนั่นคือมันจะไม่ส่งค่ากลับมาในทันที แต่สามารถหน่วงเวลาเอาไว้ได้ (เลยเป็นที่มาของชื่อ future ยังไงล่ะ)
Future State
Future นั้นมีสเตจหรือสถานะอยู่ 3 แบบ
- Uncomplete: ยังไม่เสร็จ กำลังรอข้อมูลส่งกลับมา
- Completed with a value: เสร็จแล้ว! พร้อมค่าที่ได้กลับมาด้วย
- Completed with an error: เสร็จแล้ว! แต่ Error นะจ๊ะ!!
ส่วนใหญ่นั้น Future จะใช้กับการ
- หน่วงเวลา (delay)
- โหลดข้อมูล เช่นเชื่อมต่อ API หรืออ่านข้อมูลจากไฟล์
- หรือเหตุการณ์อะไรก็ตามที่โปรเซสนานมากๆ
วิธีสร้าง Future
ในภาษา Dart มีวิธีการสร้าง Future หลายวิธีมากๆ โดยแบบที่เบสิกที่สุดคือสร้างเหมือน Object ธรรมดาตัวหนึ่ง เพราะจริงๆ Future เองก็เป็นคลาสๆ หนึ่งเช่นกัน
var f = Future<int>(...)
โดยอ็อบเจค Future นั้นต้องการฟังก์ชันสำหรับสร้าง value กลับมา แบบนี้
var f = Future<int>((){
return 100;
})
//หรือละ type ก็ได้
var f = Future((){
return 100;
})
value
นอกจากการสร้างอ็อบเจคแล้ว เราสามารถสร้าง Future ด้วยกาใช้ factory value
var f = Future.value(100);
delayed
ถ้าไม่อยากตอบค่ากลับในทันที ก็สามารถหน่วงเวลาตอบกลับด้วย delay ซึ่งวิธีการสร้างก็คล้ายๆ กับการสร้างอ็อบเจค แต่ต้องกำหนดพารามิเตอร์เพื่อบอกระยะเวลาที่จะให้ดีเลย์ด้วย Duration
var f = Future.delayed(Duration(minutes: 1), (){
return 100;
});
error
ในบางครั้ง การตอบค่ากลับก็มีข้อผิดพลาดขึ้นได้ (ถ้าเขียนแบบปกติก็คือการ throw Exception()
นั่นแหละ)
var f = Future.error(Exception('เกิดข้อผิดพลาด'));
วิธีการดึงค่าออกมาจาก Future
อย่างที่บอกไปว่า Future นั้นเป็น wrapper จะใช้ค่าได้ต้องดึงค่าออกมาก่อน โดยมี 2 วิธี
ดึงด้วยเมธอด then()
ตามปกติการใช้ Future จะใช้เมื่อเราไม่รู้ว่าค่าจะถูกส่งกลับมาเมื่อไหร่ ดั้งนั้นเราจะต้องสร้างฟังก์ชันที่เรียกว่า "Callback" ซึ่งจะถูกรันเมื่อค่าถูกส่งกลับมา
var f = Future.value(100);
f.then((value){
print(value);
});
โค้ดข้างบนเวลารันแล้วก็จะได้ค่า 100
ออกมา ซึ่งดูเผินๆ แล้วก็เหมือนกับการเขียนโค้ดแบบปกติ
แต่ถ้าเราเพิ่มโค้ดเข้าไป แบบนี้
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 ก็จะได้ผลเหมือนกัน แต่ถูกหน่วงเวลาเอาไว้
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 หลายๆ ต่อ เช่น
Future<Data1> loadData1(){
...
}
Future<Data2> loadData2(int id){
...
}
แต่มีปัญหาคือ การจะโหลด Data2 จะต้องใช้ค่าจาก Data1 ซะก่อน การโหลดข้อมูลเลยต้องเขียนแบบนี้
// โหลด data1 ให้เสร็จก่อน
loadData1().then((data1){
var id = data1.id;
// จากนั้นดึง id ออกมาแล้วเอาไปใช้โหลด data2 ต่อ
loadData2().then((data2){
//TODO
})
})
แน่นอนว่าการเขียนอะไรแบบนี้ทำให้อ่านยากมาก ยิ่งถ้าต้องมีการโหลด data ต่อๆ กันหลายๆๆๆ ทอด โค้ดก็จะซ้อนๆ กันเข้าไปเรื่อยๆ
แต่ถ้าเราใช้คีย์เวิร์ด await
(และ async
) มาช่วย ก็จะทำให้เขียนโค้ดง่ายขึ้น วิธีการใช้งานก็คือวางคีย์เวิร์ด await
ไว้หน้า Future โค้ดทั้งหมดจะถูกแปลงเป็นสไตล์ sync ได้ง่ายๆ โดยมีข้อแม้คือการใช้ await
จะต้องอยู่ในฟังก์ชัน async เท่านั้น
void main() async {
var data1 = await loadData1();
var id = data1.id;
var data2 = await loadData2(id);
//TODO
}
จะเห็นว่าการใช้ await
และ async
นั้นทำให้เราใช้งาน Future ได้ง่ายขึ้นมากๆ แต่ๆๆๆ มีเรื่องสำคัญที่ต้องจำไว้เวลาใช้ คือ...
การใช้ await ไม่ใช่การแปลงให้โค้ดที่เคยทำงานแบบ Asynchronous กลับมาทำงานแบบ Synchronous นะ!! มันยังทำงานแบบ Asynchronous เหมือนเดิมนั่นแหละ
แค่รูปแบบการเขียนมันถูกแปลงให้มาอยู่ในรูป sync เท่านั้นเอง เบื้องหลังเวลารัน มันทำงานเหมือนตอนเขียนด้วยthen()
ทั้งหมดนะ คอนเซ็ป Isolates และ Event Loop ก็ยังอยู่ทั้งหมด เวลาใช้งาน ต้องจำไว้ดีๆ ล่ะ
Error Handlering
นอกจากการตอบค่าคืนแบบปกติ เราสามารถสร้าง Error กลับมาได้ด้วย เช่น
var f = Future((){
if(...){
throw Exception('อุ๊ปส์ มีข้อผิดพลาดเกิดขึ้น');
}
return 100;
});
ทีนี้ เมื่อเราสร้าง Error ได้ เราก็ต้องมีการดัก/รับมือ Error ที่เกิดขึ้น แบบนี้
- ถ้าใช้
then
เราสามารถกำหนดcatchError
ต่อได้ - ถ้าใช้
await
ไม่มีคำสั่งแบบcatchError
แต่เราใช้ try-catch แบบธรรมดาคลุมโค้ดตรงนั้นได้เลย
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
ต่อกันได้หลายๆ ชั้น แบบนี้
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 ตัวเดิมก็จะได้ค่าตัวนั้นนะ
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)
แล้วส่วนใหญ่ที่ IDE มันแนะนำให้แก้ก็คือการเติม await
ลงไปข้างหน้าแบบนี้
await future().then(...);
ซึ่งพอเติมลงไป Warning ก็จะหายไป ว้าว! งั้นวิธีแก้แบบนี้ก็ถูกแล้วสินะ
ถูกอะไรล่ะ! มันไม่ Warning แล้วก็จริง แต่การทำงานของโค้ดเปลี่ยนไปโดยสิ้นเชิงเลยนะ (แบบที่บอกไปแล้วไงว่า await คือคำสั่งที่ทำให้โค้ด async กลายเป็น sync)
เหตุผลที่ IDE มันแจ้ง Warning ก็เพราะจริงๆ แล้ว then
รีเทิร์นค่า Future ตัวใหม่กลับมาเสมอ เมื่อมีค่ารีเทิร์นกลับมาแต่ไม่ใช้งาน มันเลยแจ้ง Warning ไงล่ะ
วิธีแก้คือการใช้ฟังก์ชัน unawaited
มาครอบเอาไว้ แล้ว Warning ก็จะหายไป
import 'package:pedantic/pedantic.dart';
void main() {
unawaited(
future().then(...)
);
}
แล้วทำไมการเติม await
ลงไปถึงทำให้มันทำงานเปลี่ยนไป (ใครลองเราไปเขียนโค้ดเทส อาจจะพบว่าจะเติม await หรือไม่เติมก็ได้ผลเหมือนเดิมไม่ใช่เหรอไง)
ลองดูโค้ดนี้
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 แต่ละตัวต้องหยุดรอกันจนกว่าตัวเดิมจะรันเสร็จ แบบนี้
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
Posted on May 1, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
June 9, 2020
April 25, 2020