(二)Flutter學習之Dart展開操作符 和 Control Flow Collections

Chiclaim發表於2019-07-23

展開操作符(spread operators)

展開操作符 ... 能夠把 list、set、map 字面量裡的元素插入到一個集合中。一個物件是否可用於展開操作符取決於是否繼承了Iterable,Map集合例外,對 map 進行展開操作 實際上是 呼叫了 Mapentries.iterator()

在實際開發中,我們可能需要建立新的集合,集合的元素通常依賴另一個已經存在集合,然後再次基礎上再新增寫新的元素,如:

var args = testArgs.toList()
  ..add('--packages=${PackageMap.globalPackagesPath}')
  ..add('-rexpanded')
  ..addAll(filePaths);
複製程式碼

上面的程式碼案例,來自官網。其中 toList() 函式會建立一個新的集合,集合裡包含了 testArgs 所有元素 然後通過 級聯操作符 實現鏈式呼叫來簡化程式碼,最後通過 addAll 新增另一個集合的所有元素

但是這種寫法依然非常笨重,下面來看下如何通過 展開操作符簡化程式碼:

var args = [
  ...testArgs, // 通過展開操作符將testArgs集合解包然後將裡面的元素逐個放進args裡
  '--packages=${PackageMap.globalPackagesPath}',
  '-rexpanded',
  ...filePaths // 通過展開操作符將filePaths集合解包然後將裡面的元素逐個放進args裡
];
複製程式碼

Flutter 中是使用 宣告式UI(declarative style), Android/iOS 是使用 命令式UI(imperative style)

什麼是 宣告式UI? 在 Flutter 中,如果 Widget 發生變化通過 setState 函式觸發,然後 Flutter 會構建新的 Widget 例項和 Widget 子樹 而 命令式UI 是拿到原來的 Widget 例項,然後呼叫該例項的方法來觸發變化,而不是建立新的 Widget

他們之間的程式碼風格區別如下:

// 宣告式UI程式碼風格
return ViewB(
  color: red,
  child: ViewC(...),
)

// 命令式UI程式碼風格
b.setColor(red)
b.clearChildren()
ViewC c3 = new ViewC(...)
b.add(c3)
複製程式碼

展開操作符 在 Flutter 這種宣告式 UI 中應用非常廣泛,例如我們構建一個 ListView:

Widget build(BuildContext context) {
  return CupertinoPageScaffold(
    child: ListView(
      children: [ //children 就是一個List
        Tab2Header(),
        // buildTab2Conversation()返回值也是一個List
        ...buildTab2Conversation(), 
      ],
    ),
  );
}
複製程式碼

上面的例子是在 List 中和使用展開操作符,在 Map 集合中也是一樣的,例如將 queryParams 和 formParams 合併產生一個新的 Map 集合

var map = {
    ...options.queryParameters,
    ...options.data
};
複製程式碼

在 Set 中的使用展開操作符也一樣的,如:

var items = [2, 3, 4];
var set = { 1, 2, ...items };
複製程式碼

可空的展開操作符(Null-aware spread)

上面的我們講到的展開操作符,如果被展開的物件是 null,那麼會丟擲執行時異常

var oops = null;
//NoSuchMethodError: The getter 'iterator' was called on null
var list = [...oops];
複製程式碼

如果被展開的物件可能為 null,需要在展開操作符後面加上 ? 號 (...?):

var oops = null;
var list = [...?oops];
複製程式碼

展開操作符語義分析

List 中使用展開操作符分析

var list  = [elem_1 ... elem_n]
1. 首先會建立新的集合,用於儲存集合字面量裡的元素
2. 遍歷字面量裡的所有元素
    1)把元素表示式(展開操作符)賦值給value遍歷
    2)如果遍歷的元素是展開操作符
        a)如果使用的是可空的展開操作符,並且 value 是 null,直接 continue
        b)將 value.iterator 賦值給 iterator 變數
        c)然後遍歷 iterator,將裡面的元素追加到上面新建的集合裡
    3)如果遍歷的元素不是展開操作符,直接將元素新增到集合中
3. 就這樣,集合字面量的元素全部在上面新建的集合裡了
複製程式碼

像其他的集合如 Set、Map 也是類似的機制

控制流集合(Control Flow Collections)

控制流集合 就是在構建集合字面量的時候可以使用 if、for 控制流語句,簡稱 collection-if/for

