[TOC]
在程序中記下類型時,會限制流入代碼不同部分的值的類型。類型可以出現在兩種地方:聲明上的類型注釋和泛型調用的類型參數。
當您想到“靜態類型”時,類型注釋是您通常會想到的。您可以鍵入注釋變量,參數,字段或返回類型。在以下示例中,bool和String為類型注釋。它們掛起代碼的靜態聲明結構,并且不會在運行時“執行”。
~~~
bool isEmpty(String parameter) {
bool result = parameter.length == 0;
return result;
}
~~~
泛型調用是集合字面量、對泛型類構造函數的調用或泛型方法的調用。在下一個示例中,num和int是泛型調用的類型參數。盡管它們是類型,但它們是一級實體,在運行時被具體化并傳遞給調用。
~~~
var lists = <num>[1, 2];
lists.addAll(List<num>.filled(3, 4));
lists.cast<int>();
~~~
我們在這里強調“泛型調用”部分,因為類型參數也可以 出現在類型注釋中:
~~~
List<int> ints = [1, 2];
~~~
在這里,int是一個類型參數,但是它出現在類型注釋中,而不是泛型調用中。您通常不需要擔心這種區別,但是在一些地方,對于在通用調用中使用類型而不是類型注釋,我們有不同的指導教程。
在大多數情況下,Dart允許您省略類型注釋,并根據附近的上下文為您推斷類型,或者默認為動態類型。Dart同時具有類型推斷和動態類型的事實導致了人們對代碼是“非類型”的含義的困惑。這是否意味著代碼是動態類型的,或者您沒有編寫類型?為了避免混淆,我們避免說“untyping”,而是使用以下術語:
* 如果代碼是帶類型注釋的,則該類型顯式地寫在代碼中。
* 如果推斷出代碼,則沒有編寫類型注釋,Dart自己成功地找到了類型。推理可能會失敗,在這種情況下,指南不考慮推斷。在某些地方,推理失敗是靜態錯誤。在其他情況下,Dart使用了動態備份類型。
* 如果代碼是動態的,那么它的靜態類型就是特殊的動態類型。代碼可以被顯式地注釋為動態的,也可以被推斷出來。
換句話說,某些代碼是注釋的還是推斷的,與它是動態的還是其他類型的正交。
推理是一種強大的工具,可以讓您省去編寫和閱讀那些明顯或無趣的類型的工作。在明顯的情況下省略類型也會將讀者的注意力吸引到顯式類型上,因為這些類型很重要,比如強制類型轉換。
顯式類型也是健壯,可維護代碼的關鍵部分。它們定義了API的靜態形狀。它們記錄并強制允許哪些值允許到達程序的不同部分。
這里的指導方針在我們在簡潔性和明確性,靈活性和安全性之間找到了最佳平衡。在決定要編寫哪些類型時,您需要回答兩個問題:
* 我應該寫哪種類型,因為我認為最好讓它們在代碼中可見?
* 我應該寫哪種類型因為推理無法為我提供這些類型?
這些指南可以幫助您回答第一個問題:
* 如果類型不明顯,則優先對公共字段和頂級變量進行類型注釋。
* 如果類型不明顯,請考慮對私有字段和頂級變量進行類型注釋。
* 避免類型注釋初始化的局部變量。
* 避免在函數表達式上注釋推斷的參數類型。
* 避免泛型調用上的冗余類型參數。
這些涵蓋了第二個:
* 當Dart推斷出錯誤的類型時,請進行注釋。
* 優先使用動態注釋,而不是讓推斷失敗。
其余指南涵蓋了有關類型的其他更具體的問題。
## 如果類型不明顯,則優先對公共字段和頂級變量進行類型注釋。
類型注釋是關于如何使用庫的重要文檔。它們在程序的區域之間形成邊界以隔離類型錯誤的來源。考慮:
~~~
【bad】
install(id, destination) => ...
~~~
這里,id是什么還不清楚。一個字符串?目的是什么?字符串還是文件對象?這個方法是同步的還是異步的?這是清晰的:
~~~
Future<bool> install(PackageId id, String destination) => ...
~~~
但在某些情況下,類型是如此明顯,以至于編寫它是毫無意義的:
~~~
const screenWidth = 640; // Inferred as int.
~~~
“顯而易見”并沒有明確的定義,但這些都是很好的候選者:
* 字面量。
* 構造函數調用。
* 對顯式類型化的其他常量的引用。
* 數字和字符串的簡單表達式。
* 工廠方法,如int.parse()、Future.wait()等,讀者應該很熟悉。
如果有疑問,請添加類型注釋。即使類型很明顯,您可能仍然希望顯式注釋。如果推斷類型依賴于來自其他庫的值或聲明,您可能希望鍵入注釋您的聲明,以便對其他庫的更改不會在您沒有意識到的情況下悄無聲息地更改您自己的API的類型。
## 如果類型不明顯,請考慮對私有字段和頂級變量進行類型注釋。
在公開聲明上鍵入注釋可以幫助代碼的用戶。私有成員上的類型幫助維護人員。私有聲明的范圍更小,那些需要知道聲明類型的人也更可能熟悉周圍的代碼。這使得更依賴于推理和省略私有聲明類型變得合理,這就是為什么這個指南比上一個指南更溫和的原因。
如果您認為初始化器表達式(無論它是什么)足夠清晰,那么您可以省略注釋。但是如果您認為注釋有助于使代碼更清晰,那么添加一個注釋。
## 避免類型注釋初始化的局部變量。
局部變量的作用域非常小,尤其是在函數往往很小的現代代碼中。省略類型會將讀者的注意力集中在變量的更重要的名稱及其初始值上。
~~~
List<List<Ingredient>> possibleDesserts(Set<Ingredient> pantry) {
var desserts = <List<Ingredient>>[];
for (var recipe in cookbook) {
if (pantry.containsAll(recipe)) {
desserts.add(recipe);
}
}
return desserts;
}
~~~
~~~
【bad】
List<List<Ingredient>> possibleDesserts(Set<Ingredient> pantry) {
List<List<Ingredient>> desserts = <List<Ingredient>>[];
for (List<Ingredient> recipe in cookbook) {
if (pantry.containsAll(recipe)) {
desserts.add(recipe);
}
}
return desserts;
}
~~~
如果局部變量沒有初始化器,則無法推斷其類型。在這種情況下,注釋是一個好主意。否則,您將獲得動態,并失去靜態類型檢查的好處。
~~~
List<AstNode> parameters;
if (node is Constructor) {
parameters = node.signature;
} else if (node is Method) {
parameters = node.parameters;
}
~~~
## 避免在函數表達式上注釋推斷的參數類型。
匿名函數幾乎總是立即傳遞給具有某種回調的方法。(如果沒有立即使用該函數,通常值得將其命名為聲明。)當在類型化上下文中創建函數表達式時,Dart試圖根據預期的類型推斷函數的參數類型。
例如,當您將函數表達式傳遞給Iterable.map()時,根據map()期望的回調類型推斷函數的參數類型:
~~~
var names = people.map((person) => person.name);
~~~
~~~
【bad】
var names = people.map((Person person) => person.name);
~~~
在極少數情況下,周圍的上下文不夠精確,無法為一個或多個函數參數提供類型。在這些情況下,您可能需要注釋。
## 避免泛型調用上的冗余類型參數。
如果推理要填充相同的類型,那么類型參數就是冗余的。如果調用是類型注釋變量的初始化器,或者是函數的參數,那么推斷通常會為您填充類型:
~~~
Set<String> things = Set();
~~~
~~~
【bad】
Set<String> things = Set<String>();
~~~
這里,變量上的類型注釋用于推斷初始化器中構造函數調用的類型參數。
在其他情況下,沒有足夠的信息來推斷類型,然后你應該寫類型參數:
~~~
var things = Set<String>();
~~~
~~~
【bad】
var things = Set();
~~~
在這里,由于變量沒有類型注釋,因此沒有足夠的上下文來確定要創建哪種類型的集合,因此應該顯式地提供類型參數。
## 當Dart推斷出錯誤的類型時,請進行注釋。
有時,Dart推斷出一種類型,但不是你想要的類型。例如,您可能希望變量的類型是初始化程序類型的超類型,以便稍后可以為變量分配一些其他同級類型:
~~~
num highScore(List<num> scores) {
num highest = 0;
for (var score in scores) {
if (score > highest) highest = score;
}
return highest;
}
~~~
~~~
【bad】
num highScore(List<num> scores) {
var highest = 0;
for (var score in scores) {
if (score > highest) highest = score;
}
return highest;
}
~~~
在這里,如果分數包含double值,比如[1.2],那么賦值最高的值就會失敗,因為它的推斷類型是int,而不是num。
## 優先使用動態注釋,而不是讓推斷失敗。。
Dart允許您在許多地方省略類型注釋,并嘗試為您推斷類型。在某些情況下,如果推理失敗,它將無聲地為您提供動態。如果您想要的是dynamic,那么從技術上來說,這是最簡潔的方法。
然而,這并不是最明確的方式。如果您的代碼的臨時讀者看到注釋不見了,那么他就無法知道您是否希望它是動態的、預期的填充其他類型的推斷,或者只是忘記編寫注釋。
當您想要的類型是dynamic時,明確地編寫它可以使您的意圖變得清晰。
~~~
dynamic mergeJson(dynamic original, dynamic changes) => ...
~~~
~~~
【bad】
mergeJson(original, changes) => ...
~~~
>在Dart 2之前,這條指南提出了完全相反的觀點:當它是隱含的時,不要用動態注釋。有了新的更強大的類型系統和類型推斷,用戶現在希望Dart表現得像一種推斷的靜態類型語言。有了這個心智模型,發現一個代碼區域已經悄無聲息地失去了所有靜態類型的安全性和性能是一個令人不快的意外。
>
## 優先選擇函數類型注釋中簽名。。
標識符函數本身沒有任何返回類型或參數簽名,是指特殊的函數類型。這種類型只比使用dynamic稍微有用一點。如果要注釋,請選擇包含參數和函數返回類型的完整函數類型。
~~~
bool isValid(String value, bool Function(String) test) => ...
~~~
~~~
【bad】
bool isValid(String value, Function test) => ...
~~~
該指南的一個例外是,如果您想要一個表示多個不同函數類型聯合的類型。例如,您可以接受接受一個參數的函數或接受兩個參數的函數。因為我們沒有union類型,所以無法精確地鍵入它,通常需要使用dynamic。函數至少比這更有幫助:
~~~
void handleError(void Function() operation, Function errorHandler) {
try {
operation();
} catch (err, stack) {
if (errorHandler is Function(Object)) {
errorHandler(err);
} else if (errorHandler is Function(Object, StackTrace)) {
errorHandler(err, stack);
} else {
throw ArgumentError("errorHandler has wrong signature.");
}
}
}
~~~
## 不要為setter指定返回類型。
在Dart中setter總是返回void。所以設定void類型毫無意義。
~~~
【bad】
void set foo(Foo value) { ... }
~~~
~~~
set foo(Foo value) { ... }
~~~
## 不要使用遺留類型定義語法。
Dart有兩種符號,用于為函數類型定義命名的typedef。原始語法如下:
~~~
【bad】
typedef int Comparison<T>(T a, T b);
~~~
該語法有幾個問題:
* 沒有辦法將名稱分配給泛型函數類型。在上面的例子中,typedef本身是通用的。如果在代碼中引用比較,沒有類型參數,就會隱式地得到函數類型int函數(dynamic, dynamic),而不是int Function\<T>(T, T)。
* 參數中的單個標識符被解釋為參數的名稱,而不是其類型。考慮到:
~~~
【bad】
typedef bool TestNumber(num);
~~~
大多數用戶期望這是一個取num并返回bool的函數類型。它實際上是一個接受任何對象(動態)并返回bool的函數類型。該參數的名稱(除了typedef文檔外,它不用于任何內容)是“num”。這是Dart長期以來的錯誤來源。
新語法如下所示:
~~~
typedef Comparison<T> = int Function(T, T);
~~~
如果要包含參數的名稱,也可以這樣做:
~~~
typedef Comparison<T> = int Function(T a, T b);
~~~
新語法可以表達舊語法可以表達的任何內容,而且缺少容易出錯的錯誤特性,即將單個標識符視為參數的名稱而不是其類型。在typedef中=后面的相同函數類型語法也被允許出現類型注釋的任何地方,這給了我們一種在程序中任何地方編寫函數類型的統一方法。
舊的typedef語法仍然被支持以避免破壞現有的代碼,但它是不贊成的。
## 優先使用內聯函數類型而不是typedef。
在Dart 1中,如果你想對字段、變量或泛型類型參數使用函數類型,你必須首先為它定義一個typedef。Dart 2支持一種函數類型語法,可以在任何允許類型注釋的地方使用:
~~~
class FilteredObservable {
final bool Function(Event) _predicate;
final List<void Function(Event)> _observers;
FilteredObservable(this._predicate, this._observers);
void Function(Event) notify(Event event) {
if (!_predicate(event)) return null;
void Function(Event) last;
for (var observer in _observers) {
observer(event);
last = observer;
}
return last;
}
}
~~~
如果函數類型特別長或經常使用,那么定義typedef仍然是值得的。但在大多數情況下,用戶希望看到函數類型在使用它的地方是正確的,而函數類型語法使他們更清楚。
## 考慮對參數使用函數類型語法。
當定義類型為函數的參數時,Dart具有特殊的語法。有點像在C,你包圍參數的名字與函數的返回類型和參數簽名:
~~~
Iterable<T> where(bool predicate(T element)) => ...
~~~
在Dart 2添加函數類型語法之前,這是在沒有定義類型定義的情況下為參數提供函數類型的唯一方法。既然Dart對函數類型有了一個通用符號,那么您也可以將其用于函數類型參數:
~~~
Iterable<T> where(bool Function(T) predicate) => ...
~~~
新的語法有點冗長,但與必須使用新語法的其他位置一致。
## 使用Object而不是dynamic進行注釋,以指示允許任何對象。
有些操作適用于任何可能的對象。例如,log()方法可以接受任何對象并在其上調用toString()。Dart中有兩種類型允許所有的值:對象和動態。然而,它們傳達的信息不同。如果您只想聲明您允許所有對象,那么就使用Object,就像在Java或c#中那樣。
使用dynamic發送更復雜的信號。這可能意味著Dart的類型系統不夠復雜,無法表示允許的類型集,或者值來自互操作或靜態類型系統范圍之外,或者您明確希望在程序中的某個地方使用運行時動態。
~~~
void log(Object object) {
print(object.toString());
}
/// Returns a Boolean representation for [arg], which must
/// be a String or bool.
bool convertToBool(dynamic arg) {
if (arg is bool) return arg;
if (arg is String) return arg == 'true';
throw ArgumentError('Cannot convert $arg to a bool.');
}
~~~
## 使用Future作為不產生值的異步成員的返回類型。
當您有一個不返回值的同步函數時,您使用void作為返回類型。異步等效項是Future\<void>。
您可能會看到使用Future或Future\<Null>的代碼,因為Dart的舊版本不允許void作為類型參數。既然這樣,你應該使用它。這樣做更直接地匹配您鍵入類似的同步函數的方式,并為調用者和函數體提供更好的錯誤檢查。
## 避免使用FutureOr\<T>作為返回類型。
如果一個方法接受FutureOr\<int>,那么它接受的內容是寬松的。用戶可以使用int或Future\<int>來調用方法,因此在以后您要解包的int中,他們不需要包裝它。
如果您返回一個FutureOr\<int>,用戶需要在做任何有用的事情之前檢查是否返回一個int或一個Future\<int>。(或者他們只是在等待價值的Future,實際上他們總是把它當作Future。)返回一個Future\<int>,這個很明確。用戶更容易理解一個函數要么總是異步的,要么總是同步的,但是一個函數要么總是異步的,很難正確使用。
~~~
Future<int> triple(FutureOr<int> value) async => (await value) * 3;
~~~
~~~
【bad】
FutureOr<int> triple(FutureOr<int> value) {
if (value is int) return value * 3;
return (value as Future<int>).then((v) => v * 3);
}
~~~
本指南更精確的表述僅適用FutureOr\<T>于 逆變位置。參數是逆變的,返回類型是協變的。在嵌套函數類型中,這會被翻轉 - 如果你有一個類型本身就是函數的參數,那么回調的返回類型現在處于逆變位置,并且回調的參數是協變的。這意味著回調的類型可以返回FutureOr\<T>:
~~~
Stream<S> asyncMap<T, S>(
Iterable<T> iterable, FutureOr<S> Function(T) callback) async* {
for (var element in iterable) {
yield await callback(element);
}
}
~~~