iT邦幫忙

2023 iThome 鐵人賽

DAY 5
1

終於來到介紹 Dart 基礎語法的最後一個篇章,今天我們要來介紹 Dart 的同步與非同步。讓我們馬上開始。

同步/非同步是什麼?

Dart 是一個單執行緒語言 (single thread ),意思就是程式碼同時只能做一件事情,並且程式執行的順序與我們程式碼的編排相同,由上而下的執行,這就是我們所說的「同步」。
在 Dart 中多數的行為都是屬於同步完成的,舉例來說:

// 這三行會由上而下的照順序執行,所以就是依序印出三行
void main() {
  print('你好');
  print('我是 Micro Jordan');
  print('希望我鐵人賽可以順利完賽');
}

但是當所有的行為都需要互相等待彼此才能繼續進行的話,當行為一多就會造成塞車。就好比高速公路只有一個車道開放行駛,應該會從台北一路塞到高雄吧XD
https://ithelp.ithome.com.tw/upload/images/20230920/20135082JTZFCyq4vv.jpg
因此這時候就需要非同步的力量了。非同步的概念與同步相反,意思是同時可以做很多事情,各司其職,不必等前面的事情完成就能繼續做下件事。同樣我們也用例子來說明:

import 'dart:async';
// Future 是一個特殊的型態,我們下面會詳細介紹。
// 這裡只要知道此函式會等待 2 秒鐘才執行 print 的動作
Future<void> delayGreet() async {
  Future.delayed(Duration(seconds: 2), () {
    print('你好');
  });
}

// 我們按照順序寫,會預期應該要由上而下的執行。然而卻是跳過 delayGreet(),先印出下方的訊息
void main() {
  delayGreet();
  print('我是 Micro Jordan');
  print('希望我鐵人賽可以順利完賽');
}

當我們要取得網路上的資訊、寫資料到資料庫或是讀寫檔案時,幾乎都是透過非同步來完成的,因此讓我們來好好會會 Dart 的非同步吧!

Future

非同步既然不是呼叫的當下即完成,也就代表結果沒辦法即時的呈現。因此在 Dart 中定義了一種型態 Future 用於標示該區塊的程式碼當下雖然沒有及時完成(uncompleted ),但在未來一定會完成(completed )。
Future<T> 是 Dart 所定義的 abstract class,既然使用了泛型也就意味著 Future 中可以是任意型態,若非同步的回傳值為整數則型態為 Future<int> ;若為字串則是Future<String> ;若都不必回傳時,則定義為 Future<void>

Async 與 Await

asyncawait 是在寫非同步程式時常用到的兩個關鍵字,先從前者開始講起。
async 是用於標示某個函式為非同步函式時所需加上的關鍵字,假設我有一個函式要從網路上獲取資料:

// fetchData 這個函式裡面實作的是一個非同步行為,所以該函式為非同步函式所以使用 async 進行標示
Future<void> fetchData async {
  // 省略
}

void main() {
  // 定義一個變數 isReady 一開始為 false,直到獲取資料後才轉為 true
  bool isReady = false;
  fetchData();
  isReady = true;
}

然而上述的程式碼有個問題。一開始我們將 isReady 設為 false ,用意是希望當成功獲取資料後再將其改為 true,用以記錄是否拿到資料了。但上述程式碼會因為 fetchData 非同步不會立即完成而立馬跳到下行將 isReady 轉為 true ,而當 fetchData 真的完成時,早就已經不知道多久以後了...
顯然這並不是我們要的,我們希望 isReadyfetchData 的時候等待一下,直到完成才繼續往下走。因此這時候就是 await 的出場時機拉!
await 的意思就是用於在非同步函式中,等待一個非同步的動作完成後再進行接續的動作。
因此我們再重新檢視一次整個程式碼。main 函式中有非同步的行為,是一個非同步的函式,因此 main 函式需要用 async 的關鍵字進行標示。
簡單來說,一句話粗暴的說就是你要用 await 前把你的函式標成 async 就對了XD

void main() async {
  bool isReady = false;
  // 因為 fetchData 是執行一個非同步行為,因此整個 main 函式為一個非同步的函式
  await fetchData();
  isReady = true;
}

Stream

Dart 其實還定義了 Stream 來應對一連串需要持續接受、發送的非同步迭代事件。下表用於呈現 FutureStream 間的關係。

單物件 迭代物件
同步 int Iterator<int>
非同步 Future<int> Stream<int>

舉個常見的例子,就如同實作聊天室一樣,一旦兩個人都在該聊天室當中,對話的過程其實就是一連串的持續接受訊息、發送訊息的動作,因此很適合使用 Stream 來進行實作。
Stream 其實還可細分成兩種:
1. Single subscription streams (單一訂閱串流)-
最常見的串流類型,用於只發生一次的事件。並且事件本身需要按照順序傳遞,不能漏掉。就好比從網路上下載一個文字檔案時執行單一訂閱串流,檔案中的文字內容一定要按照順序,也不能有所遺漏,否則該檔案就不是完整的檔案了。必須重新再下載一次執行另一個單一串流。

