0%

【文章翻譯】Future vs Future, what’s the difference?

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

Future 與 Future,有什麼區別?

在 Dart 2 的諸多升級中(除了更好的靜態檢查、運行時類型安全、可選的 new/const、核心函式庫改進等等),一個值得一提的改進是 void 的正式化,使其更加實用且更不容易出錯。這在異步程式設計中尤其清晰,您可以為異步函數編寫 Future<void>,這些函數在工作完成時不返回結果。在此之前,您可能使用了 Future<Null>,所以我們經常被問到的問題是:Future<void>Future<Null> 之間有什麼區別?我應該使用哪一個,以及何時使用?

由於我的目標是提供有用的資訊,我將從 TLDR 開始。

TL;DR:99.99% 的情況下,您應該使用 void 類型。

我還建議您現在就在您的專案中啟用兩個與 void 相關的lint 規則

  • prefer_void_to_null: 幫助您改掉輸入幾乎過時的 Null 的習慣。
  • void_checks: 為您提供可能更直觀的 void 語義,即使它們對於安全程式碼並非絕對必要。

本文的其餘部分並非輕鬆閱讀。它結合了歷史、邊緣案例和類型理論。我接下來要寫的就像試圖解釋單子,但我會盡力而為,希望您能跟上。

快速 Future 小知識

為了展示閱讀本文的價值,我為您提出了一個小知識問題。

假設我們將自己限制在兩個 Future - 一個 Future<User> 和一個 Future<Null>。以下程式碼行會做什麼?

1
2
// 這段程式碼在您的編輯器中會失敗嗎?
Future<User> f1 = Future<Null>.value(null);
1
2
// 這段程式碼在運行時會失敗嗎?
Future<Null> f2 = Future<User>.value(0);

如果我們改用 await 呢?

1
2
User u = await Future<Null>.value(null);
Null n = await Future<User>.value(0);

暫停以產生戲劇性效果

答案是,除非您禁用了隱式向下轉型,否則這四行程式碼都不會在您的編輯器中顯示任何錯誤。沒錯,使用 Future<Null> 並假裝它是它不是的東西,例如 Future<User>,是 100% 合法的。Future<void> 就不是這樣!

同樣不直觀的是,第二行和第四行在運行時會失敗(以這種方式丟棄 Future 的結果是不「安全」的),而其他行則靜默地成功。

這些行為應該會讓您感到不安。但別擔心!您已經知道解決方案:使用 Future<void>。我將分析為什麼這種不直觀的行為會這樣工作。

void 現在是如何工作的

我一直在思考本文的最佳順序。這裡有很多概念需要解釋。但是,我會盡量按照從最有用的資訊到最不有用的資訊的順序進行。因此,由於 void 在 Dart 2 中比 Null 更有用,因此從 Dart 2 void 語義開始是有意義的。

Dart 中 void 背後的基本思想源於 void 值不應使用的目標。

1
2
void f() {}
print(f()); // 錯誤!

希望這就是您期望發生的!到目前為止,一切都很好。

值得注意的是,實現這個目標並且將 void 作為一個相當普通的類型公開會有點奇怪:

1
2
3
4
f(void argument) {
print(argument); // 錯誤!
// 我們可以接收參數,但我們不能使用它!
}

這就是 Dart 2 中我們新的「廣義 void」開始變得酷炫和強大的地方……儘管有時會讓人感到困惑。

我向您提出一個問題……我們可以傳遞什麼類型的值作為 f 的參數?

1
2
f(void argument) { … }
f(x); // x 是什麼?

答案是……任何東西!因為我們不允許您在函數 f 中使用 x 的值,所以我們可以自信地說 x 可以取的任何值都不會導致任何運行時錯誤。

1
2
3
4
f(1); // 與以下程式碼沒有區別
f("foo"); // 與以下程式碼沒有區別
f([1, 2, 3]); // 與以下程式碼沒有區別
// ...

如果我們根據 void 是一個不會被使用的值的思想推導出 void 的最佳語義,這就是我們得到的結果。這意味著,它是一個可以用任何東西填充的值。它就像一個真空:一個沒有輸出的輸入。

現在,在某些情況下,某些東西在運行時總是沒有錯誤,但仍然應該導致靜態錯誤。這是一個 lint 的絕佳案例!事實上,這就是 void_checks lint 背後的思想。它會尋找您將 null 以外的任何東西傳遞到 void 位置的地方,我鼓勵團隊啟用它。它並非健全性所必需的,但是將 null 以外的任何東西傳遞到 void 位置仍然可能是一個意外,lint 會為您標記它。

由於所有這些都基於基礎類型理論,因此 void 可以與 Dart 的每個部分都很好地配合使用,即使在利用類型推斷的 Future<void> 上下文中的延續也是如此:

