Flutter非同步程式設計-sync*和async*生成器函式

熊喵先生發表於2021-03-28

生成器函式可能比較陌生,在平時開發中使用的不是特別頻繁,但是因為它也是Dart非同步程式設計的不可缺少的一部分,所以這裡還是展開講解分析。生成器函式是一種用於延遲生成值序列的函式,並且在Dart中生成器函式主要分為兩種: 同步生成器函式和非同步生成器函式。我們知道比如 int 型別變數(同步)或 Future<int> (非同步)型別變數都只能產生單一的值,而 Iterable<int> 型別變數(同步)或 Stream<int> 型別變數(非同步)具有產生多個值的能力。其中同步生成器函式是立即按需生成值,並不會像Future,Stream那樣等待,而非同步生成器函式則是非同步生成值,也就是它有足夠時間去生成值

Single value(單一的值)Zero or more value(零個值或者更多的值)
Sync(同步)intIterable
Async(非同步)FutureStream

1. 為什麼需要生成器函式

其實在平時Flutter或者Dart開發中生成器函式使用並不多,但是遇到使用它的場景,有了生成器函式就會非常簡單,因為手動去實現值的生成函式還是比較繁雜的。比如說要實現一個同步生成器,需要自定義一個可迭代的類去繼承 IterableBase 並且需要重寫 iterator 方法用於返回一個新的Iterator物件。為此還得需要宣告自己的迭代器類。此外還得實現成員方法 moveNext 和成員屬性 current 以此來判斷是否迭代到末尾。這還是寫一個同步生成器步驟,整個過程還是比較繁雜的。所以Dart給你提供一個方法可以直接生成一個同步生成器函式,簡化整個實現的步驟。但是如果要去手動實現一個非同步生成器遠比同步生成器更復雜,所以直接提供一個簡單非同步生成器函式會使得整個開發更加高效,可以把精力更加專注於業務。這裡就以同步生成器函式舉例:

說到同步生成器函式先來回顧下 Iterable 和 Iterator 

//Iterator可以將一系列的值依次迭代
abstract class Iterator<E> {
  bool moveNext();//表示是否迭代器有下一個值,迭代器把下一個值載入為當前值,直到下一個值為空返回false

  E get current; //返回當前迭代的值,也就是最近迭代一次的值
}
複製程式碼

按照上面介紹步驟一起來手動實現一個 Iterable ,實際上也很簡單只是簡單地包了個殼

class StringIterable extends Iterable<String> {
  final List<String> stringList; //實際上List就是一個Iterable

  StringIterable(this.stringList);

  @override
  Iterator<String> get iterator =>
      stringList.iterator; //通過將List的iterator,賦值給iterator

}

//這樣StringIterable就是一個特定型別String的迭代器,我們就可以使用for-in迴圈進行迭代
void main() {
  var stringIterable = StringIterable([
    "Dart",
    "Java",
    "Kotlin",
    "Swift"
  ]);
  for (var value in stringIterable) {
    print('$value');
  }
}

//甚至你還可以將StringIterable結合map、where、reduce之類操作符函式之類對迭代器值進行變換
void main() {
  var stringIterable = StringIterable([
    "Dart",
    "Java",
    "Kotlin",
    "Swift"
  ]);
  stringIterable
      .map((language) => language.length)//可以結合map一起使用,實際上本質就是Iterable物件轉換,將StringIterable轉換成MappedIterable
      .forEach(print);
}
複製程式碼

輸出結果: image.png image.png 可以看到上面其實還不是一個真正嚴格意義手動實現一個 Iterable , 那麼問題來了如何手動實現一個同步生成器函式,注意:同步生成器函式必須返回一個 Iterable 型別,然後需要使用 sync* 修飾該函式,以此來標記該函式是一個同步生成器函式。

void main() {
  final numbers = getRange(1, 10);
  for (var value in numbers) {
    print('$value');
  }
}

//用於生成一個同步序列
Iterable<int> getRange(int start, int end) sync* { //sync*告訴Dart這個函式是一個按需生產值的同步生成器函式
  for (int i = start; i <= end; i++) {
    yield i;//yield關鍵字有點像return,但是它是單次返回值,並不會像return直接結束整個函式
  }
}
複製程式碼

