0%

【文章翻譯】Dart 2: Legacy of the `void`

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

Dart 2:void 的遺產

Dart 2 中類似 void 類型宇宙的半精確描述

我在 StackOverflow、Gitter 甚至 Google 內部支援頻道上看到的最常被問到的問題之一是 Dart 2 中以下內建類型之間的差異:ObjectdynamicvoidNull

長話短說,Null(或其他語言中的 Bottom,即「無」)不應該在大部分真實使用者程式碼中使用,我懷疑在不久的將來我們會看到更多文章和 lint 來溫和地阻止使用。

其餘三個類型則不太清楚,因為在 Dart 2 中,任何東西在執行時都可以是 dynamicObjectvoid,僅根據 靜態 類型簽章而有所不同。因此,讓我們看看幾個 何時 應該使用哪種類型簽章的實際範例。

Object

Object 是 Dart 類別階層的根類別,Dart 中的每個其他類別都是 Object 的子類別——包括像 intdoublebool 這樣的「原始」類型。它保證了一些東西:一個 hashCode 屬性,一個 == 運算子,一個 toString 方法

實際上,我使用 Object 作為窮人的聯合類型——期望使用者在使用某個東西之前使用 is 運算子來確定它的真實類型。**我不使用 dynamic**,因為正如下一節所述,它會停用重要的靜態分析,並且更容易讓您進入無效狀態。

1
Object readProperty(String name) { ... }
1
2
3
4
5
6
7
8
void main() {
var age = readProperty('name');
if (age is int) {
print('I am $age years old');
} else if (age is String) {
print(age);
}
}

另一個選項是使用 Object 來宣告您 不關心 資料結構的內部類型是什麼,例如 List<Object> 可能表示「任何東西的列表」。例如,在編寫一個組合 List 中每個元素的 hashCode 的函式時,這就派上用場了:

1
int hashList(List<Object> elements) { ... }

Object 的一個很好的特性(與 dynamic 相比)是,如果您嘗試在其上調用一個不存在的方法,您將立即獲得分析和編譯器回饋。例如,以下程式碼會產生一個 靜態錯誤

1
2
3
4
void main() {
Object a = 5;
a.aMethodThatDoesNotExist();
}

然而,在實際應用中,Object 是相當(並且有意地)有限的。我希望 Dart 將獲得對方法重載的支援,這將允許我在真實程式碼中顯著減少 Object 類型的使用。

dynamic

我個人在 Dart 2 中 從不 使用 dynamic 類型。在我看來,它是 Object 和一個特殊指令的聯合,該指令告訴工具和編譯器 停用靜態分析檢查。也就是說,以下程式碼是合法的,並且只會在執行時出現錯誤(而不是靜態地!):

1
2
3
4
void main() {
dynamic x = 5;
x.aMethodThatDoesNotExist();
}

在 Dart 1 中,dynamic 無處不在,任何其他靜態類型都是為了 IDE 和靜態分析支援——但編譯器(和執行時)將所有東西都視為 dynamic。儘管如此,在 Dart 2 中仍然有一些不幸的「陷阱」可能會 意外地 建立一個動態類型的變數:

1
computeAge() => 5; // 返回類型是 dynamic
1
2
3
4
void main() {
var name; // 靜態類型是 dynamic
var animals = []; // 靜態和執行時類型是 List<dynamic>
}

更糟糕的是,dynamic 調用會丟失 Dart 2 中至關重要的類型資訊:

1
2
3
class User {
String name;
}
1
2
3
4
5
6
7
void main() {
var users = []; // 隱式地是 List<dynamic>,還記得嗎?
users.add(new User()..name = 'Matan');

// 執行時錯誤:List<dynamic> 不是 Iterable<String>
Iterable<String> names = users.map((u) => u.name);
}

發生此錯誤的原因是因為這裡的實際調用是:

1
users.map((dynamic u) => u.name);

…它沒有足夠的靜態類型資訊來產生 Iterable<String>。透過將 users 修正為正確的類型(並避免動態調用),一切正常:

1
2
3
4
5
6
7
void main() {
// 我們也可以寫成 `var users = <User>[
var users = [new User()..name = 'Matan'];

// OK!
Iterable<String> names = users.map((u) => u.name);
}

void

最後,我們有 void,這是 Dart 2 中最新的類型。在 Dart 1 中,void 只能用作函式的返回類型(例如 void main()),但在 Dart 2 中,它已被 泛化,並且可以在其他地方使用,例如 Future<void>

void 類型在語義上類似於 Object(它可以是任何東西),但有一些額外的限制——void 類型不能用於任何東西(即使是 ==hashCode),並且將某個東西賦值給 void 類型是無效的:

1
2
3
4
5
void foo() {}

void main() {
var bar = foo(); // 無效
}

實際應用中,我使用 void 來表示「任何東西,我不關心元素」,或者更常見的是表示「省略」,例如在 Future<void>Stream<void> 中:

1
2
/// 清除快取。
Future<void> purgeCache() { ... }

在上面的程式碼片段中,我不希望使用者嘗試使用提供的 Future 的返回值,因為它不相關。我見過使用 Future<Null> 來達到此目的的範例,這實際上是在 Future<void> 成為可能 之前 的一種解決方法。

例如,這在靜態上是正常的,但在執行時在 Dart 2 中是無效的:

1
2
3
4
5
6
7
8
9
import 'dart:async';

Future<String> _doAThing() async => 'Test';
Future<Null> doAThing() async => _doAThing();

void main() async {
// Future<String> 不是 FutureOr<Null> 類型的子類型
await doAThing();
}

…而使用 Future<void> 作為 doAThing() 是有效且正確的。

另一個例子可能是不帶任何事件資料的 Stream

1
2
/// 當使用者登出系統時觸發事件。
Stream<void> get onLogOut { ... }

另一個更實際的用途是實作一個具有您不會使用的泛型類型引數的類別。例如,實作流行的 訪問者模式,當 C(上下文)類型引數未使用時,我們可以透過傳遞 void 來忽略它:

1
2
3
4
5
6
7
8
abstract class Visitor<N, C> {
N visitNode(N node, [C context]);
}

class IdentityVisitor<N> extends Visitor<N, void> {
@override
N visitNode(N node, [_]) => node;
}

我希望這篇簡短的文章可以幫助您圍繞使用 Objectdynamicvoid 做出 API 決策。如果您有任何其他問題或想法,請留言!


Dart 2:void 的遺產 最初發佈於 Medium 上的 Dart,人們在那裡透過突出顯示和回應這個故事來繼續討論。