1
2
3
4
Future<void>().then((x) {
print(x); // 錯誤!x 的類型為「void」,所以您不能列印它!
// 這就是我們想要的!
});
1
2
// 是的,這也是一個錯誤:
Foo f = await voidFuture();

此時,普通開發人員可能已經了解了有效使用 void 所需知道的一切。

然而,如果您仍在繼續閱讀,還有一些更有趣的東西。

即使啟用了 void_checks,也不能保證 void 位置包含 null。以下列覆寫為例:

1
2
3
4
5
6
7
8
class A {
void f(Object o) {}
}

class B extends A {
@override
Object f(Object o) => o;
}

我們不想讓此覆寫非法,因為它是安全的、有用的,並且與 Dart 1 相比是一個重大變更。因此,我們不得不接受 void 位置可能包含任何值。我們也不能「優化掉」A.f() 返回的值,因為在運行時它可能是一個 B!

相反,我們有一個聰明的選擇,即讓 void 成為 Object 的姊妹類型。畢竟,它可以包含任何值,並且所有值都是物件。這不是我們設計的東西,它只是現實。認識到現實,我們就可以利用它。

透過使 void 成為 Object 的同級,我們沒有任何要求 void 值完全未使用。我們盡力讓 void 保持自身,但為了向後兼容性,我們特意在一些地方放鬆了這些限制:

1
2
dynamic f() => voidFn(); // 這是合法的
voidFn() as dynamic; // 這是合法的

這些是對於 Object 合法的特殊情況,為了使 Dart 2 更順利地推出,我們也讓它對 void 合法。

使 void 成為 Object 的同級也意味著 void 可以用作類型參數,而不會使編譯後的輸出膨脹(對於 C++ 使用者來說,沒有「模板特化」)。這種減少有助於保持 Web 和 Flutter 應用程式的小巧,但代价是允許以下操作:

1
<void>[1, 2, 3].toString(); // 這是合法的,並列印 [1, 2, 3]

最後,將參數類型指定為 void 可能看起來沒有用,尤其是因為它是 Object 的一種形式。然而,void 值可以傳遞到 void 位置

1
2
f(void x) { … }
f(voidFn()); // 這是合法的

這對於排序很有用,例如在模擬返回 void 的方法時,Mockito 會使用它。(將上面的程式碼中的 f 替換為 when 以獲得更接近的近似值。)

總而言之:

  • void 類型是 Object 的同級。
  • 幾乎總是,void 物件不能被使用。
  • 標記為 void 的東西實際上可以是任何東西。
  • 任何東西都可以「丟棄」到標記為 void 的位置,而 lint void_checks 限制了這種行為。
  • void 值可以傳遞到其他 void 位置。

底部類型

在我開始討論 Null 之前,我們必須先討論「底部」類型。

這是一個類型理論中自然發生的類型,它有一個簡短的學術定義和一些實際應用。

如果您因為它太奇怪了而停止閱讀本節,那就是我最初 TLDR 的有力證據。除非您希望您的程式碼同樣奇怪,否則您可能需要 void。現在讓我們探索這個奇怪的兔子洞,看看它有多深,好嗎?

底部類型是所有類型的子類型。用更簡單的面向對象術語來說,這意味著它是一個人。還是一輛車。還是一種動物。還有來自每個程式的所有其他類型。

如果這聽起來很荒謬,那是因為它確實如此。我喜歡將它視為一個「佔位符」類型。但是「荒謬的」或「虛構的」類型也是一個合理的稱呼。然而,它被稱為底部類型,因為它是類型層次結構的底部,在電腦科學中,它是顛倒的。¯_(ツ)_/¯。正式地,它可以用符號 ⊥ 表示,您可能也認識到它是「假」的符號。

如果您試圖想像一個既是人又是又是動物的值,您將無法想到任何東西。令人驚訝的是,這就是 ⊥ 的實際用途的來源!

如果我編寫一個永不返回的函數會怎樣?有兩種簡單的方法可以做到這一點:

1
2
3
loopForever() {
while(true); // 第一種方法
}
1
2
3
alwaysThrow() {
throw Exception(); // 第二種方法
}

這兩個函數的最佳返回類型是什麼?

這取決於情況。因為函數永不返回,所以返回類型並不重要。您可以使用任何類型 - 即使是荒謬的底部類型,它可以在各種語言中以各種方式使用。

C++ 將其稱為 noreturn。Rust 有 !,Scala 有 Nothing。但我最喜歡的例子來自 Haskell,它有一個非常常用的 undefined 函數,它會中止程式。它返回,您猜對了,底部類型。

如果我們假設 Dart 中有一個 undefined 函數,就像 Haskell 中的那樣,它在被調用時只拋出一個異常並返回 ⊥,那麼它看起來像這樣:

