高效 Dart 語言指南:API 設計
命名
要使用一致的術語。
避免縮寫。
推薦把最具描述性的名詞放到最後。
考慮儘量讓程式碼看起來像普通的句子。
推薦使用名詞短語來命名不是布林型別的變數和屬性。
推薦使用非命令式動詞短語命名布林型別的變數和屬性。
考慮省略命名布林引數的動詞。
考慮為布林屬性或變數取“肯定”含義的名字。
推薦使用命令式動詞短語來命名帶有副作用的函式或者方法。
考慮使用名詞短語或者非命令式動詞短語命名返回資料為主要功能的方法或者函式。
考慮使用命令式動詞短語命名一個函式或方法,若果你希望它的執行能被重視。
避免在方法命名中使用 get 開頭。
推薦使用 to___() 來命名把物件的狀態轉換到一個新的物件的函式。
推薦使用 as___() 來命名把原來物件轉換為另外一種表現形式的函式。
避免在方法或者函式名稱中描述引數。
要在命名引數時,遵循現有的助記符約定。
庫
類
建構函式
成員
型別
- DO type annotate variables without initializers
推薦為型別不明顯的公共欄位和公共最上層變數指定型別註解。
避免為初始化的區域變數新增冗餘地型別註解。
- DO annotate return types on function declarations
- DO annotate parameter types on function declarations
避免在函式表示式上註解推斷的引數型別。
- DON’T type annotate initializing formals
- DO write type arguments on generic invocations that aren’t inferred
- DON’T write type arguments on generic invocations that are inferred
- AVOID writing incomplete generic types
推薦使用 dynamic 註解替換推斷失敗的情況。
推薦使 function 型別註解的特徵更明顯
不要為 setter 方法指定返回型別。
不要使用棄用的 typedef 語法。
推薦優先使用行內函數型別,而後是 typedef。
考慮在引數上使用函式型別語法。
避免使用 dynamic 除非你希望禁用靜態檢查
要使用 Future<void> 作為無法回值非同步成員的返回型別。
避免使用 FutureOr<T> 作為返回型別。
引數
相等
下面給出的準則用於指導為庫編寫一致的、可用的 API。
命名
命名是編寫可讀,可維護程式碼的重要部分。以下最佳實踐可幫助你實現這個目標。
要 使用一致的術語。
在你的程式碼中,同樣的東西要使用同樣的名字。如果之前已經存在的 API 之外命名,並且使用者已經熟知,那麼請繼續使用這個命名。
pageCount // A field.
updatePageCount() // Consistent with pageCount.
toSomething() // Consistent with Iterable's toList().
asSomething() // Consistent with List's asMap().
Point // A familiar concept.
renumberPages() // Confusingly different from pageCount.
convertToSomething() // Inconsistent with toX() precedent.
wrappedAsSomething() // Inconsistent with asX() precedent.
Cartesian // Unfamiliar to most users.
總的目的是充分利用使用者已經知道的內容。這裡包括他們所瞭解的問題領域,所熟悉的核心庫,以及你自己 API 那部分。基於以上這些內容,他們在使用之前,不需要學習大量的新知識。
避免 縮寫。
只使用廣為人知的縮寫,對於特有領域的縮寫,請避免使用。如果要使用,請 正確的指定首字母大小寫。
pageCount
buildRectangles
IOStream
HttpRequest
numPages // "Num" is an abbreviation of "number (of)".
buildRects
InputOutputStream
HypertextTransferProtocolRequest
推薦 把最具描述性的名詞放到最後。
最後一個詞應該是最具描述性的東西。你可以在其前面新增其他單詞,例如形容詞,以進一步描述該事物。
pageCount // A count (of pages).
ConversionSink // A sink for doing conversions.
ChunkedConversionSink // A ConversionSink that's chunked.
CssFontFaceRule // A rule for font faces in CSS.
numPages // Not a collection of pages.
CanvasRenderingContext2D // Not a "2D".
RuleFontFaceCss // Not a CSS.
考慮 儘量讓程式碼看起來像普通的句子。
當你不知道如何命名 API 的時候,使用你的 API 編寫些程式碼,試著讓程式碼看起來像普通的句子。
// "If errors is empty..."
if (errors.isEmpty) ...
// "Hey, subscription, cancel!"
subscription.cancel();
// "Get the monsters where the monster has claws."
monsters.where((monster) => monster.hasClaws);
// Telling errors to empty itself, or asking if it is?
if (errors.empty) ...
// Toggle what? To what?
subscription.toggle();
// Filter the monsters with claws *out* or include *only* those?
monsters.filter((monster) => monster.hasClaws);
嘗試著使用你自己的 API,並且閱讀寫出來的程式碼,可以幫助你為 API 命名,但是不要過於冗餘。新增文章和其他詞性以強制名字讀起來就像語法正確的句子一樣,是沒用的。
if (theCollectionOfErrors.isEmpty) ...
monsters.producesANewSequenceWhereEach((monster) => monster.hasClaws);
推薦 使用名詞短語來命名不是布林型別的變數和屬性。
讀者關注屬性是什麼。如果使用者更關心如何確定一個屬性,則很可能應該是一個使用動詞短語命名函式。
list.length
context.lineWidth
quest.rampagingSwampBeast
list.deleteItems
推薦 使用非命令式動詞短語命名布林型別的變數和屬性。
布林名稱通常用在控制陳述式中當做條件,因此你要應該讓這個名字在控制陳述式中讀起來語感很好。比較下面的兩個:
if (window.closeable) ... // Adjective.
if (window.canClose) ... // Verb.
好的名字往往以某一種動詞作為開頭:
-
“to be” 形式:
isEnabled
,wasShown
,willFire
。就目前來看,這些時做常見的。 -
一個 輔助動詞:
hasElements
,canClose
,shouldConsume
,mustSave
。 -
一個主動動詞:
ignoresInput
,wroteFile
。因為經常引起歧義,所以這種形式比較少見。loggedResult
是一個不好的命名,因為它的意思可能是: “whether or not a result was logged” 或者 “the result that was logged”。closingConnection
的意思可能是: “whether the connection is closing” 或者 “the connection that is closing”。 只有 當名字可以預期的時候才使用主動動詞。
可以使用命令式動詞來區分佈爾變數名字和函式名字。一個布林變數的名字不應該看起來像一個命令,告訴這個物件做什麼事情。原因在於存取一個變數的屬性並沒有修改物件的狀態。(如果這個屬性確實修改了物件的狀態,則它應該是一個函式。)
isEmpty
hasElements
canClose
closesWindow
canShowPopup
hasShownPopup
empty // Adjective or verb?
withElements // Sounds like it might hold elements.
closeable // Sounds like an interface.
// "canClose" reads better as a sentence.
closingWindow // Returns a bool or a window?
showPopup // Sounds like it shows the popup.
考慮 省略命名布林引數的動詞。
提煉於上一條規則。對於命名布林引數,沒有動詞的名稱通常看起來更加舒服。
Isolate.spawn(entryPoint, message, paused: false);
var copy = List.from(elements, growable: true);
var regExp = RegExp(pattern, caseSensitive: false);
考慮 為布林屬性或變數取“肯定”含義的名字。
大多數布林值名稱具有概念形式上的“肯定”和“否定”,前者感覺更像是基本描述,後者是對基本描述的否定,例如: “open” 和 “closed”, “enabled” 和 “disabled”,等等。通常後者的名稱字面上有個字首,用來否定前者: “visible” 和 “in-visible”, “connected” 和 “dis-connected”, “zero” 和 “non-zero”。
當選擇 true
代表兩種情況中的其中一種情況在布林的兩種情況中,當選擇 true
代表其中一種情況,或使用這種情況作為屬性名稱時,更傾向使用“肯定”或基本描述的方式。布林成員通常巢狀(Nesting)在邏輯表示式中,包括否定運算子。如果屬性本身讀起來想是個“否定”的,這將讓讀者耗費更多精力去閱讀雙重否定及理解程式碼的含義。
if (socket.isConnected && database.hasData) {
socket.write(database.read());
}
if (!socket.isDisconnected && !database.isEmpty) {
socket.write(database.read());
}
對於一些屬性,沒有明顯的“肯定”形式。文件已經重新整理 “saved” 到磁碟,或者 “un-changed”?文件還未屬性 “un-saved” 到磁碟,或者 “changed”?在模稜兩可的情況下,傾向於選擇不太可能被使用者否定或較短的名字。
例外: “否定”使用者絕大多數用到的形式。選擇「肯定」方式,將會迫使在他們到處使用 !
對屬性進行取反操作。這樣相反,屬性應該使用「否定」形式進行命名。
推薦 使用命令式動詞短語來命名帶有副作用的函式或者方法。
函式通常返回一個結果給呼叫者,並且執行一些任務或者帶有副作用。在像 Dart 這種命令式語言中,呼叫函式通常為了實現其副作用:可能改變了物件的內部狀態、產生一些輸出內容、或者和外部世界溝通等。
這種型別的成員應該使用命令式動詞短語來命名,強調該成員所執行的任務。
list.add('element');
queue.removeFirst();
window.refresh();
這樣,呼叫的方法讀起來會讓人覺得是一個執行命令。
考慮 使用名詞短語或者非命令式動詞短語命名返回資料為主要功能的方法或者函式。
雖然這些函式可能也有副作用,但是其主要目的是返回一個數據給呼叫者。如果該函式無需引數通常應該是一個 getter 。有時候獲取一個屬性則需要一些引數,比如,
elementAt()
從集合中返回一個數據,但是需要一個指定返回那個資料的引數。
在語法上看這是一個函式,其實嚴格來說其返回的是集合中的一個屬性,應該使用一個能夠表示該函式返回的是什麼的詞語來命名。
var element = list.elementAt(3);
var first = list.firstWhere(test);
var char = string.codeUnitAt(4);
這條規則比前一條要寬鬆一些。有時候一些函式沒有副作用,但仍然使用一個動詞短語來命名,例如:
list.take()
或者 string.split()
。
考慮 使用命令式動詞短語命名一個函式或方法,若果你希望它的執行能被重視。
當一個成員產生的結果沒有額外的影響,它通常應該使用一個 getter 或者一個名詞短語描述來命名,用於描述它返回的結果。但是,有時候執行產生的結果很重要。它可能容易導致執行時故障,或者使用重量級的資源(例如,網路或檔案 I/O)。在這種情況下,你希望呼叫者考慮成員在進行的工作,這時,為成員提供描述該工作的動詞短語。
var table = database.downloadData();
var packageVersions = packageGraph.solveConstraints();
但請注意,此準則比前兩個更寬鬆。操作執行工作的實現細節通常與呼叫這無關,並且效能和健壯性是隨時間經常改變的。大多數情況下,根據成員為呼叫者做了“什麼”來命名,而不是“如何”做。
get
開頭。
避免 在方法命名中使用 在大多數情況下,getter 方法名稱中應該移除 get
。例如,定義一個名為 breakfastOrder
的 getter 方法,來替代名為 getBreakfastOrder()
的方法。
即使成員因為需要傳入引數或者 getter 不適用,而需要透過方法來實現,也應該避免使用 get
開頭。與之前的準則一樣:
-
如果呼叫者主要關心的是方法的返回值,只需刪除
get
並使用 名詞短語 命名,如breakfastOrder()
。 -
如果呼叫者關心的是正在完成的工作,請使用 動名詞短語 命名,這種情況下應該選擇一個更能準確描述工作的動名詞,而不是使用
get
命名,如create
,download
,fetch
,calculate
,request
,aggregate
,等等。
to___()
來命名把物件的狀態轉換到一個新的物件的函式。
推薦 使用 Linter rule: use_to_and_as_if_applicable
一個轉換函式返回一個新的物件,裡面包含一些原物件的狀態,但通常新物件的形式或表現方式與原物件不同。核心函式庫有一個約定,這些型別結果的方法名應該以 to
作為開頭。
如果要定義一個轉換函式,遵循該約定是非常有益的。
list.toSet();
stackTrace.toString();
dateTime.toLocal();
as___()
來命名把原來物件轉換為另外一種表現形式的函式。
推薦 使用 Linter rule: use_to_and_as_if_applicable
轉換函式提供的是“快照功能”。返回的物件有自己的資料副本,修改原來物件的資料不會改變返回的物件中的資料。另外一種函式返回的是同一份資料的另外一種表現形式,返回的是一個新的物件,但是其內部參考的資料和原來物件參考的資料一樣。修改原來物件中的資料,新返回的物件中的資料也一起被修改。
這種函式在核心庫中被命名為 as___()
。
var map = table.asMap();
var list = bytes.asFloat32List();
var future = subscription.asFuture();
避免 在方法或者函式名稱中描述引數。
在呼叫程式碼的時候可以看到引數,所以無需再次顯示引數了。
list.add(element);
map.remove(key);
list.addElement(element)
map.removeKey(key)
但是,對於具有多個類似的函式的時候,使用引數名字可以消除歧義,這個時候應該帶有引數名字:
map.containsKey(key);
map.containsValue(value);
要 在命名引數時,遵循現有的助記符約定。
單字母命名沒有直接的啟發性,但是幾乎所有通用型別都使用時情況就不一樣了。幸運的是,它們大多數以一致的助記方式在使用,這些約定如下:
-
E
用於集合中的 元素 型別:class IterableBase<E> {} class List<E> {} class HashSet<E> {} class RedBlackTree<E> {}
-
K
和V
分別用於關聯集合中的 key 和 value 型別:class Map<K, V> {} class Multimap<K, V> {} class MapEntry<K, V> {}
-
R
用於函式或類方法的 返回值 型別。這種情況並不常見,但有時會出現在typedef中,或實現存取者模式的類中:abstract class ExpressionVisitor<R> { R visitBinary(BinaryExpression node); R visitLiteral(LiteralExpression node); R visitUnary(UnaryExpression node); }
-
除此以外,對於具有單個型別引數的泛型,如果助記符能在周圍型別中明顯表達泛型含義,請使用
T
,S
和U
。這裡允許多個字母巢狀(Nesting)且不會與周圍命名產生歧義。例如:class Future<T> { Future<S> then<S>(FutureOr<S> onValue(T value)) => ... }
這裡,通常
then<S>()
方法使用S
避免Future<T>
中的T
產生歧義。
如果上述情況都不合適,則可以使用另一個單字母助記符名稱或描述性的名稱:
class Graph<N, E> {
final List<N> nodes = [];
final List<E> edges = [];
}
class Graph<Node, Edge> {
final List<Node> nodes = [];
final List<Edge> edges = [];
}
在實踐中,以上的約定涵蓋了大多數引數型別。
庫
以 ( _
) 開頭的成員只能在其庫的內部被存取,是庫的私有成員。這是 Dart 語言的內建特性,不僅僅是慣例。
推薦 使用私有宣告。
庫中的公開宣告—最上層定義或者在類中定義—是一種訊號,表示其他庫可以並應該存取這些成員。同時公開宣告也是一種你的函式庫需要實現的契約,當使用這些成員的時候,應該實現其宣稱的功能。
如果某個成員你不希望公開,則在成員名字之前新增一個 _
即可。減少公開的介面讓你的函式庫更容易維護,也讓使用者更加容易掌握你的函式庫如何使用。另外,分析工具還可以分析出沒有用到的私有成員定義,然後告訴你可以刪除這些無用的程式碼。私有成員第三方程式碼無法呼叫而你自己在庫中也沒有使用,所以是無用的程式碼。
考慮 宣告多個類在一個函式庫中。
一些其他語言,比如 Java。將檔案結構和類結構進行捆綁&mdash:每個檔案僅能定義一個最上層類別。 Dart 沒有這樣的限制。庫與類是相互獨立的。如果多個類,最上層變數,以及函式,他們再邏輯上歸為同一類,那麼將他們包含到單一的函式庫中,這樣做是非常棒的。
將多個類組織到一個函式庫中,就可以使用一些有用的模式。因為在 Dart 中私有特性是在庫級別上有效,而不是在類級別,基於這個模式你可以定義類似於 C++ 中的 “friend” 類別。所有定義在同一個庫中的類可以互相存取彼此的私有成員,但庫以外的程式碼無法發存取。
當然,指南並不建議你 應該 把所有的類都放在單個巨大的函式庫中,你可以同時在一個函式庫中放置多個類別。
類
Dart是一種 “純粹的” 物件導向語言,因為所有物件都是類別的例項。但是 Dart 並沒有要求所有程式碼都定義到類中— 類似在面向過程或函式的語言,你可以在 Dart 中定義最上層變數,常量,以及函式。
避免 避免為了使用一個簡單的函式而去定義一個單一成員的抽象類別
Linter rule: one_member_abstracts
和 Java 不同,Dart 擁有一等公民的函式,閉套件,以及它們簡潔的使用語法。如果你僅僅是需要一個類似於回呼(Callback)的功能,那麼使用函式即可。例如如果你正在定義一個類別,並且它僅擁有一個毫無意義名稱的抽象成員,如 call
或 invoke
,那麼這時你很可能只是需要一個函式。
typedef Predicate<E> = bool Function(E element);
abstract class Predicate<E> {
bool test(E element);
}
避免 定義僅包含靜態成員的類別。
Linter rule: avoid_classes_with_only_static_members
在 Java 和 C# 中,所有的定義必須要在類中。所有常常會看到一些這樣的類,這些類中僅僅放置了些靜態成員。其他類僅用於名稱空間—一種為一堆成員提供共享字首將它們相互關聯或避免名稱衝突的方法。
Dart 有最上層函式、變數和常量,因此你 不需要 僅僅為了定義一些內容而建立一個類別。如果你想要的是一個名稱空間,那麼一個函式庫是更合適的。庫支援匯入時指定字首,以及僅匯入其一部分。這些強大的功能讓呼叫的程式碼可以以最適合的方式處理 它們的 名稱衝突。
如果函式或變數在邏輯上與類無關,那麼應該將其置於最上層。如果擔心名稱衝突,那麼請為其指定更精確的名稱,或將其移動到可以使用字首匯入的單獨庫中。
DateTime mostRecent(List<DateTime> dates) {
return dates.reduce((a, b) => a.isAfter(b) ? a : b);
}
const _favoriteMammal = 'weasel';
class DateUtils {
static DateTime mostRecent(List<DateTime> dates) {
return dates.reduce((a, b) => a.isAfter(b) ? a : b);
}
}
class _Favorites {
static const mammal = 'weasel';
}
通常在 Dart 中,類定義了一類物件。一個類別型,如果型別從來沒有被初始化,那麼這是另一種的程式碼氣息。
當然,這並不是一條硬性規則。對於常量和類似列舉的型別,將它們組合在一個類別中看起來也是很自然。
class Color {
static const red = '#f00';
static const green = '#0f0';
static const blue = '#00f';
static const black = '#000';
static const white = '#fff';
}
避免 整合一個不期望被整合的類別。
如果一個類別的建構函式從產生建構函式被更改為工廠建構函式,則呼叫該建構函式的任何子類別建構函式都將失敗。此外,如果一個類別改變了它在 this
上呼叫的自己的方法,那麼覆蓋這些方法並期望他們在某些點被呼叫的子類別再呼叫時會失敗。
以上兩種情況都意味著一個類別需要考慮是否要允許被子類別化。這種情況可以透過文件註釋來溝通,或者為類提供一個顯示命名,如 IterableBase
。如果該類別的作者不這樣做,最好假設你不能夠繼承這個類別。否則,後續對它的修改可能會破壞你的程式碼。
要 把能夠繼承的說明新增到文件中,如果這個類可以繼承。
該規則是上條規則的結果。如果允許你的類被子類別化,請在文件中說明情況。使用 Base
作為類別名稱的字尾,或者在類別的註釋文件中註明。
避免 去實現一個不期望成為介面的類(該類不想作為介面被實現)。
隱含介面是Dart中的一個強大工具,當一個類別中可以很容易的推斷出一些已經約定的有特徵的實現時,隱含介面可以避免重複定義這個類別的約定。
但是透過類別的隱含介面實現的新類,新類會與這個類產生非常緊密的耦合。也就是說,對於介面類別的 任何修改,你實現的新類都會被破壞。例如,向類中新增新成員通常是安全,不會產生破壞性的改變。但是如果你實現了這個類別的介面,那麼現在你的類會產生一個靜態錯誤,因為它缺少了新方法的實現。
庫的維護人員需要能夠在不破壞使用者程式碼的情況下迭代現有的累。如果把每個類都看待成是暴露給使用者的介面,使用者可以自由的實現,這時修改這些類將變得非常困難。反過來,這個困難將導致你的函式庫迭代緩慢,從而無法適應新的需求。
為了給你的類別的開發人員提供更多的餘地,避免實現隱含介面,除非那些類明確需要實現。否則,你可能會引入開發者沒有預料到的耦合情況,這樣可能會在沒有意識到的情況下破壞你的程式碼。
要 對支援介面的類在文件註明
如果你的類可以被用作介面,那麼將這個情況註明到類別的文件中。
要 對支援 mixin 的類在文件註明
Linter rule: prefer_mixin
Dart originally didn’t have a separate syntax for declaring a class intended to be mixed in to other classes. Instead, any class that met certain restrictions (no non-default constructor, no superclass, etc.) could be used as a mixin. This was confusing because the author of the class might not have intended it to be mixed in.
Dart 2.1.0 added a mixin
keyword for explicitly declaring a mixin. Types
created using that can only be used as mixins, and the language also ensures
that your mixin stays within the restrictions. When defining a new type that you
intend to be used as a mixin, use this syntax.
mixin ClickableMixin implements Control {
bool _isDown = false;
void click();
void mouseDown() {
_isDown = true;
}
void mouseUp() {
if (_isDown) click();
_isDown = false;
}
}
You might still encounter older code using class
to define mixins, but the new
syntax is preferred.
避免 去 mixin 一個不期望被 mixin 的類
Linter rule: prefer_mixin
For compatibility, Dart still allows you to mix in classes that aren’t defined
using mixin
. However, that’s risky. If the author of the class doesn’t intend
the class to be used as a mixin, they might change the class in a way that
breaks the mixin restrictions. For example, if they add a constructor, your
class will break.
If the class doesn’t have a doc comment or an obvious name like IterableMixin
,
assume you cannot mix in the class if it isn’t declared using mixin
.
建構函式
透過宣告與類具有相同名稱的函式以及附加可選的識別符號來建立 Dart 建構函式。後者附加標示符的建構函式被稱為命名建構函式。
const
。
考慮 在類支援的情況下,指定建構函式為 如果一個類別,它所有的欄位都是 final ,並且構造函數出了初始化他們之外沒有任何其他操作,那麼可以將其作為 const
建構函式。這樣就能夠允許使用者在需要常量的位置建立類別的例項—一些大型的常量,switch case 陳述式,預設引數中,以及其他的情況。
如果沒有顯示的指定為 const
建構函式,那麼就無法實現上述目的。
但需要注意的是,建構函式被指定為 const
,那它就是公共 API 的一中承諾。如果後面將建構函式更改為非 const
,那麼在常量表達式中呼叫它的程式碼就會被破壞。如果不想做出這樣的承諾,那麼就不要指定它為 const
建構函式。在實際運用中,
const
建構函式對於簡單的,不可變的資料記錄類是非常有用的。
成員
成員屬於物件,成員可以是方法或例項變數。
final
。
推薦 指定欄位或最上層變數為 Linter rule: prefer_final_fields
狀態 不可變—隨著時間推移狀態不發生變化—有益於程式設計師推理。類和庫中可變狀態量越少,類和庫越容易維護。
當然,可變資料是非常有用的。但是,如果並不需要可變資料,應該儘可能預設指定欄位和最上層變數為 final
。
有時例項的某些欄位在被初始化後不會再變化,但只能在例項被構造後才能被初始化。例如,某些欄位可能需要參考 this
。在這種情況下,請考慮將其宣告為 late final
形式。當這樣聲明後,您也許可以 在宣告時完成初始化。
要 對概念上是存取的屬性使用 getter 方法。
判定一個成員應該是一個 getter 而不是一個方法是一件具有挑戰性的事情。它雖然微妙,但對於好的
API 設計是非常重要的,也導致本規則會很長。其他的一些語言文化中迴避了getter。他們只有在幾乎類似於欄位存取的時候才會使用—它僅僅是根據物件的狀態進行微小的計算。任何比這更復雜或重量級的東西得到帶有 ()
的名字後面,給出一種”計算的操作在這!”訊號。因為 .
後面只跟名稱意味著是”欄位”。
Dart 與他們 不 同。在 Dart 中,所有點名稱都可以是進行計算的成員呼叫。欄位是特殊的— 欄位的 getter 的實現是有語言提供的。換句話說,在 Dart 中,getter 不是”存取特別慢的欄位”;欄位是”存取特別快的 getter “。
即便如此,選擇 getter 而不是方法對於呼叫者來說是一個重要訊號。訊號大致的意思成員的操作 “類似於欄位”。至少原則上可以這麼認為,只要呼叫者清楚,這個操作可以使用欄位來實現。這意味著:
-
操作返回一個結果但不接受任何引數。
-
呼叫者主要關係結果。 如果希望呼叫者關係操作產生結果的方式多於產生的結果,那麼為操作提供一個方法,使用描述工作的動詞作為方法的名稱。
這並不意味著操作必須特別快才能成為 getter 方法。
IterableBase.length
複雜度是O(n)
,是可以的。使用 getter 方法進行重要計算是沒問題的。但是如果它做了 超 大量的工作,你可能需要透過一個描述其功能的動詞的方法來引起使用者的注意。connection.nextIncomingMessage; // Does network I/O. expression.normalForm; // Could be exponential to calculate.
-
操作不會產生使用者可見的副作用。 在程式中存取一個實際的欄位不會改變物件或者其他的狀態。操作不會產生輸出,寫入檔案等。同樣 getter 方法也一樣。
注意關鍵字”使用者可見”。只要呼叫者不關心這些副作用。getter 方法可以修改隱藏狀態或產生帶外副作用。 getter 方法可以惰性計算和儲存他們的結果,寫入快取, log 等。這樣是沒有問題的。
stdout.newline; // Produces output. list.clear; // Modifies object.
-
注意關鍵字”使用者可見”。只要呼叫者不關心這些副作用。getter 方法可以修改隱藏狀態或產生帶外副作用。 getter 方法可以惰性計算和儲存他們的結果,寫入快取, log 等。這樣是沒有問題的。
這裡”相同的結果”並不意味著 getter 方法必須一定要在每次呼叫成功後都返回相同的物件。如果按這樣的要求會迫使很過 getter 方法需要進行脆弱的快取 (brittle caching) ,這樣就否定了使用 getter 的全部意義。常見的非常好的範例是,每次呼叫一個 getter 方法返回一個新的 future 或 list。重點在於, future 完成後返回相同的值,list 包含了相同的元素。
換句話說,呼叫者關係的是結果值應該相等。
DateTime.now; // New result each time.
-
結果物件不用公開所有原始物件的狀態。 一個欄位僅公開物件的一部分。如果操作返回的結果公開了原始物件的整個狀態,那麼把該操作作為
to___()
或as___()
方法可能會更好。
如果操作符合上述描述,那麼它應該是一個 getter 方法。看似滿足這一系列要求的成員並不多,但實際上會超出你的想象。許多操作只是對某些狀態進行一些計算,其中大多數能夠,並且也應該作為 getter 方法。
rectangle.area;
collection.isEmpty;
button.canShow;
dataSet.minimumValue;
要 對概念上是修改的屬性使用 setter 方法。
Linter rule: use_setters_to_change_properties
判定一個成員應該是一個 setter 而不是一個方法與 getter 的判定一樣。兩者的操作都應該是 “類似於欄位”的操作。
對於 setter 方法,”類似於欄位”意味著:
-
操作只有一個引數,不會返回結果。
-
操作會更改物件中的某些狀態。
-
操作是冪等的。 使用相同的值呼叫相同的 setter 兩次,就呼叫者而言,第二次不應該執行任何操作。在內部,也許你會得到一些無效的快取或者多次的日誌記錄。沒關係,從呼叫者的角度來看,第二次呼叫似乎沒做任何事情。
rectangle.width = 3;
button.visible = false;
不要 在沒有對應的 getter 的情況下定義 setter。
Linter rule: avoid_setters_without_getters
使用者將 getter 和 setter 視為一個物件的可見屬性。一個 “dropbox” 屬性可以被寫入但無法讀取,會令人感到困惑。並且也混淆了他們對屬性如何工作的直觀理解。例如,沒有 getter 的 setter
意味著你可以使用 =
來修改它,但卻不能使用 +=
。
本規則意義並 不是 說,你需要先新增一個 getter 才被允許新增 setter ,物件通常不應該暴露出多餘的狀態。如果某個物件的某個狀態可以修改但不能以相同的方式存取,請改用方法實現。
AVOID using runtime type tests to fake overloading
It’s common for an API to support similar operations on different types of parameters. To emphasize the similarity, some languages support overloading, which lets you define multiple methods that have the same name but different parameter lists. At compile time, the compiler looks at the actual argument types to determine which method to call.
Dart doesn’t have overloading.
You can define an API that looks like overloading
by defining a single method and then using is
type tests
inside the body to look at the runtime types of the arguments and perform the
appropriate behavior.
However, faking overloading this way turns a compile time method selection
into a choice that happens at runtime.
If callers usually know which type they have and which specific operation they want, it’s better to define separate methods with different names to let callers select the right operation. This gives better static type checking and faster performance since it avoids any runtime type tests.
However, if users might have an object of an unknown type
and want the API to internally use is
to pick the right operation,
then a single method where the parameter is a supertype
of all of the supported types might be reasonable.
late final
fields without initializers
AVOID public Unlike other final
fields, a late final
field without an initializer does
define a setter. If that field is public, then the setter is public. This is
rarely what you want. Fields are usually marked late
so that they can be
initialized internally at some point in the instance’s lifetime, often inside
the constructor body.
Unless you do want users to call the setter, it’s better to pick one of the following solutions:
- Don’t use
late
. - Use a factory constructor to compute the
final
field values. - Use
late
, but initialize thelate
field at its declaration. - Use
late
, but make thelate
field private and define a public getter for it.
Future
, Stream
, and collection types
AVOID returning nullable When an API returns a container type, it has two ways to indicate the absence of
data: It can return an empty container or it can return null
. Users generally
assume and prefer that you use an empty container to indicate “no data”. That
way, they have a real object that they can call methods on like isEmpty
.
To indicate that your API has no data to provide, prefer returning an empty collection, a non-nullable future of a nullable type, or a stream that doesn’t emit any values.
如果確實有成員可能返回 null
的型別,請在文件中註明,以及在什麼情況下回返回 null
。
this
。
避免 為了書寫流暢,而從方法中返回 Linter rule: avoid_returning_this
方法級聯是連結方法呼叫的更好的解決方式。
var buffer = StringBuffer()
..write('one')
..write('two')
..write('three');
var buffer = StringBuffer()
.write('one')
.write('two')
.write('three');
型別
程式中的型別用於約束流入程式碼各位置的 值 的不同型別。型別會出現在兩種位置:宣告中的 **型別註解 (type annotations) ** 和 **泛型呼叫 (generic invocations) ** 的型別引數。
當你想到 靜態型別 時,通常會聯想到型別註解。型別註解可以用於為變數,引數,欄位,或者返回值宣告型別。在下面的範例中,bool
和 String
是型別註解。他們位於程式碼靜態宣告結構的前面,並且他們不會在執行時”執行”。
bool isEmpty(String parameter) {
bool result = parameter.isEmpty;
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
是一個類別型引數,但它出現在了型別註解中,而不是泛型呼叫。通常來說不需要擔心這種情況,但在幾個地方,對於型別的運用是泛型呼叫而不是型別註解有不同的指導。
Type inference
Type annotations are optional in Dart.
If you omit one, Dart tries to infer a type
based on the nearby context. Sometimes it doesn’t have enough information to
infer a complete type. When that happens, Dart sometimes reports an error, but
usually silently fills in any missing parts with dynamic
. The implicit
dynamic
leads to code that looks inferred and safe, but actually disables
type checking completely. The rules below avoid that by requiring types when
inference fails.
在大多數地方,Dart 允許省略型別註解並根據附近的上下文提供推斷型別,或預設指定為 dynamic
型別。Dart 同時具有型別推斷和 dynamic
型別的情況,導致對程式碼中 “untyped” 的含義產生一些混淆。意思就是不寫型別就是動態型別嗎?為避免這種混淆,應該避免說 “untyped” ,而是使用以下術語:
-
如果程式碼是型別註解,則在程式碼中顯式寫入型別。
-
如果程式碼的型別是推斷的,則不必寫型別註解,Dart 會自己會找出它的型別。規則不考慮推斷可能會失敗的情況,在一些地方,推理失敗會產生一個靜態錯誤。在其他情況下,Dart 使用
dynamic
作為備選型別。 -
如果程式碼是動態型別,那麼它的靜態型別就是特殊的
dynamic
型別。程式碼可以明確地註解為dynamic
型別,也可以由 Dart 進行推斷。
換句話說,對於程式碼的型別是 dynamic
型別還是其他型別,在型別註解或型別推斷中是正交的。
Inference is a powerful tool to spare you the effort of writing and reading types that are obvious or uninteresting. It keeps the reader’s attention focused on the behavior of the code itself. Explicit types are also a key part of robust, maintainable code. They define the static shape of an API and create boundaries to document and enforce what kinds of values are allowed to reach different parts of the program.
Of course, inference isn’t magic. Sometimes inference succeeds and selects a type, but it’s not the type you want. The common case is inferring an overly precise type from a variable’s initializer when you intend to assign values of other types to the variable later. In those cases, you have to write the type explicitly.
The guidelines here strike the best balance we’ve found between brevity and control, flexibility and safety. There are specific guidelines to cover all the various cases, but the rough summary is:
-
Do annotate when inference doesn’t have enough context, even when
dynamic
is the type you want. -
Don’t annotate locals and generic invocations unless you need to.
-
Prefer annotating top-level variables and fields unless the initializer makes the type obvious.
DO type annotate variables without initializers
Linter rule: prefer_typing_uninitialized_variables
The type of a variable—top-level, local, static field, or instance field—can often be inferred from its initializer. However, if there is no initializer, inference fails.
List<AstNode> parameters;
if (node is Constructor) {
parameters = node.signature;
} else if (node is Method) {
parameters = node.parameters;
}
var parameters;
if (node is Constructor) {
parameters = node.signature;
} else if (node is Method) {
parameters = node.parameters;
}
其餘指南涵蓋了和型別有關的其他具體問題。
推薦 為型別不明顯的公共欄位和公共最上層變數指定型別註解。
Linter rule: type_annotate_public_apis
型別註解是關於如何使用庫的重要文件。它們在程式的區域之間形成邊界以隔離型別錯誤來源。思考下面程式碼:
install(id, destination) => ...
在這裡,無法判斷:這個 id
是什麼,一個字串?destination
又是什麼,一個字串還是一個
File
物件?方法是同步的還是非同步的?下面的例項會清晰很多:
Future<bool> install(PackageId id, String destination) => ...
但在一些情況下,型別非常明顯,根本沒有指明型別的必要:
const screenWidth = 640; // Inferred as int.
這裡的”明顯”並沒有精確的定義,下面這些可以作為很好的參考:
-
字面量。
-
建構函式呼叫。
-
參考的其他型別明確的常量。
-
數字和字串的簡單表示式。
-
讀者熟悉的工廠方法,如
int.parse()
,Future.wait()
等。
如果你認為初始化表示式—無論是什麼表示式—足夠清晰,那麼可以省略它的註解。但是如果你認為註解有助於使程式碼更清晰,那麼你應該加上這個註解。
如有疑問,請新增型別註解。即使型別很明顯,但可能任然希望明確的註解。如果推斷型別依賴於其他庫中的值或宣告,可能需要添加註解的宣告。這樣自己的API就不會因為其他庫的修改而被悄無聲息的改變了型別。
這條規則同時適用於公有和私有宣告。就像 API 裡的型別註釋可以更好幫助程式碼的 使用者,私有成員上的型別可以幫助 維護者。
避免 為初始化的區域變數新增冗餘地型別註解。
Linter rule: omit_local_variable_types
區域變數,特別是現代的函式往往很少,範圍也很小。省略區域變數型別會將讀者的注意力集中在變數的 名稱 及初始化值上。
List<List<Ingredient>> possibleDesserts(Set<Ingredient> pantry) {
var desserts = <List<Ingredient>>[];
for (final recipe in cookbook) {
if (pantry.containsAll(recipe)) {
desserts.add(recipe);
}
}
return desserts;
}
List<List<Ingredient>> possibleDesserts(Set<Ingredient> pantry) {
List<List<Ingredient>> desserts = <List<Ingredient>>[];
for (final List<Ingredient> recipe in cookbook) {
if (pantry.containsAll(recipe)) {
desserts.add(recipe);
}
}
return desserts;
}
Sometimes the inferred type is not the type you want the variable to have. For example, you may intend to assign values of other types later. In that case, annotate the variable with the type you want.
Widget build(BuildContext context) {
Widget result = Text('You won!');
if (applyPadding) {
result = Padding(padding: EdgeInsets.all(8.0), child: result);
}
return result;
}
DO annotate return types on function declarations
如果區域變數沒有初始值設定項,那麼就無法判斷它的型別了。這種情況下,最好是為變數加上型別註解。否則,你的到的會是一個 dynamic
型別,並失去靜態型別的好處。
String makeGreeting(String who) {
return 'Hello, $who!';
}
makeGreeting(String who) {
return 'Hello, $who!';
}
Note that this guideline only applies to named function declarations: top-level functions, methods, and local functions. Anonymous function expressions infer a return type from their body. In fact, the syntax doesn’t even allow a return type annotation.
DO annotate parameter types on function declarations
A function’s parameter list determines its boundary to the outside world. Annotating parameter types makes that boundary well defined. Note that even though default parameter values look like variable initializers, Dart doesn’t infer an optional parameter’s type from its default value.
void sayRepeatedly(String message, {int count = 2}) {
for (var i = 0; i < count; i++) {
print(message);
}
}
void sayRepeatedly(message, {count = 2}) {
for (var i = 0; i < count; i++) {
print(message);
}
}
Exception: Function expressions and initializing formals have different type annotation conventions, as described in the next two guidelines.
避免 在函式表示式上註解推斷的引數型別。
Linter rule: avoid_types_on_closure_parameters
匿名函式幾乎都是作為一個回呼(Callback)引數型別立即傳遞給一個方法。當在型別化上下文中建立函式表示式時,Dart 會嘗試根據預期型別來推斷函式的引數型別。
例如,當為 Iterable.map()
傳遞一個函式表示式時,函式的引數型別會根據 map()
回呼(Callback)中所期望的型別進行推斷。
var names = people.map((person) => person.name);
var names = people.map((Person person) => person.name);
If the language is able to infer the type you want for a parameter in a function expression, then don’t annotate. In rare cases, the surrounding context isn’t precise enough to provide a type for one or more of the function’s parameters. In those cases, you may need to annotate. (If the function isn’t used immediately, it’s usually better to make it a named declaration.)
DON’T type annotate initializing formals
Linter rule: type_init_formals
If a constructor parameter is using this.
to initialize a field,
or super.
to forward a super parameter,
then the type of the parameter
is inferred to have the same type as
the field or super-constructor parameter respectively.
class Point {
double x, y;
Point(this.x, this.y);
}
class MyWidget extends StatelessWidget {
MyWidget({super.key});
}
class Point {
double x, y;
Point(double this.x, double this.y);
}
class MyWidget extends StatelessWidget {
MyWidget({Key? super.key});
}
DO write type arguments on generic invocations that aren’t inferred
在其他情況下,如果沒有足夠的資訊來推斷型別時,應該為引數新增型別註解:
var playerScores = <String, int>{};
final events = StreamController<Event>();
var playerScores = {};
final events = StreamController();
Sometimes the invocation occurs as the initializer to a variable declaration. If the variable is not local, then instead of writing the type argument list on the invocation itself, you may put a type annotation on the declaration:
class Downloader {
final Completer<String> response = Completer();
}
class Downloader {
final response = Completer();
}
在這裡,由於變數沒有型別註解,因此沒有足夠的上下文來確定建立的 Set
是什麼型別,因此應該顯式的提供引數型別。
DON’T write type arguments on generic invocations that are inferred
This is the converse of the previous rule. If an invocation’s type argument list is correctly inferred with the types you want, then omit the types and let Dart do the work for you.
class Downloader {
final Completer<String> response = Completer();
}
class Downloader {
final Completer<String> response = Completer<String>();
}
Here, the type annotation on the field provides a surrounding context to infer the type argument of constructor call in the initializer.
var items = Future.value([1, 2, 3]);
var items = Future<List<int>>.value(<int>[1, 2, 3]);
Here, the types of the collection and instance can be inferred bottom-up from their elements and arguments.
AVOID writing incomplete generic types
The goal of writing a type annotation or type argument is to pin down a complete type. However, if you write the name of a generic type but omit its type arguments, you haven’t fully specified the type. In Java, these are called “raw types”. For example:
List numbers = [1, 2, 3];
var completer = Completer<Map>();
Here, numbers
has a type annotation, but the annotation doesn’t provide a type
argument to the generic List
. Likewise, the Map
type argument to Completer
isn’t fully specified. In cases like this, Dart will not try to “fill in” the
rest of the type for you using the surrounding context. Instead, it silently
fills in any missing type arguments with dynamic
(or the bound if the
class has one). That’s rarely what you want.
Instead, if you’re writing a generic type either in a type annotation or as a type argument inside some invocation, make sure to write a complete type:
List<num> numbers = [1, 2, 3];
var completer = Completer<Map<String, int>>();
dynamic
註解替換推斷失敗的情況。
推薦 使用 When inference doesn’t fill in a type, it usually defaults to dynamic
. If
dynamic
is the type you want, this is technically the most terse way to get
it. However, it’s not the most clear way. A casual reader of your code who
sees that an annotation is missing has no way of knowing if you intended it to be
dynamic
, expected inference to fill in some other type, or simply forgot to
write the annotation.
When dynamic
is the type you want, write that explicitly to make your intent
clear and highlight that this code has less static safety.
dynamic mergeJson(dynamic original, dynamic changes) => ...
mergeJson(original, changes) => ...
Note that it’s OK to omit the type when Dart successfully infers dynamic
.
Map<String, dynamic> readJson() => ...
void printUsers() {
var json = readJson();
var users = json['users'];
print(users);
}
Here, Dart infers Map<String, dynamic>
for json
and then from that infers
dynamic
for users
. It’s fine to leave users
without a type annotation. The
distinction is a little subtle. It’s OK to allow inference to propagate
dynamic
through your code from a dynamic
type annotation somewhere else, but
you don’t want it to inject a dynamic
type annotation in a place where your
code did not specify one.
Exception: Type annotations on unused parameters (_
) can be omitted.
推薦 使 function 型別註解的特徵更明顯
成員型別註解識別符號只有 Function
,註解識別符號不包括任何返回值型別或引數型別,請參考專門的 Function 型別說明。使用 Function
型別要稍微比使用 dynamic
更好些。如果要使用 Function
來進行型別註解,註解型別應該包含函式的所有引數及返回值型別。
bool isValid(String value, bool Function(String) test) => ...
bool isValid(String value, Function test) => ...
此條規則有個例外,如果期望一個類別型能夠表示多種函式型別的集合。例如,我們希望接受的可能是一個引數的函式,也可能是兩個引數的函式。由於 Dart 沒有集合型別,所以沒有辦法為類似成員精確的指定型別,這個時候通常只能使用 dynamic
。但這裡使用 Function
要稍微比使用 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 方法指定返回型別。
Linter rule: avoid_return_types_on_setters
在 Dart 中,setter 永遠返回 void
。為 setter 指定型別沒有意義。
void set foo(Foo value) { ... }
set foo(Foo value) { ... }
不要 使用棄用的 typedef 語法。
Linter rule: prefer_generic_function_type_aliases
Dart 有兩種為函式型別定義命名 typedef 註解語法。原始語法如下:
typedef int Comparison<T>(T a, T b);
該語法有幾個問題:
-
無法為一個泛型函式型別指定名稱。在上面的例子中,typedef 自己就是泛型。如果在程式碼中去參考
Comparison
卻不指定引數型別,那麼你會隱含的得到一個int Function(dynamic, dynamic)
型別的函式,而不是int Function<T>(T, T)
。在實際應用中雖然不常用,但是在極少數情況下是很重要的。 -
引數中的單個識別符號會被認為是引數名稱,而不是引數型別。參考下面程式碼:
typedef bool TestNumber(num);
大多數使用者希望這是一個接受
num
返回bool
的函式型別。但它實際上是一個接受任何 物件(dynamic
)返回bool
的型別。 “num” 是引數名稱(它除了被用在 typedef 的宣告程式碼中,再也沒有其他作用)。這個錯誤在 Dart 中存在了很長時間。
新語法如下所示:
typedef Comparison<T> = int Function(T, T);
如果想在方法中包含引數名稱,可以這樣做:
typedef Comparison<T> = int Function(T a, T b);
新語法可以表達舊語法所表達的任何內容,並且避免了單個識別符號會被認為是引數型別的常見錯誤。同一個函式型別語法(typedef 中 =
之後的部分)允許出現在任何型別註解可以能出現的地方。這樣在程式的任何位置,我們都可以以一致的方式來書寫函式型別。
為了避免對已有程式碼產生破壞, typedef 的舊語法依舊支援。但已被棄用。
推薦 優先使用行內函數型別,而後是 typedef。
Linter rule: avoid_private_typedef_functions
在 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 (final observer in _observers) {
observer(event);
last = observer;
}
return last;
}
}
如果函式型別特別長或經常使用,那麼還是有必要使用 typedef 進行定義。但在大多數情況下,使用者更希望知道函式使用時的真實型別,這樣函式型別語法使它們清晰。
考慮 在引數上使用函式型別語法。
Linter rule: use_function_type_syntax_for_parameters
在定義引數為函式型別時,Dart 具有特殊的語法。與 C 類似,使用引數名稱作為函式引數的函式名:
Iterable<T> where(bool predicate(T element)) => ...
在 Dart 2 新增函式型別語法之前,如果希望不透過 typedef 使用函式引數型別,上例是唯一的方法。如今 Dart 已經可以為函式提供泛型註解,那麼也可以將泛型註解用於函式型別引數中:
Iterable<T> where(bool Function(T) predicate) => ...
雖然新語法稍微冗長一點,但是你必須使用新語法才能與其他位置的型別註解的語法保持一致。
dynamic
除非你希望禁用靜態檢查
避免 使用 某些操作適用於任何物件。例如,log()
方法可以接受任何物件,並呼叫物件上的 toString()
方法。在 Dart 中兩種型別可以表示所有型別:Object
和 dynamic
。但是,他們傳達的意義並不相同。和 Java 或 C# 類似,要表示成員型別為所有物件,使用 Object
進行註解。
dynamic
這個型別不僅接受所有物件,也允許所有 operations。在編譯時任何成員對 dynamic
型別值存取是允許的,但在執行時可能會引發例外。如果你可以承擔風險來達到靈活性,dynamic
型別是你不錯的選擇。
除此之外,我們建議你使用 Object?
或者 Object
,並使用 is
來檢查和進行型別升級,以確保在執行時存取判斷這個值支援您要存取的成員。
/// Returns a Boolean representation for [arg], which must
/// be a String or bool.
bool convertToBool(Object arg) {
if (arg is bool) return arg;
if (arg is String) return arg.toLowerCase() == 'true';
throw ArgumentError('Cannot convert $arg to a bool.');
}
這個規則的主要例外是,與已經使用 dynamic
的型別,特別是通用類進行操作的時候。比如,JSON 物件有 Map<String, dynamic>
型別,而且程式碼需要接受相同的型別。即便如此,在呼叫和使用這些 API 的時候,將型別轉換成一個更精確的型別之後再去呼叫成員會更好。
Future<void>
作為無法回值非同步成員的返回型別。
要 使用 對於不返回值得同步函式,要使用 void
作為返回型別。對於需要等待的,但無返回值的非同步方法方法,使用 Future<void>
作為返回值型別。
你可能會見到使用 Future
或 Future<Null>
作為返回值型別,這是因為舊版本的 Dart 不允許
void
作為型別引數。既然現在允許了,那麼就應該使用新的方式。使用新的方式能夠更直接地匹配那些已經指定了型別的同步函式,並在函式體中為呼叫者提供更好的錯誤檢查。
對於一些非同步函式,這些非同步函式不會返回有用的值,而且不需要等待非同步執行結束或不需要處理錯誤結果。那麼使用 void
作為這些非同步函式的返回型別。
FutureOr<T>
作為返回型別。
避免 使用 如果一個方法接受了一個 FutureOr<int>
引數,那麼 引數接受的類型範圍就會變大 。使用者可以使用 int
或者 Future<int>
來呼叫這個方法,所以呼叫這個方法時就不用把 int
包裝到一個
Future
中再傳到方法中。而在方法中這個引數一定會進行被解開封裝處理。
如果是返回一個 FutureOr<int>
型別的值,那麼方法呼叫者在做任何有意義的操作之前,需要檢查返回值是一個 int
還是 Future<int>
(或者呼叫者僅 await
得到一個值,卻把它當做了
Future
)。返回值使用 Future<int>
,型別就清晰了。一個函式要麼一直非同步,要麼一直是同步,這樣才能夠讓呼叫者更容易理解,否則這個函式很難被正確的使用。
Future<int> triple(FutureOr<int> value) async => (await value) * 3;
FutureOr<int> triple(FutureOr<int> value) {
if (value is int) return value * 3;
return value.then((v) => v * 3);
}
對這條規則更準確的描述是,僅在 逆變 位置使用 FutureOr<T>
。引數是逆變 (contravariant) ,返回型別是協變 (covariant) 。在巢狀(Nesting)函式型別中,描述是相反的—如果一個引數自身就是函式引數型別,那麼此時回呼(Callback)函式的返回型別處於逆變位置,回呼(Callback)函式的引數是協變。這意味著回呼(Callback)中的函式型別可以返回 FutureOr<T>
:
Stream<S> asyncMap<T, S>(
Iterable<T> iterable, FutureOr<S> Function(T) callback) async* {
for (final element in iterable) {
yield await callback(element);
}
}
引數
在 Dart 中,可選引數可以是位置引數,也可以是命名引數,但不能兩者都是。
避免 布林型別的位置引數。
Linter rule: avoid_positional_boolean_parameters
與其他型別不同,布林值通常以字面量方式使用。數字值的通常可以包含在命名的常量裡,但對於布林值通常喜歡直接傳 true
和 false
。如果不清楚布林值的含義,這樣會造成呼叫者的程式碼不可讀:
new Task(true);
new Task(false);
new ListBox(false, true, true);
new Button(false);
這裡,應該考慮使用命名引數,命名建構函式或命名常量來闡明呼叫所執行的操作。
Task.oneShot();
Task.repeating();
ListBox(scroll: true, showScrollbars: true);
Button(ButtonState.enabled);
請注意,這並不適用於 setter ,因為 setter 的名稱能夠清楚的闡明值得含義:
listBox.canScroll = true;
button.isEnabled = false;
避免 在呼叫者需要省略前面引數的方法中,使用位置可選引數。
可選的位置引數應該具有邏輯性,前面引數應該比後面的引數使用更頻繁。呼叫者不需要刻意的跳過或省略前面的一個引數而為後面的引數賦值。如果需要省略前面引數,這種情況最好使用命名可選引數。
String.fromCharCodes(Iterable<int> charCodes, [int start = 0, int? end]);
DateTime(int year,
[int month = 1,
int day = 1,
int hour = 0,
int minute = 0,
int second = 0,
int millisecond = 0,
int microsecond = 0]);
Duration(
{int days = 0,
int hours = 0,
int minutes = 0,
int seconds = 0,
int milliseconds = 0,
int microseconds = 0});
避免 強制引數去接受一個特定表示”空引數”的值。
如果呼叫者在邏輯上省略了引數,那麼建議使用可選引數的方式讓這些引數能夠實際性的被省略,而不是強制讓呼叫者去為他們傳入 null
,或者空字串,或者是一些其他特殊的值來表示該引數”不需要傳值”。
省略引數更加簡潔,也有助於防止在呼叫者偶然地將 null
作為實際值傳遞到方法中而引起 bug。
var rest = string.substring(start);
var rest = string.substring(start, null);
要 使用開始為閉區間,結束為開區間的半開半閉區間作為接受範圍。
如果定義一個方法或函式來讓呼叫者能夠從某個整數索引序列中選擇一系列元素或項,開始索引指向的元素為選取的第一個元素,結束索引(可以為可選引數)指向元素的上一個元素為獲取的最後一個元素。
這種方式與核心函式庫一致。
[0, 1, 2, 3].sublist(1, 3) // [1, 2]
'abcd'.substring(1, 3) // 'bc'
在這裡保持一致尤為重要,因為這些引數通常是未命名引數。如果你的 API 中第二個引數使用了長度值,而不是結束索引,那麼在呼叫端是無法區分兩者之間的差異的。
相等
可能為類實現自訂相等的判定是比較棘手事情。使用者對於物件的判等情況有著很深的直覺,同時像雜湊表這樣的集合型別擁有一些細微的規則,包含在這些集合中的元素需要遵循這些規則。
==
運運算元的類,重寫 hashCode
方法。
要 對重寫 Linter rule: hash_and_equals
預設的雜湊實現為物件提供了一個身份雜湊—如果兩個物件是完全相同的,那麼它們通常具有相同的雜湊值。同樣,==
的預設行為是比較兩個物件的身份雜湊。
如果你重寫 ==
,就意味著你可能有不同的物件要讓你的類認為是”相等的”。任何兩個物件要相等就必須必須具有相同的雜湊值。 否則,這兩個物件就無法被 map 和其他基於雜湊的集合識別為等效物件。
==
運運算元的相等遵守數學規則。
要 讓 等價關係應該是:
-
自反性:
a == a
應該始終返回true
。 -
對稱性:
a == b
應該與b == a
的返回值相同。 -
傳遞性: If
a == b
和b == c
都返回true
,那麼a == c
也應該返回true
。
Users and code that uses ==
expect all of these laws to be followed. If your
class can’t obey these rules, then ==
isn’t the right name for the operation
you’re trying to express.
避免 為可變類自訂相等。
Linter rule: avoid_equals_and_hash_code_on_mutable_classes
定義 ==
時,必須要定義 hashCode
。兩者都需要考慮物件的欄位。如果這些欄位發生了變化,則意味著物件的雜湊值可能會改變。
大多數基於雜湊的集合是無法預料元素雜湊值的改變—他們假設元素物件的雜湊值是永遠不變的,如果元素雜湊值發生了改變,可能會出現不可預測的結果
==
運運算元與可空值比較。
不要 使用 Linter rule: avoid_null_checks_in_equality_operators
Dart 指定此檢查是自動完成的,只有當右側不是 null
時才呼叫 ==
方法。
class Person {
final String name;
// ···
bool operator ==(Object other) => other is Person && name == other.name;
}
class Person {
final String name;
// ···
bool operator ==(Object? other) =>
other != null && other is Person && name == other.name;
}