0%

【文章翻譯】Exploring collections in Dart

【文章內容使用 Gemini 1.5 Pro 自動翻譯產生】

探索 Dart 中的集合

如果您曾經調用 add()addAll()map()toList() 來建立列表或映射,您可能需要查看 collection ifcollection forspreads。去年,Dart 在 2.3 版中加入了這些功能

在本文中,我們將研究集合,探索這些新功能,並查看一些有趣的範例。透過掌握這些功能,您可以使您的程式碼更簡潔、更易於閱讀。

集合

首先,我們需要了解什麼是集合。集合 是一個包含其他物件的物件。例如:

  • List:具有長度的有序物件集合(也稱為 陣列
  • Set:唯一物件的無序集合
  • Map:鍵值對的無序集合
  • Queue:可以在兩端添加/移除物件的有序集合
  • SplayTreeMap:基於自平衡二元樹的鍵值對的有序集合

這些類型在 dart:collection 套件中可用。如需更多集合類型,請在 pub.dev 上查看 package:collection

這些集合類型都實作了 Iterable,它提供了一些常用行為,例如在集合中的每個物件上運行函數、獲取第一個物件、確定集合的長度等等。

集合字面量

Dart 支援用於構造三種類型集合的語法:列表字面量 ([])、映射字面量 ({}) 和集合字面量(也是 {})。

以下是一個列表字面量:

1
2
3
4
5
6
7
List<String> getArtists() {
return [
'Picasso',
'Warhol',
'Monet',
];
}

以下是一個映射字面量:

1
2
3
4
5
6
7
Map<String, String> getArtistsByPainting {
return {
'The Old Guitarist': 'Picasso',
'Orange Prince': 'Warhol',
'The Water Lily Pond': 'Monet',
};
}

以下是一個集合字面量,在 Dart 2.3 中加入:

1
2
3
4
5
6
7
Set<String> getArtistsSet() {
return {
'Picasso',
'Warhol',
'Monet',
};
}

如果您想知道為什麼映射和集合可以使用相同的 {} 語法,那是因為 Dart 使用 類型推斷 來區分。類型系統根據參數 a 和 b 的類型確定類型。它通常可以根據內容確定這一點——例如,{1} 顯然是一個 Set,而 {1: 2} 顯然是一個 Map

注意: 使用 {} 預設構造一個映射。要建立一個集合,您可以使用泛型類型註釋:<String>{}。使用兩個泛型類型參數則建立一個映射:<String, String>{}

元素的類型

集合字面量中的每個項目通常是一個值或表達式,但也可以是以下新功能之一:collection ifcollection forspread。所有這些都被稱為 元素

每個元素解包零個或多個項目,並將它們放入周圍的集合中。例如,一個字串字面量(例如「oatmeal」)會產生一個項目,但 collection for 會解包 0 個或多個項目。這些功能也可以以有趣的方式組合,我們將在下面探討。

Spreads

spread 接收一個集合(例如,一個列表),並將其內容放入周圍的集合中:

1
2
3
4
5
6
List<String> combineLists(List<String> a, List<String> b) {
return [
...a,
...b,
];
}

前面的程式碼相當於:

1
2
3
4
5
6
List<String> combineLists(List<String> a, List<String> b) {
var list = [];
list.addAll(a);
list.addAll(b);
return list;
}

您也可以在映射和集合字面量中使用 spreads:

1
2
3
4
5
6
Map<String, String> combineMaps(Map<String, String> a, Map<String, String> b) {
return {
...a,
...b,
};
}
1
2
3
4
5
6
Set<String> combineSets(Set<String> a, Set<String> b) {
return {
...a,
...b,
};
}

在映射和集合中,當發生衝突時,b 的內容會覆蓋 a 中的內容。例如,調用 combineMaps({'foo': 'bar'}, {'foo': 'baz'}) 會產生一個包含 {'foo': 'baz'} 的映射。

Null-aware spreads (…?)

null-aware spread 僅當運算符後的表達式為非 null 時才將內容加入到集合中:

1
2
3
4
5
6
List<String> combineIfExists(List<String> a, List<String> b) {
return [
...?a,
...?b,
];
}
1
2
3
4
void main() {
var result = combineIfExists(['foo'], null);
print(result); // [foo]
}

Collection if

使用 ifelseelse if 關鍵字根據條件將某些內容加入到集合中。以下是一個使用 collection if 的範例:

1
2
3
4
5
6
7
8
9
10
11
12
13
class Article {
String title;
DateTime date;

Article(this.title, this.date);

String toString() {
return [
if (title != null) title,
date.toString(),
].join(', ');
}
}

可以在末尾加入 else 關鍵字:

1
2
3
4
5
String toString() {
return [
if (title != null) title else '(no title)',
].join(', ');
}

請注意逗號的位置。逗號不能在 title 之後,因為 else 是同一個元素的一部分。將 ifelse 保持在一起,在逗號之前,可以將它們與集合中的下一個元素區分開來。

加入 else if 也可以:

1
2
3
4
5
6
7
8
9
10
11
String toString() {
return [
if (title != null) title else '(no title)',
if (date == null)
'(no date)'
else if (date.year == DateTime.now().year)
'this year'
else
'${date.year}',
].join(', ');
}

Collection for

最後,使用 for 關鍵字將序列插入到集合中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Article {
String title;
DateTime date;
List<String> tags;

Article(this.title, this.date, this.tags);

String toString() {
return [
title,
date.toString(),
for (var tag in tags) 'tag: $tag'
].join(', ');
}
}

在此範例中,for 表達式為 tags 列表中的每個項目加入一個字串。就像 Dart 中的普通 for 迴圈一樣,tags 表達式可以是任何 Iterable

Flutter 程式碼中的集合

如果您正在使用 Dart,很有可能您正在使用它來構建 Flutter 應用程式。由於這裡描述的功能是在設計時考慮到 Flutter 的,讓我們來看看一些 Flutter 程式碼。

重構 build() 方法

在 Flutter 中,通常在 build() 方法中構建 Widget 列表:

1
2
3
4
5
6
7
8
9
10
@override
Widget build(BuildContext context) {
var articleWidgets = articles
.map<Widget>((article) => ArticleWidget(article: article))
.toList();

return ListView(
children: articleWidgets,
);
}

可以使用 spread 重寫此程式碼:

1
2
3
4
5
6
7
8
Widget build(BuildContext context) {

return ListView(
children: [
...articles.map((article) => ArticleWidget(article:article))
],
);
}

或者使用 collection for

1
2
3
4
5
6
7
8
Widget build(BuildContext context) {
return ListView(
children: [
for (var article in articles)
ArticleWidget(article: article)
],
);
}

第一個程式碼片段使用 map()Article 類別轉換為 ArticleWidget 物件的集合,然後應用 spread 運算符將它們展開到周圍的列表中。在第二個範例中,collection for 運算符讓您可以更簡潔地表達這一點。

更大的 build() 方法

以下是一個更複雜的範例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Widget build(BuildContext context) {
var headerStyle = Theme.of(context).textTheme.headline4;

return Column(
children: [
if (article.title != null)
Text(article.title, style: headerStyle),
if (article.date != null)
Text(article.date.toString()),
Text('Tags:'),
for (var tag in article.tags)
Text(tag),
],
);
}

放置 Widget 到 Column 中的邏輯就在讀者可能期望的位置,並且節省了大量程式碼。在這些功能出現之前,實現相同行為最常用的方法是建立一個變數,並使用調用 add() 的普通 if 陳述式。

組合這些功能

這些功能可以以有趣的方式組合,如本節中的範例所示。以下是一些需要注意的事項:

  • 從語法上講,collection ifcollection forspread 是一個 單個元素——即使它最終建立了多個物件。
  • 任何表達式都可以放在 collection ifcollection for 的主體中。
  • 任何 元素 都可以放在 collection ifcollection for 的主體中。

結合使用 if 和 for

以下是一些使用 collection for 內部的 collection if 建立列表的程式碼。在這裡,如果每個文章的日期晚於特定日期,則將其加入到列表中:

1
2
3
4
5
6
7
8
List<Article> recentArticles(List<Article> allArticles) {
var ninetyDaysAgo = DateTime.now().subtract(Duration(days: 90));
return [
for (var article in allArticles)
if (article.date.isAfter(ninetyDaysAgo))
article
];
}

如果您更喜歡 spreads,則返回值可以寫成 ...allArticles.where((article) => article.date.isAfter(ninetyDaysAgo))

將 collection if 和 spreads 結合使用

collection if 接收單個元素,但如果您想包含多個元素,則可以使用 spread

1
2
3
4
5
6
7
8
9
10
Widget build(BuildContext context) {
return Column(
children: [
if (article.date != null) ...[
Icon(Icons.calendar_today),
Text('${article.date}'),
],
],
);
}

將集合功能與 async-await 結合使用

您也可以將非同步調用與集合字面量結合使用。例如,一個常見的模式是使用 Future.wait() 觸發一組非同步調用:

1
2
3
4
5
6
7
8
9
Future<Article> fetchArticle(String id);

Future<List<Article>> fetchArticles() async {
return Future.wait([
fetchArticle('1'),
fetchArticle('2'),
fetchArticle('3'),
]);
}

可以使用 collection for 改進該程式碼:

1
2
3
4
5
6
Future<List<Article>> fetchArticles(List<String> ids) async {
return Future.wait([
for (var id in ids)
fetchArticle(id),
]);
}

也可以在集合字面量中放入 await,儘管它會依次等待每個 Future

1
2
3
4
5
6
7
Future<List<Article>> fetchArticles(List<String> ids) async {
return [
// 一次獲取一個
for (var id in ids)
await fetchArticle(id),
];
}

前面的程式碼會依次等待,因為它相當於以下程式碼:

1
2
3
4
5
6
7
8
Future<List<Article>> fetchArticles() async {
return <Article>[
// 一次獲取一個
await fetchArticle('1'),
await fetchArticle('2'),
await fetchArticle('3'),
];
}

您也可以使用 await for 展開 Stream

1
2
3
4
5
6
7
8
9
10
11
Stream<String> get idStream => Stream.fromIterable(['1','2','3']);
Future<List<String>> gatherIds(Stream<String> ids) async {
return [
await for (var id in ids)
id
];
}

void main() async {
print(await gatherIds(idStream)); // [1, 2, 3]
}

這是 collection ifcollection forspreads 如何與語言的其他部分一起使用的另一個範例。如果您使用過 await for 陳述式,您可能會猜到其行為:它偵聽 Stream 中的新值,並將主體放入周圍的列表中。

進一步探索

希望這些技巧能幫助您編寫更乾淨的 Dart 程式碼。除了這裡提到的之外,還有更多使用這些功能的方法。如果您發現了一個好的技巧,請與社群分享在 Twitter 上提及 @dart_lang。如需更多詳細資訊,請查看使 Dart 成為更好的 UI 語言或 GitHub 上的初始語言提案


探索 Dart 中的集合 最初發佈在 Dart 上的 Medium,人們在那裡透過突出顯示和回應這個故事來繼續討論。