1
Foo foo = cond ? val : undefined();

在示例用法行中,當 cond 為真時,程式可以安全地運行並儲存 val。而當 cond 為假時,程式將在 undefined() 期間拋出異常。將 undefined() 的結果「儲存」到 foo 中是安全的,無論 foo 的類型如何,因為該儲存永遠不會真正發生!

undefined() 這裡沒有返回任何東西。但這裡的教訓不是我們可以讓 foo 為空……而是底部類型像一個空的承諾一樣是空的。它比空還空。它永遠不會發生。

我必須小心地說明的一點是,您可以在實踐中從這些函數中返回 void,並且根據用法,通常應該返回 void。通常,像 return loopForever() 這樣的程式碼更有可能是一個錯誤,而不是一個有用的模式。然而,您可以自行選擇。

底部類型也適用於唯讀空列表。在 Dart 中,List 是協變的,因此允許將 List<int> 用作 List<Object>。如果您試圖將 String 放入該 List<Object> 中,運行時檢查會為您捕獲該錯誤並拋出錯誤。

這意味著,如果我們建立一個 ⊥ 列表,我們就無法在其中放入任何東西,但我們可以將其視為任何東西的列表:

1
2
3
4
List<int> intList = <⊥>[];
for (int i in intList) {
print(i * 2); // 有效,因為這永遠不會發生
}

當您查看所謂的底部類型的「逆變」位置時,還會有更多有趣的案例。

假設我們定義了一個具有 ⊥ 參數的函數:

1
void f(⊥ x) {}

這幾乎與 undefined() 示例相反。我們不是聲明了一個永不返回的函數,而是聲明了一個無法被調用的函數!沒有實際值可以賦值給該參數 x。您不能傳入 Person,因為它也不是 Car,並且您不能傳入 Car,因為它也不是 Person。您唯一可以傳入的就是荒謬的類型本身:

1
f(undefined());

但是正如我們之前所討論的,undefined() 永不返回,所以在這種情況下,f() 仍然永遠不會被實際調用!

將參數類型指定為 ⊥ 可能看起來沒有用,但它具有深奧的價值,因為所有可以被調用的函數都是不能被調用的函數的子類型。(想一想:一個可以被調用的函數不一定要被調用。如果一個函數沒有被調用,它就不會產生運行時錯誤。)

如果您還在繼續閱讀,請深呼吸,並拍拍自己的背。

具體來說,將任何 Function(X) 轉換為 Function(⊥) 是安全的,對於任何 X 都是如此。這比使用 Dart 的包羅萬象的 Function 類型更好,因為它更具體。

例如,您可以將任何一元函數儲存在一個欄位中,並動態調用它,以繞過靜態錯誤,如果您犯了錯誤,則用運行時錯誤代替它們:

1
2
Function(⊥) f;
f = (int x) => x + 1;
1
2
// 123 作為 f 的參數的有效性在運行時檢查
(f as dynamic)(123);

這是一個在緊要關頭幫助您的小技巧。

現在我們可以討論 Null 了。

Dart 2 中的 Null

概念上的「底部」類型(所有類型的子類型)存在於 Dart 中,但這並不是全部。這樣的值也存在於 Dart 2 中!在 Dart 中,我們稱它為 Null,而該值就是 - 您猜對了 - null。

由於我可以使任何東西都為 null(/Null),所以在 Dart 中荒謬的類型並不那麼荒謬。這使得它有點複雜。

注意:值 null 當然不是 Car,也不是 Person。我們確實收到了對 Dart 中非空類型的請求。因此,如果我們確實對 Dart 進行了這樣的更改,我們將需要一個新的底部類型,可能命名為 Nothing 之類的名稱,那時它將是一個更真實的底部類型。

Null 不僅具有與底部類型相同的所有荒謬用法,而且還有一個逃生出口。如果您真的需要從一個不能返回的函數中返回,或者調用一個不能被調用的函數,我們會給您一個出路!您可以將 null 傳遞進去!老實說,如果您從整體上看,我們不這樣做會有點不公平。

但是,對於一個原本簡單的聲明,它確實給出了很多警告:

1
Null nothing() => null;

Null 是 foo() 最特定的類型,所以它是一個合乎邏輯的選擇,也是一個良好的起點。如果您在其上調用不存在的方法,分析器也會善意地警告您:

1
nothing().x; // 錯誤!Null 上沒有成員 x

但这可能会造成安全的假象。

1
2
nothing().toString(); // 沒有錯誤:Null 定義了 toString()
Foo foo = nothing(); // 沒有錯誤:foo 將變為 null
1
2
3
// 對於 f(x) 中的所有參數類型都可接受,
// 如果 f(x) 需要非 null 值,則會出現運行時錯誤
f(nothing());

如果您的目標是讓 nothing() 成為