上面我們提到 Flutter 是宣告式 UI,例如:

Widget build(BuildContext context) {
  return Row(
    children: [ // 集合字面量
      IconButton(icon: Icon(Icons.menu)),
      Expanded(child: title),
      IconButton(icon: Icon(Icons.search)),
    ],
  );
}
複製程式碼

如果我們想在上面的程式碼的基礎上加一些判斷,例如只在 Android 平臺展示搜尋按鈕(Icons.search),我們可能會這麼寫:

Widget build(BuildContext context) {
  // 構建button集合
  var buttons = <Widget>[
      IconButton(icon: Icon(Icons.menu)),
      Expanded(child: title),
  ];
  // 如果是 android 平臺,新增搜尋按鈕
  if (isAndroid) {
    buttons.add(IconButton(icon: Icon(Icons.search)));
  }

  return Row(
    children: buttons,
  );
}
複製程式碼

有了 Control Flow Collection,我們可以這樣類簡化程式碼:

Widget build(BuildContext context) {
  return Row(
    children: [
      IconButton(icon: Icon(Icons.menu)),
      Expanded(child: title),
      // 如果是 android 平臺,新增搜尋按鈕
      if (isAndroid) IconButton(icon: Icon(Icons.search)),
    ]
  );
}
複製程式碼

除了使用 if,還可以使用 else,例如在 Android 平臺展示搜尋按鈕, 其他平臺展示關於按鈕 :

Widget build(BuildContext context) {
  return Row(
    children: [
      IconButton(icon: Icon(Icons.menu)),
      Expanded(child: title),
      if (isAndroid)
        IconButton(icon: Icon(Icons.search))
      else
        IconButton(icon: Icon(Icons.about)),
    ]
  );
}
複製程式碼

再比如我們需要根據一個集合然後生成另一個集合,可能會這樣寫:

var command = [
  engineDartPath,
  frontendServer,
];
for (var root in fileSystemRoots) {
  command.add('--filesystem-root=$root');
}
for (var entryPointsJson in entryPointsJsonFiles) {
  if (fileExists("$entryPointsJson.json")) {
    command.add(entryPointsJson);
  }
}
command.add(mainPath);
複製程式碼

有了 Control Flow Collection,我們可以這樣寫,可讀性更強:

var command = [
  engineDartPath,
  frontendServer,
  for (var root in fileSystemRoots) '--filesystem-root=$root',
  for (var entryPointsJson in entryPointsJsonFiles)
    if (fileExists("$entryPointsJson.json")) entryPointsJson,
  mainPath
];
複製程式碼

還可以在構建集合字面量的時候 for 迴圈計算:

var integers = [for (var i = 1; i < 5; i++) i]; // [1, 2, 3, 4]
var squares = [for (var n in integers) n * n]; // [1, 4, 9, 16]
複製程式碼

構建 Map 字面量的時候,我們可能會這樣寫:

Map<String, WidgetBuilder>.fromIterable(
  kAllGalleryDemos,
  key: (demo) => '${demo.routeName}',
  value: (demo) => demo.buildRoute,
);
複製程式碼

collection-for 可以讓我們的程式碼簡化成如下所示:

return {
  for (var demo in kAllGalleryDemos)
    '${demo.routeName}': demo.buildRoute,
};
複製程式碼

collection if-for 還可以組合,例如 for 迴圈巢狀,for-if 巢狀:

//for-for
[for (var x in hor) for (var y in vert) Point(x, y)]

//for-if
[for (var i in integers) if (i.isEven) i * i]
複製程式碼

總結

Dart 的展開操作符 (Null-aware spread、Non-null-ware spread) 和 Control flow collections(collection if/for)

讓我們編寫的程式碼的時候程式碼的可讀性更高

特別是編寫 Flutter UI 的時候更符合 declarative style 程式碼風格

從中可以看出 Dart 程式碼的表現力還是很強的

Reference

關於 Dart 展開操作符 和 Control Flow Collections 就講到這裡, 更多的關於 Android 學習資料可以檢視我的GitHub: github.com/chiclaim/An…

flutter.dev/docs/get-st… github.com/dart-lang/l… github.com/dart-lang/l…


聯絡我

下面是我的公眾號,乾貨文章不錯過,有需要的可以關注下,有任何問題可以聯絡我:

公眾號:  chiclaim

相關文章