Async in Dart (2) เดี๋ยวจะตอบให้ในอนาคต กับการใช้ Future

nartra

Ta

Posted on May 1, 2020

Async in Dart (2) เดี๋ยวจะตอบให้ในอนาคต กับการใช้ Future

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

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

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

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

ถ้านึกไม่ออกให้นึกถึง List (หรือจะเป็น Map ก็ยังได้นะ แนวคิดเดียวกัน)

var wrapper = [123];
Enter fullscreen mode Exit fullscreen mode

นั่นคือค่า 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>(...)
Enter fullscreen mode Exit fullscreen mode

โดยอ็อบเจค Future นั้นต้องการฟังก์ชันสำหรับสร้าง value กลับมา แบบนี้

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

//หรือละ type ก็ได้
var f = Future((){
    return 100;
})
Enter fullscreen mode Exit fullscreen mode

value

นอกจากการสร้างอ็อบเจคแล้ว เราสามารถสร้าง Future ด้วยกาใช้ factory value

var f = Future.value(100);
Enter fullscreen mode Exit fullscreen mode

delayed

ถ้าไม่อยากตอบค่ากลับในทันที ก็สามารถหน่วงเวลาตอบกลับด้วย delay ซึ่งวิธีการสร้างก็คล้ายๆ กับการสร้างอ็อบเจค แต่ต้องกำหนดพารามิเตอร์เพื่อบอกระยะเวลาที่จะให้ดีเลย์ด้วย Duration

var f = Future.delayed(Duration(minutes: 1), (){
    return 100;
});
Enter fullscreen mode Exit fullscreen mode

error

ในบางครั้ง การตอบค่ากลับก็มีข้อผิดพลาดขึ้นได้ (ถ้าเขียนแบบปกติก็คือการ throw Exception() นั่นแหละ)

var f = Future.error(Exception('เกิดข้อผิดพลาด'));
Enter fullscreen mode Exit fullscreen mode

วิธีการดึงค่าออกมาจาก Future

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

ดึงด้วยเมธอด then()

ตามปกติการใช้ Future จะใช้เมื่อเราไม่รู้ว่าค่าจะถูกส่งกลับมาเมื่อไหร่ ดั้งนั้นเราจะต้องสร้างฟังก์ชันที่เรียกว่า "Callback" ซึ่งจะถูกรันเมื่อค่าถูกส่งกลับมา

var f = Future.value(100);
f.then((value){
    print(value);
});
Enter fullscreen mode Exit fullscreen mode

โค้ดข้างบนเวลารันแล้วก็จะได้ค่า 100 ออกมา ซึ่งดูเผินๆ แล้วก็เหมือนกับการเขียนโค้ดแบบปกติ

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

print('start');

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

print('end');
Enter fullscreen mode Exit fullscreen mode

คำถามคือเมื่อเราเอาไปรัน แล้วจะได้ผลอะไรออกมา?

ตามปกติเราก็มักจะคิดว่ามันจะรันตามบรรทัด แล้วรันออกมาได้แบบนี้

start
100
end
Enter fullscreen mode Exit fullscreen mode

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

ดังนั้น เวลาเอาไปรันแล้วจะได้ผลแบบนี้

start ─┬─ run in main thread
end   ─┘
100   ─── run in second thread
Enter fullscreen mode Exit fullscreen mode

แล้วถ้าเราใช้ delayed ก็จะได้ผลเหมือนกัน แต่ถูกหน่วงเวลาเอาไว้

print('start');

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

print('end');
Enter fullscreen mode Exit fullscreen mode

ก็จะได้ผลแบบนี้

start ─┬─ รันในทันที
end   ─┘
100   ─── รันเมื่อผ่านไป 1 นาที
Enter fullscreen mode Exit fullscreen mode

ลดรูป Future ด้วย await

ในบางกรณีเรามีการโหลดข้อมูลจาก API หลายๆ ต่อ เช่น

Future<Data1> loadData1(){
    ...
}

Future<Data2> loadData2(int id){
    ...
}
Enter fullscreen mode Exit fullscreen mode

แต่มีปัญหาคือ การจะโหลด Data2 จะต้องใช้ค่าจาก Data1 ซะก่อน การโหลดข้อมูลเลยต้องเขียนแบบนี้