// 串建一個 stream 資料流,stream 預設為單一訂閱串流
final stream = ChatRoom().stream;

// 監聽該 stream 當中的訊息 
final room1 = stream.listen((message) => print(message));

// 單一訂閱串流並無法同時被兩個物件所監聽
// final room2 = stream.listen((message) => print(message)); 

// stream 提供多種操作串流的方法
room1.pause(); // 暫停
room1.resume(); // 恢復
room1.cancel(); // 取消

2. Broadcast streams (廣播串流) -
跟電台廣播一樣,可以在任意時刻收聽來觸發事件,也可以多個人同時進行收聽。

// 將 stream 改為廣播串流
final stream = ChatRoom().stream.asBroadcaseStream;

// 監聽該 stream 當中的訊息 
final room1 = stream.listen((message) => print(message));

// 因為為廣播串流,所以可以同時有多組監聽者
final room2 = stream.listen((message) => print(message)); // 不能這樣

在我們了解了 stream 之後,我們來試著自己實作看看一個 stream。在 Dart 中提供我們數種建立 stream 的方式:

  1. 使用非同步產生器函式 (async* )。一旦該函式被呼叫時,stream 就會被建立;一旦函式回傳後,該 stream 就關閉。在函式執行的過程當中,可使用 yield 來觸發事件。
// 根據定義的 interval 週期性的觸發事件
Stream<int> timedCounter(Duration interval, [int? maxCount]) async* {
  int counter = 0;
  while(true) {
    await Future.delayed(interval);
    // 每隔一個 interval 則觸發將 i 傳出去,再將 i 的值 +1
    yield counter++;
    if (counter == maxCount) break;
  }
} // 函式結束,stream 也隨之結束

void main() {
  // 設定 interval 為 1,最多執行 30 個 interval 的 stream
  var stream = timedCounter(const Duration(seconds: 1), 30);
  // 監聽 stream 事件
  stream.listen((val) => print('收到 $val'));

  /* 也可以透過這樣來監聽事件,不過 single subsription stream 一次只能有一個 listener
  因此想使用這行記得要把上面的 listen 註解掉
  await for (final val in stream) {
    print('收到 $val');
  }
  */
}
  1. 若 stream 的事件可能會從程式碼的各個地方產生,那可以使用 StreamController 來處理這些比較複雜的情況。
import 'dart:async';

Stream<int> timedCounter(Duration interval, [int? maxCount]) {
  late StreamController<int> controller;
  Timer? timer;
  int counter = 0;

  void startTimer() {
    timer = Timer.periodic(interval, (_) {
      // StreamController 使用 add 來觸發事件
      controller.add(counter++);
      if (counter == maxCount) {
        // 取消 timer
        timer?.cancel();
        // 關閉串流
        controller.close();
      }
    });
  }

  void stopTimer() {
    timer?.cancel();
    timer = null;
  }
    // StreamController 的四個參數,onListener 表示開始時執行 startTimer,其他依此類推
  controller = StreamController<int>(
    onListen: startTimer,
    onPause: stopTimer,
    onResume: startTimer,
    onCancel: stopTimer
  );
  return controller.stream;
}

void main() {
  var stream = timedCounter(const Duration(seconds: 1), 30);
  stream.listen(
    (val) => print('收到:$val'),
    onDone: () => print('完成')
  );
}

今日總結

我們簡單的回顧今天的內容:

  • Dart 是一個單執行緒語言,通常情況下程式碼按照上到下的順序執行,這種執行方式稱為同步。
  • 非同步是指在程式碼執行時不必等待某個操作完成,而可以繼續執行其他操作的能力。Dart 支援非同步操作。
  • Future 是 Dart 中用於處理非同步操作的一種方式,它表示一個未來可能完成的操作,可以使用 asyncawait 來處理非同步操作。
  • Stream 是 Dart 中用於處理一連串非同步事件的方式,它可以表示一系列事件的流,可以用於處理需要持續接收和發送事件的情況。
  • Dart 中有兩種 Stream 類型:單一訂閱串流(Single subscription streams)和廣播串流(Broadcast streams)。單一訂閱串流只能被一個訂閱者監聽,而廣播串流可以被多個訂閱者同時監聽。
  • Dart 中的 StreamController 用於創建和管理 Stream,它可以用於定義自己的事件流並控制事件的發送。
  • 您可以使用 async*StreamController 來創建自己的 Stream,並使用 yieldadd 來觸發事件。

今天的內容比較複雜一點點,如果你沒有碰過寫過非同步程式的話很難一開始就搞懂,很容易不小心就寫成同步程式,然後又找不到 Bug 在哪QQ 多練練手其實非常的重要,也有助於你快速的掌握箇中的奧妙!

我們花了 4 天的時間來介紹 Dart 的基礎語法,希望有幫助到大家認識這個語言。接下來的篇章我們就真的要來開始把我們的所學運用在開發應用程式上拉!!我們明天見囉~


上一篇
[Day 04] Dart 基礎語法 Part 3
下一篇
[Day 06] 安裝 Flutter 環境
系列文
Flutter 從零到實戰 - 30 天の學習筆記30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言