輸出結果: image.png 通過對比發現了生成器函式是真的簡單方便,只需要通過 sync* 和 yield 關鍵字就能實現一個任意型別迭代器,比手動實現一個同步生成器函式更加簡單,所以應該知道為什麼需要生成器函式。其實非同步生成器函式也是類似。

2. 什麼是生成器(Generator)

生成器函式是一種可以很方便延遲生成值的序列的函式,生成器主要分為兩種:**同步生成器函式和非同步生成器函式。其中同步生成函式需要使用 sync* 關鍵字修飾,返回一個 Iterable 物件(表示可以順序訪問值的集合);非同步生成器函式需要使用 async* 關鍵字修飾,返回的是一個 Stream 物件(表示非同步資料事件)。**此外同步生成器函式是立即按需生成值,並不會像Future,Stream那樣等待,而非同步生成器函式則是非同步生成值,也就是它有足夠時間去生成值。

2.1 同步生成器(Synchronous Generator)

image.png 同步生成器函式需要配合 sync* 關鍵字和 yield 關鍵字,最終返回的是一個 Iterable 物件,其中yield 關鍵字用於每次返回單次的值,並且需要注意它的返回並不是結束整個函式。

import 'dart:io';

void main() {
  final numbers = countValue(10);
  for (var value in numbers) {
    print('$value');
  }
}

Iterable<int> countValue(int max) sync* {//sync*告訴Dart這個函式是一個按需生產值的同步生成器函式
  for (int i = 0; i < max; i++) {
    yield i; //yield關鍵字每次返回單次的值
    sleep(Duration(seconds: 1));
  }
}

複製程式碼

輸出結果: image.png

2.2 非同步生成器(Asynchronous Generator)

image.png 非同步生成器函式需要配合 async* 關鍵字和 yield 關鍵字,最終返回的是一個 Stream 物件,需要注意的是生成Stream也是一個單一訂閱模型的Stream, 也就是說不能同時存在多個訂閱者監聽否則會出現異常,如果要實現支援多個監聽者通過 asBroadcastStream 轉換成一個廣播訂閱模型的Stream。

import 'dart:io';

void main() {
  Stream<int> stream = countStream(10);
  stream.listen((event) {
    print('event value: $event');
  }).onDone(() {
    print('is done');
  });
}

Stream<int> countStream(int max) async* {
  //async*告訴Dart這個函式是生成非同步事件流的非同步生成器函式
  for (int i = 0; i < max; i++) {
    yield i;
    sleep(Duration(seconds: 1));
  }
}
複製程式碼

輸出結果: image.png

2.3 yield關鍵字

yield關鍵字在生成器函式中是用於依序生成每一個值,它有點類似return語句,但是和return語句不一樣的是執行一次yield並不會結束整個函式。相反它每次只提供單個值,並掛起(注意不是阻塞)等待呼叫者請求下一個值,然後它就會再執行一遍,比如上述例子for迴圈中,每一次迴圈執行都會觸發yield執行一次返回每次的單值並且進入下一次迴圈的等待

Iterable<int> countValue(int max) sync* {//sync*告訴Dart這個函式是一個按需生產值的同步生成器函式
  for (int i = 0; i < max; i++) {
    yield i; //每執行一次迴圈就會觸發當前次yield生成值,然後進入下一次的等待
    sleep(Duration(seconds: 1));
  }
}
複製程式碼

2.4 yield* 關鍵字

yield關鍵字是用於迴圈中生產值後跟著一個具體物件或者值,但是yield後面則是跟著一個函式,一般會跟著遞迴函式,通過它就能實現類似二次遞迴函式功能。雖然yield關鍵字也能實現一個二次遞迴函式但是比較複雜,但是如果使用yield關鍵字就會更加簡單。

//這是使用yield實現二次遞迴函式
Iterable naturals(n) sync* { 
  if (n > 0) { 
     yield n; 
     for (int i in naturals(n-1)) { 
       yield i; 
     } 
  }
} 