// โหลด data1 ให้เสร็จก่อน
loadData1().then((data1){
    var id = data1.id;
    // จากนั้นดึง id ออกมาแล้วเอาไปใช้โหลด data2 ต่อ
    loadData2().then((data2){
        //TODO
    })
})
Enter fullscreen mode Exit fullscreen mode

แน่นอนว่าการเขียนอะไรแบบนี้ทำให้อ่านยากมาก ยิ่งถ้าต้องมีการโหลด 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
}
Enter fullscreen mode Exit fullscreen mode

จะเห็นว่าการใช้ await และ async นั้นทำให้เราใช้งาน Future ได้ง่ายขึ้นมากๆ แต่ๆๆๆ มีเรื่องสำคัญที่ต้องจำไว้เวลาใช้ คือ...

การใช้ await ไม่ใช่การแปลงให้โค้ดที่เคยทำงานแบบ Asynchronous กลับมาทำงานแบบ Synchronous นะ!! มันยังทำงานแบบ Asynchronous เหมือนเดิมนั่นแหละ

แค่รูปแบบการเขียนมันถูกแปลงให้มาอยู่ในรูป sync เท่านั้นเอง เบื้องหลังเวลารัน มันทำงานเหมือนตอนเขียนด้วย then() ทั้งหมดนะ คอนเซ็ป Isolates และ Event Loop ก็ยังอยู่ทั้งหมด เวลาใช้งาน ต้องจำไว้ดีๆ ล่ะ

Error Handlering

นอกจากการตอบค่าคืนแบบปกติ เราสามารถสร้าง Error กลับมาได้ด้วย เช่น

var f = Future((){
    if(...){
        throw Exception('อุ๊ปส์ มีข้อผิดพลาดเกิดขึ้น');
    }
    return 100;
});
Enter fullscreen mode Exit fullscreen mode

ทีนี้ เมื่อเราสร้าง 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');
}
Enter fullscreen mode Exit fullscreen mode

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);
})
Enter fullscreen mode Exit fullscreen mode

output

160
Enter fullscreen mode Exit fullscreen mode

ข้อควรระวังคือเวลาเขียนต้อง 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);
});
Enter fullscreen mode Exit fullscreen mode

output

1
2
100
200
Enter fullscreen mode Exit fullscreen mode

unawaited

ถ้าเราเขียนโค้ดใน IDE บางตัวที่มีการเช็กโค้ดให้เราดีๆ การใช้คำสั่ง then อาจจะทำให้เราเจอ Warning แบบนี้ (Warning นะ ไม่ใช่ Error)

แล้วส่วนใหญ่ที่ IDE มันแนะนำให้แก้ก็คือการเติม await ลงไปข้างหน้าแบบนี้

await future().then(...);
Enter fullscreen mode Exit fullscreen mode

ซึ่งพอเติมลงไป Warning ก็จะหายไป ว้าว! งั้นวิธีแก้แบบนี้ก็ถูกแล้วสินะ

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

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

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

import 'package:pedantic/pedantic.dart';

void main() {
    unawaited(
        future().then(...)
    );
}
Enter fullscreen mode Exit fullscreen mode

แล้วทำไมการเติม 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);
Enter fullscreen mode Exit fullscreen mode

output (ตัวเลขข้างหน้าเป็นหลักเวลา)

0|    1sec.  │    │
1| C  ─┘    2sec. │
2| A  ───────┘    │
3|               4sec.
4| B  ────────────┘
5|
Enter fullscreen mode Exit fullscreen mode

สังเกตว่าจุดเริ่มต้นของ 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);
Enter fullscreen mode Exit fullscreen mode

output (ตัวเลขข้างหน้าเป็นหลักเวลา)

0|        │
1|        1sec.
2| A  ────┤
3|        │
4|        4sec.
5|        │
6| B  ────┤
7| C  ────1sec.  
8|
Enter fullscreen mode Exit fullscreen mode

Next - ในตอนต่อไป

ในบทนี้เราสอนวิธีการสร้าง Future ไป ก็เหมือนกับการใช้งานตัวแปรแบบธรรมดาแต่ตอบค่าในอนาคต

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

💖 💪 🙅 🚩
nartra
Ta

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