//這是使用yield*實現的二次遞迴函式
Iterable naturals(n) sync* { 
  if ( n > 0) { 
    yield n; 
    yield* naturals(n-1); 
 } 
} 
複製程式碼

2.5 await for

關於await for在Stream那一篇文章中已經有說到,對於一個同步的迭代器Iterable而言我們可以使用for-in迴圈來遍歷每個元素,而對於一個非同步的Stream可以很好地使用await for來遍歷每個事件元素。

void main() async {
  Stream<int> stream = Stream<int>.periodic(Duration(seconds: 1), (int value) {
    return value + 1;
  });
  await stream.forEach((element) => print('stream value is: $element'));
}
複製程式碼

3. 如何使用生成器函式(Generator)

3.1 sync* + yield實現同步生成器

void main() {
  final Iterable<int> sequence = countDownBySync(10);
  print('start');
  sequence.forEach((element) => print(element));
  print('end');
}

Iterable<int> countDownBySync(int num) sync* {//sync*
  while (num > 0) {
    yield num--;//yield返回值
  }
}
複製程式碼

輸出結果: image.png

3.2  async* + yield實現非同步生成器

void main() {
  final Stream<int> sequence = countDownByAsync(10);
  print('start');
  sequence.listen((event) => print(event)).onDone(() => print('is done'));
  print('end');
}

Stream<int> countDownByAsync(int num) async* {//async*表示非同步返回一個Stream
  while (num > 0) {
    yield num--;
  }
}
複製程式碼

輸出結果: image.png

3.3 sync* + yield*實現遞迴同步生成器

void main() {
  final Iterable<int> sequence = countDownBySyncRecursive(10);
  print('start');
  sequence.forEach((element) => print(element));
  print('end');
}

Iterable<int> countDownBySyncRecursive(int num) sync* {
  if (num > 0) {
    yield num;
    yield* countDownBySyncRecursive(num - 1);//yield*後跟一個遞迴函式
  }
}
複製程式碼

輸出結果: image.png

3.4 async* + yield*實現遞迴非同步生成器

void main() {
  final Stream<int> sequence = countDownByAsync(10);
  print('start');
  sequence.listen((event) => print(event)).onDone(() => print('is done'));
  print('end');
}

Stream<int> countDownByAsyncRecursive(int num) async* {
  if (num > 0) {
    yield num;
    yield* countDownByAsyncRecursive(num - 1);//yield*後跟一個遞迴函式
  }
}
複製程式碼

輸出結果: image.png

4. 生成器函式(Generator)使用場景

關於生成器函式使用在開發過程其實並不多,但是也不是說關於生成器函式使用場景就沒有了,否則這篇文章就沒啥意義了。實際上如果有小夥伴接觸Flutter開發中其中有一個Bloc的狀態管理框架,可以發現它裡面大量地使用了非同步生成器函式,一起來看下:

class CountBloc extends Bloc<CounterEvent, int> {
  @override
  int get initialState => 0;

  @override
  Stream<int> mapEventToState(CounterEvent event) async* {
    switch (event) {
      case CounterEvent.decrement:
        yield state - 1;
        break;
      case CounterEvent.increment:
        yield state + 1;
        break;
    }
  }
}
複製程式碼

其實除了上面所說Bloc狀態管理框架中使用到的場景,可能還有一種場景那就是非同步二次函式遞迴場景,比如實現某個動畫軌跡計算,實際上都是通過一些二次函式計算模擬出來,所以這時候生成器遞迴函式是不是就可以派上用場了。雖然生成器函式使用場景不是很頻繁,但是需要做到某個特定場景第一時間想到用它可以更加簡單的實現就可以了。

5. 總結

到這裡有關Flutter非同步程式設計系列就結束了,下一個系列將進入Dart註解+APT生成程式碼技術專題。儘管生成器函式使用在開發過程其實並不多,但是它也作為Flutter非同步中一部分,有一些特定場景如果能想到用它來解決,一定會事半功倍的。

感謝關注,熊喵先生願和你在技術路上一起成長!

相關文章