跳到主要內容
目錄

Effective Dart:設計

目錄 keyboard_arrow_down keyboard_arrow_up
更多選項

這裡是一些為庫編寫一致、可用 API 的指導方針。

命名

#

命名是編寫可讀、可維護程式碼的重要組成部分。以下最佳實踐可以幫助你實現這一目標。

DO 始終如一地使用術語

#

在整個程式碼中,對同一事物使用相同的名稱。如果你的 API 之外已經存在使用者可能知道的先例,請遵循該先例。

好的示例dart
pageCount         // A field.
updatePageCount() // Consistent with pageCount.
toSomething()     // Consistent with Iterable's toList().
asSomething()     // Consistent with List's asMap().
Point             // A familiar concept.
不好的示例dart
renumberPages()      // Confusingly different from pageCount.
convertToSomething() // Inconsistent with toX() precedent.
wrappedAsSomething() // Inconsistent with asX() precedent.
Cartesian            // Unfamiliar to most users.

目標是利用使用者已知的知識。這包括他們對問題領域本身的瞭解、核心庫的約定以及你自己的 API 的其他部分。透過在此基礎上構建,你可以減少他們在能夠高效工作之前必須獲取的新知識量。

AVOID 使用縮寫

#

除非縮寫比未縮寫術語更常見,否則不要使用縮寫。如果你確實使用了縮寫,請正確大寫

好的示例dart
pageCount
buildRectangles
IOStream
HttpRequest
不好的示例dart
numPages    // "Num" is an abbreviation of "number (of)".
buildRects
InputOutputStream
HypertextTransferProtocolRequest

PREFER 將最具描述性的名詞放在最後

#

最後一個詞應該是對事物的最描述性詞語。你可以在其前面加上其他詞語,例如形容詞,以進一步描述事物。

好的示例dart
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.
不好的示例dart
numPages                  // Not a collection of pages.
CanvasRenderingContext2D  // Not a "2D".
RuleFontFaceCss           // Not a CSS.

CONSIDER 使程式碼讀起來像句子

#

在命名方面有疑問時,編寫一些使用你的 API 的程式碼,並嘗試像讀句子一樣閱讀它。

好的示例dart
// "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);
不好的示例dart
// 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 並檢視它在程式碼中的“閱讀”效果是有幫助的,但你可能做得過頭。新增冠詞和其他詞性來強制你的名稱“字面上”讀起來像一個語法正確的句子是沒有幫助的。

不好的示例dart
if (theCollectionOfErrors.isEmpty) {
  // ...
}

monsters.producesANewSequenceWhereEach((monster) => monster.hasClaws);

PREFER 為非布林屬性或變數使用名詞短語

#

讀者的注意力集中在屬性是“什麼”。如果使用者更關心屬性是“如何”確定的,那麼它可能應該是一個帶有動詞短語名稱的方法。

好的示例dart
list.length
context.lineWidth
quest.rampagingSwampBeast
不好的示例dart
list.deleteItems

PREFER 為布林屬性或變數使用非命令式動詞短語

#

布林名稱通常用作控制流中的條件,因此你需要一個在那裡讀起來很好的名稱。比較一下:

dart
if (window.closeable) ...  // Adjective.
if (window.canClose) ...   // Verb.

好的名稱通常以幾種動詞開頭:

  • “to be” 的形式:isEnabledwasShownwillFire。這些是迄今為止最常見的。

  • 一個助動詞hasElementscanCloseshouldConsumemustSave

  • 一個主動動詞:ignoresInputwroteFile。這些很少見,因為它們通常模稜兩可。loggedResult 是一個不好的名稱,因為它可能意味著“是否記錄了結果”或“記錄的結果”。同樣,closingConnection 可能意味著“連線是否正在關閉”或“正在關閉的連線”。當名稱只能被理解為謂詞時,允許使用主動動詞。

將所有這些動詞短語與方法名稱區分開來的是,它們不是“命令式”的。布林名稱絕不應該聽起來像命令物件做某事,因為訪問屬性不會改變物件。(如果屬性“確實”以有意義的方式修改物件,則它應該是一個方法。)

好的示例dart
isEmpty
hasElements
canClose
closesWindow
canShowPopup
hasShownPopup
不好的示例dart
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.

CONSIDER 為具名布林“引數”省略動詞

#

這細化了上一條規則。對於布林型別的具名引數,名稱通常即使沒有動詞也同樣清晰,並且程式碼在呼叫點讀起來更好。

好的示例dart
Isolate.spawn(entryPoint, message, paused: false);
var copy = List.from(elements, growable: true);
var regExp = RegExp(pattern, caseSensitive: false);

PREFER 為布林屬性或變數使用“肯定”名稱

#

大多數布林名稱在概念上都有“肯定”和“否定”形式,其中前者感覺像是基本概念,後者是其否定——“開啟”和“關閉”、“啟用”和“停用”等等。通常,後一個名稱字面上帶有一個否定前者的字首:“可見”(visible)和“不可見”(in-visible),“已連線”(connected)和“已斷開”(dis-connected),“零”(zero)和“非零”(non-zero)。

在選擇 true 代表哪兩種情況時——因此屬性以哪種情況命名——優先選擇肯定或更基本的那個。布林成員經常巢狀在邏輯表示式中,包括否定運算子。如果你的屬性本身讀起來像否定,則讀者很難在腦海中進行雙重否定並理解程式碼的含義。

好的示例dart
if (socket.isConnected && database.hasData) {
  socket.write(database.read());
}
不好的示例dart
if (!socket.isDisconnected && !database.isEmpty) {
  socket.write(database.read());
}

對於某些屬性,沒有明顯的肯定形式。已重新整理到磁碟的文件是“已儲存”還是“未更改”?尚未重新整理的文件是“未儲存”還是“已更改”?在模稜兩可的情況下,傾向於選擇使用者不太可能否定或名稱較短的那個。

例外: 對於某些屬性,負面形式是使用者絕大多數需要使用的。選擇肯定情況會迫使他們隨處使用 ! 來否定屬性。在這種情況下,最好為該屬性使用負面情況。

PREFER 為主要目的是產生副作用的函式或方法使用命令式動詞短語

#

可呼叫成員可以向呼叫者返回結果,並執行其他工作或副作用。在像 Dart 這樣的命令式語言中,成員通常主要因其副作用而被呼叫:它們可能改變物件的內部狀態、產生一些輸出或與外部世界通訊。

這些型別的成員應使用命令式動詞短語命名,以闡明成員執行的工作。

好的示例dart
list.add('element');
queue.removeFirst();
window.refresh();

這樣,呼叫讀起來就像執行該工作的命令。

PREFER 如果返回值為其主要目的,則為函式或方法使用名詞短語或非命令式動詞短語

#

其他可呼叫成員副作用很少,但向呼叫者返回有用的結果。如果成員不需要引數即可完成此操作,通常應為 getter。但有時邏輯“屬性”需要一些引數。例如,elementAt() 從集合中返回一塊資料,但它需要一個引數才能知道返回“哪”塊資料。

這意味著成員在“語法上”是一個方法,但在“概念上”是一個屬性,應使用描述成員返回“什麼”的短語來命名。

好的示例dart
var element = list.elementAt(3);
var first = list.firstWhere(test);
var char = string.codeUnitAt(4);

此指導原則有意比前一條更寬鬆。有時方法沒有副作用,但使用像 list.take()string.split() 這樣的動詞短語命名仍然更簡單。

CONSIDER 如果你想引起對函式或方法所執行工作的注意,則使用命令式動詞短語

#

當一個成員產生結果而沒有任何副作用時,它通常應該是一個 getter 或一個帶有描述其返回結果的名詞短語的方法。但是,有時產生結果所需的工作很重要。它可能容易發生執行時錯誤,或使用重量級資源,如網路或檔案 I/O。在這種情況下,當你希望呼叫者考慮成員正在執行的工作時,給成員一個動詞短語名稱來描述該工作。

好的示例dart
var table = database.downloadData();
var packageVersions = packageGraph.solveConstraints();

但請注意,此指導原則比前兩條更寬鬆。操作執行的工作通常是與呼叫者無關的實現細節,並且效能和健壯性邊界會隨時間變化。大多數情況下,根據成員為呼叫者做了“什麼”來命名,而不是根據它們“如何”做來命名。

AVOID 方法名以 get 開頭

#

在大多數情況下,該方法應為 getter,並且從名稱中刪除 get。例如,不要使用名為 getBreakfastOrder() 的方法,而應定義名為 breakfastOrder 的 getter。

即使成員確實需要成為一個方法(因為它需要引數或不適合作為 getter),你也應該避免使用 get。正如前面的指導原則所述,要麼:

  • 只需去掉 get使用名詞短語命名,例如 breakfastOrder(),如果呼叫者主要關心方法返回的值。

  • 使用動詞短語命名,如果呼叫者關心正在進行的工作,但選擇一個比 get 更精確地描述工作的動詞,例如 createdownloadfetchcalculaterequestaggregate 等。

PREFER 如果方法將物件狀態複製到新物件,則將其命名為 to___()

#

Linter 規則:use_to_and_as_if_applicable

一個“轉換”方法是返回一個新物件的方法,該新物件包含接收者幾乎所有狀態的副本,但通常採用不同的形式或表示。核心庫有一個約定,這些方法名稱以 to 開頭,後跟結果的型別。

如果你定義轉換方法,遵循此約定會很有幫助。

好的示例dart
list.toSet();
stackTrace.toString();
dateTime.toLocal();

PREFER 如果方法返回由原始物件支援的不同表示形式,則將其命名為 as___()

#

Linter 規則:use_to_and_as_if_applicable

轉換方法是“快照”。結果物件擁有原始物件狀態的獨立副本。還有其他類似轉換的方法返回“檢視”——它們提供一個新物件,但該物件引用回原始物件。對原始物件的後續更改會反映在檢視中。

你需要遵循的核心庫約定是 as___()

好的示例dart
var map = table.asMap();
var list = bytes.asFloat32List();
var future = subscription.asFuture();

AVOID 在函式或方法名稱中描述引數

#

使用者將在呼叫點看到引數,因此在名稱本身中提及它通常對提高可讀性沒有幫助。

好的示例dart
list.add(element);
map.remove(key);
不好的示例dart
list.addElement(element)
map.removeKey(key)

但是,提及引數有助於區分它與接受不同型別的其他同名方法:

好的示例dart
map.containsKey(key);
map.containsValue(value);

DO 命名型別引數時遵循現有助記約定

#

單字母名稱並非十分清晰,但幾乎所有泛型型別都使用它們。幸運的是,它們大多以一致的、助記的方式使用它們。約定如下:

  • 集合中的“元素”(Element)型別使用 E

    好的示例dart
    class IterableBase<E> {}
    class List<E> {}
    class HashSet<E> {}
    class RedBlackTree<E> {}
  • 關聯集合中的“鍵”(Key)和“值”(Value)型別使用 KV

    好的示例dart
    class Map<K, V> {}
    class Multimap<K, V> {}
    class MapEntry<K, V> {}
  • 用作函式或類方法的“返回”(Return)型別的型別使用 R。這不常見,但有時出現在 typedef 和實現訪問者模式的類中

    好的示例dart
    abstract class ExpressionVisitor<R> {
      R visitBinary(BinaryExpression node);
      R visitLiteral(LiteralExpression node);
      R visitUnary(UnaryExpression node);
    }
  • 否則,對於只有一個型別引數且周圍型別使其含義明顯的泛型,使用 TSU。這裡有多個字母,以便巢狀時不會遮蓋周圍的名稱。例如:

    好的示例dart
    class Future<T> {
      Future<S> then<S>(FutureOr<S> onValue(T value)) => ...
    }

    在這裡,泛型方法 then<S>() 使用 S 來避免遮蓋 Future<T> 上的 T

如果以上情況都不適用,則可以使用另一個單字母助記名稱或描述性名稱:

好的示例dart
class Graph<N, E> {
  final List<N> nodes = [];
  final List<E> edges = [];
}

class Graph<Node, Edge> {
  final List<Node> nodes = [];
  final List<Edge> edges = [];
}

在實踐中,現有約定涵蓋了大多數型別引數。

#

前導下劃線字元(_)表示成員對其庫是私有的。這不僅僅是約定,而是語言本身內建的特性。

PREFER 將宣告設為私有

#

庫中的公共宣告——無論是頂層還是類中——都表明其他庫可以並且應該訪問該成員。這同時也是你的庫承諾支援並在此發生時表現正確的承諾。

如果這不是你的意圖,請新增小小的 _ 並放心。窄的公共介面對你來說更容易維護,對使用者來說更容易學習。一個不錯的額外好處是,分析器會告訴你未使用的私有宣告,以便你可以刪除死程式碼。如果成員是公共的,它就無法做到這一點,因為它不知道檢視之外的任何程式碼是否正在使用它。

CONSIDER 在同一個庫中宣告多個類

#

一些語言,如 Java,將檔案組織與類組織關聯起來——每個檔案只能定義一個頂層類。Dart 沒有這個限制。庫是與類不同的實體。如果它們在邏輯上都屬於一起,一個庫包含多個類、頂層變數和函式是完全可以的。

將多個類放在一個庫中可以實現一些有用的模式。由於 Dart 中的隱私是在庫級別而非類級別工作,這是一種定義“友元”類的方式,就像你在 C++ 中可能做的那樣。同一庫中宣告的每個類都可以訪問彼此的私有成員,但該庫之外的程式碼則不能。

當然,這條指導原則並不意味著你應該將所有類都放入一個巨大的、單一的庫中,只是允許你在一個庫中放置不止一個類。

類與混入

#

Dart 是一種“純粹”的面嚮物件語言,因為所有物件都是類的例項。但 Dart 不要求所有程式碼都定義在類內部——你可以像在過程式或函式式語言中一樣定義頂層變數、常量和函式。

AVOID 在簡單函式即可滿足需求時,定義單成員抽象類

#

Linter 規則:one_member_abstracts

與 Java 不同,Dart 具有第一類函式、閉包以及易於使用的簡潔語法。如果只需要像回撥這樣的東西,只需使用函式即可。如果你正在定義一個類,並且它只有一個抽象成員,名稱沒有意義(例如 callinvoke),很有可能你只是需要一個函式。

好的示例dart
typedef Predicate<E> = bool Function(E element);
不好的示例dart
abstract class Predicate<E> {
  bool test(E element);
}

AVOID 定義只包含靜態成員的類

#

Linter 規則:avoid_classes_with_only_static_members

在 Java 和 C# 中,每個定義都“必須”在類內部,因此常見的是看到“類”僅作為放置靜態成員的場所存在。其他類用作名稱空間——一種為一組成員提供共享字首以關聯它們或避免名稱衝突的方式。

Dart 有頂層函式、變數和常量,因此你不需要一個類來定義某些東西。如果你想要的是名稱空間,那麼庫是更好的選擇。庫支援匯入字首和 show/hide 組合器。這些強大的工具讓你的程式碼使用者能夠以最適合“他們”的方式處理名稱衝突。

如果函式或變數在邏輯上不與類繫結,則將其放在頂層。如果你擔心名稱衝突,則可以給它一個更精確的名稱,或者將其移動到可以帶字首匯入的獨立庫中。

好的示例dart
DateTime mostRecent(List<DateTime> dates) {
  return dates.reduce((a, b) => a.isAfter(b) ? a : b);
}

const _favoriteMammal = 'weasel';
不好的示例dart
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 中,類定義“物件型別”。從未例項化過的型別是一種程式碼異味。

然而,這並非硬性規定。例如,對於常量和類似列舉的型別,將它們分組在一個類中可能是很自然的。

好的示例dart
class Color {
  static const red = '#f00';
  static const green = '#0f0';
  static const blue = '#00f';
  static const black = '#000';
  static const white = '#fff';
}

AVOID 繼承不打算被子類化的類

#

如果建構函式從生成建構函式更改為工廠建構函式,則任何呼叫該建構函式的子類建構函式都將中斷。此外,如果類更改其呼叫 this 的方法,這也可能會中斷重寫這些方法並期望在特定點被呼叫的子類。

這兩點都意味著一個類需要仔細考慮是否允許子類化。這可以在文件註釋中傳達,或者透過給類一個明顯的名稱,例如 IterableBase。如果類的作者沒有這樣做,最好假定你“不”應該繼承該類。否則,對其的後續更改可能會破壞你的程式碼。

DO 使用類修飾符來控制你的類是否可以被繼承

#

類修飾符如 final, interface, 或 sealed 限制了類如何被繼承。例如,使用 final class A {}interface class B {} 來阻止在當前庫之外的繼承。使用這些修飾符來傳達你的意圖,而不是依賴文件。

AVOID 實現不打算作為介面的類

#

隱式介面是 Dart 中強大的工具,可以避免在可以從合同實現的簽名中輕鬆推斷出類的合同的情況下重複該合同。

但是實現類的介面與該類的耦合非常緊密。這意味著幾乎對你正在實現的類的介面的“任何”更改都會破壞你的實現。例如,向類新增新成員通常是安全且非破壞性的更改。但是如果你正在實現該類的介面,現在你的類將出現靜態錯誤,因為它缺少新方法的實現。

庫維護者需要有能力在不破壞使用者的情況下演進現有類。如果你將每個類都視為暴露了一個使用者可以自由實現的介面,那麼更改這些類將變得非常困難。這種困難反過來意味著你所依賴的庫會更慢地增長和適應新的需求。

為了給您使用的類的作者更多餘地,請避免實現隱式介面,除非這些類明確打算被實現。否則,您可能會引入作者不打算引入的耦合,並且他們可能會在不知情的情況下破壞您的程式碼。

DO 使用類修飾符來控制你的類是否可以作為介面

#

設計庫時,使用類修飾符如 finalbasesealed 來強制預期的用法。例如,使用 final class C {}base class D{} 來阻止在當前庫之外的實現。雖然所有庫都使用這些修飾符來強制設計意圖是理想的,但開發者仍可能遇到未應用這些修飾符的情況。在這種情況下,請注意意外的實現問題。

PREFER 定義純 mixin 或純 class 而非 mixin class

#

Linter 規則:prefer_mixin

Dart 之前(語言版本從 2.122.19)允許滿足某些限制(沒有非預設建構函式,沒有超類等)的任何類混入到其他類中。這令人困惑,因為類的作者可能並不打算將其混入。

Dart 3.0.0 現在要求,任何打算混入到其他類以及作為普通類處理的型別,都必須使用 mixin class 宣告明確宣告。

然而,需要同時作為 mixin 和類的型別應該屬於罕見情況。mixin class 宣告主要旨在幫助將用作 mixin 的 3.0.0 之前的類遷移到更明確的宣告。新程式碼應透過僅使用純 mixin 或純 class 宣告來明確其宣告的行為和意圖,並避免 mixin class 的歧義。

閱讀 Migrating classes as mixins 以獲取有關 mixinmixin class 宣告的更多指導。

建構函式

#

Dart 建構函式透過宣告一個與類同名且可選地帶附加識別符號的函式來建立。後者稱為“具名建構函式”。

CONSIDER 如果類支援,則使你的建構函式為 const

#

如果你有一個類,其中所有欄位都是 final,並且建構函式除了初始化它們之外什麼也不做,則可以將該建構函式設為 const。這允許使用者在需要常量的地方建立你的類例項——在其他更大的常量、switch case、預設引數值等內部。

如果你不明確地將其設為 const,他們就無法做到這一點。

但是請注意,const 建構函式是公共 API 中的承諾。如果你以後將建構函式更改為非 const,它將破壞在常量表達式中呼叫它的使用者。如果你不想承諾這一點,請不要將其設為 const。實際上,const 建構函式對於簡單的、不可變的值型別最有用。

成員

#

成員屬於物件,可以是方法或例項變數。

PREFER 將欄位和頂級變數設為 final

#

Linter 規則:prefer_final_fields

不可變的狀態——不會隨時間變化的狀態——程式設計師更容易推理。儘量減少處理可變狀態的類和庫往往更容易維護。當然,擁有可變資料通常很有用。但是,如果你不需要它,你應該預設在可能的情況下將欄位和頂層變數設為 final

有時,一個例項欄位在初始化後不會改變,但直到例項構造後才能初始化。例如,它可能需要引用 this 或例項上的其他欄位。在這種情況下,請考慮將欄位設為 late final。這樣做時,你還可能能夠在宣告時初始化欄位

DO 對於概念上訪問屬性的操作使用 getter

#

決定成員應該是 getter 還是方法,是良好 API 設計中一個微妙但重要的部分,因此有這麼長的指導原則。一些其他語言的文化迴避 getter。他們只在操作幾乎完全像欄位一樣時使用它們——它對完全存在於物件上的狀態進行微小的計算。任何比這更復雜或更重量級的操作都會在名稱後加上 () 來表示“正在進行計算!”,因為點號後的裸名稱表示“欄位”。

Dart “不是”那樣的。在 Dart 中,“所有”帶點的名稱都是可能進行計算的成員呼叫。欄位很特別——它們是語言提供的實現的 getter。換句話說,getter 在 Dart 中不是“特別慢的欄位”;欄位是“特別快的 getter”。

即便如此,選擇 getter 而非方法會向呼叫者傳送一個重要的訊號。這個訊號大致是操作是“類似欄位的”。至少原則上,就呼叫者而言,操作“可以”使用欄位實現。這意味著:

  • 操作不接受任何引數並返回結果。

  • 呼叫者主要關心結果。如果你希望呼叫者更關心操作“如何”產生結果,而不是關心正在產生的結果,那麼給操作一個描述工作的動詞名稱並將其設為一個方法。

    這“並不”意味著操作為了成為 getter 必須特別快。IterableBase.lengthO(n),這沒問題。getter 執行顯著計算是允許的。但是如果它執行了“驚人”的工作量,你可能希望透過將其設為一個方法來引起他們的注意,該方法的名稱是一個描述其工作的動詞。

    不好的示例dart
    connection.nextIncomingMessage; // Does network I/O.
    expression.normalForm; // Could be exponential to calculate.
  • 該操作沒有使用者可見的副作用。訪問真正的欄位不會改變物件或程式中的任何其他狀態。它不會產生輸出、寫入檔案等。getter 也不應該做這些事情。

    “使用者可見”部分很重要。getter 修改隱藏狀態或產生帶外副作用是可以的。getter 可以延遲計算和儲存結果、寫入快取、記錄資訊等。只要呼叫者“不關心”副作用,這可能都沒問題。

    不好的示例dart
    stdout.newline; // Produces output.
    list.clear; // Modifies object.
  • 操作是冪等的。“冪等”是一個奇怪的詞,在這個語境下基本意味著多次呼叫同一個操作會產生相同的結果,除非這些呼叫之間有狀態被明確修改。(顯然,如果你在呼叫之間向列表中新增元素,list.length 會產生不同的結果。)

    這裡的“相同結果”並不意味著 getter 在連續呼叫時必須字面意思上產生完全相同的物件。要求這樣做會迫使許多 getter 擁有脆弱的快取,這與使用 getter 的初衷相悖。getter 每次呼叫時返回一個新的 future 或 list 是常見且完全可以接受的。重要的是 future 完成到相同的值,並且 list 包含相同的元素。

    換句話說,結果值在呼叫者關心的方面應該是一樣的。

    不好的示例dart
    DateTime.now; // New result each time.
  • 結果物件不會暴露原始物件的全部狀態。欄位只暴露物件的一部分。如果你的操作返回一個暴露原始物件全部狀態的結果,它可能最好是 to___()as___() 方法。

如果以上所有描述都適用於你的操作,那麼它應該是一個 getter。看起來符合條件的情況不多,但令人驚訝的是有很多。許多操作只是對某些狀態進行一些計算,其中大多數都可以而且應該成為 getter。

好的示例dart
rectangle.area;
collection.isEmpty;
button.canShow;
dataSet.minimumValue;

DO 對於概念上更改屬性的操作使用 setter

#

Linter 規則:use_setters_to_change_properties

在 setter 和方法之間進行選擇,類似於在 getter 和方法之間進行選擇。在兩種情況下,操作都應“類似欄位”。

對於 setter,“類似欄位”意味著:

  • 操作接受一個引數且不產生返回值。

  • 操作更改物件中的某些狀態。

  • 該操作是冪等的。從呼叫者的角度來看,用相同的值呼叫同一個 setter 兩次,第二次呼叫應該什麼都不做。內部,你可能有一些快取失效或日誌記錄。這沒問題。但從呼叫者的角度來看,第二次呼叫似乎什麼也沒做。

好的示例dart
rectangle.width = 3;
button.visible = false;

DON'T 定義沒有對應 getter 的 setter

#

Linter 規則:avoid_setters_without_getters

使用者將 getter 和 setter 視為物件的可見屬性。“投遞箱”屬性只能寫入而不能檢視,這會令人困惑,並與他們對屬性工作方式的直覺相沖突。例如,沒有 getter 的 setter 意味著你可以使用 = 來修改它,但不能使用 +=

這條指導原則“不”意味著你應該僅僅為了允許新增你想要的 setter 而新增 getter。物件通常不應該暴露超出其需要狀態。如果你有某個物件狀態可以修改,但不能以相同方式暴露,則改用方法。

AVOID 使用執行時型別測試來模擬過載

#

API 通常支援對不同型別的引數進行類似的操作。為了強調相似性,一些語言支援“過載”,它允許你定義多個具有相同名稱但引數列表不同的方法。在編譯時,編譯器會檢視實際的引數型別來確定呼叫哪個方法。

Dart 沒有過載。你可以透過定義單個方法,然後在方法體內使用 is 型別測試來檢視引數的執行時型別並執行適當的行為,從而定義一個看起來像過載的 API。然而,以這種方式模擬過載會將“編譯時”方法選擇轉變為“執行時”發生的選擇。

如果呼叫者通常知道他們擁有哪種型別以及他們想要哪種特定操作,那麼最好定義具有不同名稱的單獨方法,讓呼叫者選擇正確的操作。這提供了更好的靜態型別檢查和更快的效能,因為它避免了任何執行時型別測試。

但是,如果使用者可能擁有未知型別的物件,並且“希望”API 在內部使用 is 來選擇正確的操作,那麼引數是所有支援型別超型別的單個方法可能是合理的。

AVOID 沒有初始化器的公共 late final 欄位

#

與其他 final 欄位不同,沒有初始化器的 late final 欄位“會”定義一個 setter。如果該欄位是公共的,則 setter 也是公共的。這很少是你想要的。欄位通常被標記為 late,以便它們可以在例項生命週期中的某個時刻在“內部”初始化,通常是在建構函式體內部。

除非你“確實”希望使用者呼叫該 setter,否則最好選擇以下解決方案之一:

  • 不要使用 late
  • 使用工廠建構函式計算 final 欄位的值。
  • 使用 late,但在宣告時初始化 late 欄位。
  • 使用 late,但將 late 欄位設為私有,併為其定義一個公共 getter。

AVOID 返回可為空的 FutureStream 和集合型別

#

當一個 API 返回一個容器型別時,它有兩種方式表示資料缺失:它可以返回一個空容器,也可以返回 null。使用者通常認為並更喜歡你使用空容器表示“無資料”。這樣,他們就擁有一個真實的物件,可以呼叫 isEmpty 等方法。

為了表示你的 API 沒有資料提供,最好返回一個空集合、一個不可為空的 Future(型別可為空)或一個不發出任何值的 stream。

例外: 如果返回 null “意味著”與返回空容器“不同”,則使用可空型別可能是有意義的。

AVOID 為了實現鏈式呼叫而從方法返回 this

#

Linter 規則:avoid_returning_this

方法級聯 (Method cascades) 是更好的鏈式呼叫方法。

好的示例dart
var buffer =
    StringBuffer()
      ..write('one')
      ..write('two')
      ..write('three');
不好的示例dart
var buffer =
    StringBuffer()
        .write('one')
        .write('two')
        .write('three');

型別

#

當你寫下程式中的型別時,你就約束了流入程式碼不同部分的數值種類。型別可以出現在兩種地方:“宣告”上的型別註釋和“泛型呼叫”的型別引數。

型別註釋是您通常想到“靜態型別”時所想到的。您可以註釋變數、引數、欄位或返回型別。在以下示例中,boolString 是型別註釋。它們附加在程式碼的靜態宣告結構上,並且在執行時不會“執行”。

dart
bool isEmpty(String parameter) {
  bool result = parameter.isEmpty;
  return result;
}

泛型呼叫是一個集合字面量、對泛型類建構函式的呼叫或對泛型方法的呼叫。在下一個示例中,numint 是泛型呼叫上的型別引數。儘管它們是型別,但它們是第一類實體,會在執行時具化並傳遞給呼叫。

dart
var lists = <num>[1, 2];
lists.addAll(List<num>.filled(3, 4));
lists.cast<int>();

我們在這裡強調“泛型呼叫”部分,因為型別引數也可能出現在型別註釋中:

dart
List<int> ints = [1, 2];

這裡,int 是一個型別引數,但它出現在型別註釋中,而不是泛型呼叫中。您通常無需擔心這種區別,但在少數地方,我們對型別在泛型呼叫中與在型別註釋中使用的指導有所不同。

型別推斷

#

在 Dart 中,型別註釋是可選的。如果您省略一個,Dart 會嘗試根據附近的上下文推斷一個型別。有時它沒有足夠的資訊來推斷完整的型別。發生這種情況時,Dart 有時會報告錯誤,但通常會靜默地用 dynamic 填充任何缺失的部分。隱式 dynamic 會導致程式碼看起來是推斷的且安全的,但實際上完全停用了型別檢查。下面的規則透過在推斷失敗時要求型別來避免這種情況。

Dart 既有型別推斷又有 dynamic 型別的事實導致了一些關於程式碼“無型別”含義的困惑。這是指程式碼是動態型別的,還是您沒有“編寫”型別?為了避免這種困惑,我們避免說“無型別”,而是使用以下術語:

  • 如果程式碼是“型別註釋”的,則型別在程式碼中明確寫出。

  • 如果程式碼是“推斷”的,則沒有編寫型別註釋,並且 Dart 成功地自行找出了型別。推斷可能會失敗,在這種情況下,指導原則不認為這是推斷的。

  • 如果程式碼是“動態”的,則其靜態型別是特殊的 dynamic 型別。程式碼可以顯式註釋為 dynamic,也可以推斷為 dynamic

換句話說,程式碼是否被註釋或推斷與它是否是 dynamic 或其他型別是正交的。

型別推斷是一個強大的工具,可以讓你省去編寫和閱讀那些顯而易見或不重要的型別。它讓讀者的注意力集中在程式碼本身的行為上。顯式型別也是健壯、可維護程式碼的關鍵部分。它們定義了 API 的靜態結構,並建立了邊界來記錄和強制執行允許哪些型別的值到達程式的各個部分。

當然,型別推斷並非魔術。有時推斷成功並選擇了一種型別,但這不是你想要的型別。常見情況是從變數的初始化器推斷出過於精確的型別,而你打算稍後將其他型別的數值賦給該變數。在這種情況下,你必須顯式地寫出型別。

這裡的指導原則在簡潔性和控制、靈活性和安全性之間找到了我們發現的最佳平衡。有具體的指導原則涵蓋所有各種情況,但大致總結如下:

  • 即使 dynamic 是您想要的型別,當推斷沒有足夠的上下文時也要註釋。

  • 除非必要,否則不要註釋區域性變數和泛型呼叫。

  • 除非初始化器使型別顯而易見,否則優先註釋頂層變數和欄位。

DO 註釋未初始化變數的型別

#

Linter 規則:prefer_typing_uninitialized_variables

變數的型別——頂層、區域性、靜態欄位或例項欄位——通常可以從其初始化器推斷出來。但是,如果沒有初始化器,推斷就會失敗。

好的示例dart
List<AstNode> parameters;
if (node is Constructor) {
  parameters = node.signature;
} else if (node is Method) {
  parameters = node.parameters;
}
不好的示例dart
var parameters;
if (node is Constructor) {
  parameters = node.signature;
} else if (node is Method) {
  parameters = node.parameters;
}

DO 如果型別不明顯,則註釋欄位和頂級變數的型別

#

Linter 規則:type_annotate_public_apis

型別註釋是關於如何使用庫的重要文件。它們在程式的區域之間形成邊界,以隔離型別錯誤的來源。考慮一下:

不好的示例dart
install(id, destination) => ...

這裡不清楚 id 是什麼。一個字串?destination 是什麼?一個字串還是一個 File 物件?這個方法是同步的還是非同步的?這樣更清楚:

好的示例dart
Future<bool> install(PackageId id, String destination) => ...

然而,在某些情況下,型別非常明顯,寫出來毫無意義:

好的示例dart
const screenWidth = 640; // Inferred as int.

“明顯”沒有精確定義,但這些都是很好的候選:

  • 字面量。
  • 建構函式呼叫。
  • 引用其他明確指定型別的常量。
  • 數字和字串上的簡單表示式。
  • 使用者應熟悉的工廠方法,如 int.parse()Future.wait() 等。

如果您認為初始化表示式——無論是什麼——足夠清晰,則可以省略註釋。但如果您認為註釋有助於使程式碼更清晰,則新增一個。

如果拿不準,就新增型別註釋。即使型別很明顯,您可能仍然希望顯式註釋。如果推斷的型別依賴於來自其他庫的值或宣告,您可能希望註釋您自己的宣告,這樣對其他庫的更改就不會在您未意識到的情況下悄悄更改您自己 API 的型別。

此規則適用於公共和私有宣告。正如 API 上的型別註釋有助於程式碼的“使用者”一樣,私有成員上的型別有助於“維護者”。

DON'T 對已初始化的區域性變數進行冗餘的型別註釋

#

Linter 規則:omit_local_variable_types

區域性變數,特別是在函式趨於短小的現代程式碼中,作用域很小。省略型別可以使讀者的注意力集中在更重要的變數“名稱”及其初始化值上。

好的示例dart
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;
}
不好的示例dart
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;
}

有時推斷的型別不是你希望變數擁有的型別。例如,你可能打算稍後為變數賦其他型別的值。在這種情況下,用你想要的型別註釋變數。

好的示例dart
Widget build(BuildContext context) {
  Widget result = Text('You won!');
  if (applyPadding) {
    result = Padding(padding: EdgeInsets.all(8.0), child: result);
  }
  return result;
}

DO 註釋函式宣告的返回型別

#

與某些其他語言不同,Dart 通常不會從函式宣告的函式體推斷返回型別。這意味著你應該自己為返回型別編寫型別註釋。

好的示例dart
String makeGreeting(String who) {
  return 'Hello, $who!';
}
不好的示例dart
makeGreeting(String who) {
  return 'Hello, $who!';
}

請注意,此指南僅適用於“非區域性”函式宣告:頂級、靜態以及例項方法和 getter。區域性函式和匿名函式表示式從其函式體推斷返回型別。事實上,匿名函式語法甚至不允許返回型別註釋。

DO 註釋函式宣告的引數型別

#

函式的引數列表定義了其與外部世界的邊界。註釋引數型別可以使該邊界明確定義。請注意,儘管預設引數值看起來像變數初始化器,但 Dart 不會從可選引數的預設值推斷其型別。

好的示例dart
void sayRepeatedly(String message, {int count = 2}) {
  for (var i = 0; i < count; i++) {
    print(message);
  }
}
不好的示例dart
void sayRepeatedly(message, {count = 2}) {
  for (var i = 0; i < count; i++) {
    print(message);
  }
}

例外:函式表示式和初始化形式引數有不同的型別註釋約定,如下面兩條指南所述。

DON'T 註釋函式表示式中推斷的引數型別

#

Linter 規則:avoid_types_on_closure_parameters

匿名函式幾乎總是立即傳遞給接受某種型別回撥的方法。當在型別化上下文中建立函式表示式時,Dart 會嘗試根據期望型別推斷函式的引數型別。例如,當你將函式表示式傳遞給 Iterable.map() 時,你的函式引數型別會根據 map() 期望的回撥型別進行推斷:

好的示例dart
var names = people.map((person) => person.name);
不好的示例dart
var names = people.map((Person person) => person.name);

如果語言能夠推斷出您想要的函式表示式引數型別,則無需註釋。在極少數情況下,周圍的上下文不足以提供函式一個或多個引數的型別。在這種情況下,您可能需要註釋。(如果函式未立即使用,通常最好將其設為具名宣告。)

DON'T 註釋初始化形式引數 (initializing formals) 的型別

#

Linter 規則:type_init_formals

如果建構函式引數使用 this. 初始化欄位,或使用 super. 轉發超級引數,則引數的型別將被推斷為與欄位或超級建構函式引數的型別相同。

好的示例dart
class Point {
  double x, y;
  Point(this.x, this.y);
}

class MyWidget extends StatelessWidget {
  MyWidget({super.key});
}
不好的示例dart
class Point {
  double x, y;
  Point(double this.x, double this.y);
}

class MyWidget extends StatelessWidget {
  MyWidget({Key? super.key});
}

DO 為未推斷的泛型呼叫寫入型別引數

#

Dart 在泛型呼叫中推斷型別引數非常智慧。它查看錶達式發生的期望型別以及傳遞給呼叫的值的型別。然而,有時這些不足以完全確定型別引數。在這種情況下,請顯式地寫出完整的型別引數列表。

好的示例dart
var playerScores = <String, int>{};
final events = StreamController<Event>();
不好的示例dart
var playerScores = {};
final events = StreamController();

有時呼叫作為變數宣告的初始化器出現。如果變數“不是”區域性的,則您可以在宣告上放置型別註釋,而不是在呼叫本身上寫入型別引數列表:

好的示例dart
class Downloader {
  final Completer<String> response = Completer();
}
不好的示例dart
class Downloader {
  final response = Completer();
}

註釋變數也滿足此指導原則,因為現在型別引數“已”被推斷。

DON'T 為已推斷的泛型呼叫寫入型別引數

#

這與上一條規則相反。如果呼叫的型別引數列表“確實”正確推斷出您想要的型別,則省略這些型別,讓 Dart 為您完成工作。

好的示例dart
class Downloader {
  final Completer<String> response = Completer();
}
不好的示例dart
class Downloader {
  final Completer<String> response = Completer<String>();
}

在這裡,欄位上的型別註釋提供了周圍的上下文,以便推斷初始化器中建構函式呼叫的型別引數。

好的示例dart
var items = Future.value([1, 2, 3]);
不好的示例dart
var items = Future<List<int>>.value(<int>[1, 2, 3]);

在這裡,集合和例項的型別可以從其元素和引數自下而上推斷出來。

AVOID 編寫不完整的泛型型別

#

編寫型別註釋或型別引數的目的是確定一個完整的型別。然而,如果你寫出了泛型型別的名稱但省略了它的型別引數,你就沒有完全指定型別。在 Java 中,這被稱為“原始型別”(raw types)。例如:

不好的示例dart
List numbers = [1, 2, 3];
var completer = Completer<Map>();

在這裡,numbers 有一個型別註釋,但該註釋沒有為泛型 List 提供型別引數。同樣,CompleterMap 型別引數也沒有完全指定。在這種情況下,Dart 不會嘗試使用周圍的上下文為你“填充”型別的其餘部分。相反,它會靜默地用 dynamic(或邊界,如果類有邊界)填充任何缺失的型別引數。這很少是你想要的。

相反,如果你在型別註釋中或在某個呼叫中將泛型型別作為型別引數寫入,請確保寫入一個完整的型別:

好的示例dart
List<num> numbers = [1, 2, 3];
var completer = Completer<Map<String, int>>();

DO 使用 dynamic 進行註釋,而不是讓推斷失敗

#

當推斷未能填充型別時,通常會預設使用 dynamic。如果 dynamic 是你想要的型別,這在技術上是最簡潔的方式來實現它。然而,這並不是最“清晰”的方式。隨手閱讀你程式碼的人看到缺少註釋,無法知道你是打算使用 dynamic,期望推斷填充其他型別,還是僅僅忘記編寫註釋。

dynamic 是您想要的型別時,請顯式寫出,以明確您的意圖並突出顯示此程式碼的靜態安全性較低。

好的示例dart
dynamic mergeJson(dynamic original, dynamic changes) => ...
不好的示例dart
mergeJson(original, changes) => ...

請注意,當 Dart“成功”推斷出 dynamic 時,可以省略型別。

好的示例dart
Map<String, dynamic> readJson() => ...

void printUsers() {
  var json = readJson();
  var users = json['users'];
  print(users);
}

在這裡,Dart 為 json 推斷出 Map<String, dynamic>,然後從中為 users 推斷出 dynamic。可以不為 users 新增型別註釋。區別有點微妙。允許推斷從其他地方的 dynamic 型別註釋將 dynamic“傳播”到你的程式碼是可以的,但你不希望它在你未指定型別的地方注入 dynamic 型別註釋。

例外:可以省略未使用引數(_)的型別註釋。

PREFER 函式型別註釋中的簽名

#

識別符號 Function 本身不帶任何返回型別或引數簽名,指的是特殊的 Function 型別。這種型別只比使用 dynamic 稍微有用一點。如果你要註釋,最好使用包含函式引數和返回型別的完整函式型別。

好的示例dart
bool isValid(String value, bool Function(String) test) => ...
不好的示例dart
bool isValid(String value, Function test) => ...

例外: 有時,您想要一個表示多種不同函式型別聯合的型別。例如,您可能接受一個接受一個引數的函式或一個接受兩個引數的函式。由於我們沒有聯合型別,無法精確地為此型別化,通常您必須使用 dynamicFunction 至少比這稍微有用一些:

好的示例dart
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.');
    }
  }
}

DON'T 為 setter 指定返回型別

#

Linter 規則:avoid_return_types_on_setters

Dart 中的 setter 總是返回 void。寫出這個詞是毫無意義的。

不好的示例dart
void set foo(Foo value) {
   ...
}
好的示例dart
set foo(Foo value) {
   ...
}

DON'T 使用遺留的 typedef 語法

#

Linter 規則:prefer_generic_function_type_aliases

Dart 有兩種定義函式型別的具名 typedef 的表示法。原始語法如下所示:

不好的示例dart
typedef int Comparison<T>(T a, T b);

這種語法存在一些問題:

  • 沒有辦法為“泛型”函式型別指定名稱。在上面的示例中,typedef 本身是泛型的。如果在程式碼中引用 Comparison 時沒有型別引數,您將隱式獲得函式型別 int Function(dynamic, dynamic),而“不是” int Function<T>(T, T)。這在實踐中不常出現,但在某些角落情況下很重要。

  • 引數中的單個識別符號被解釋為引數的“名稱”,而不是其“型別”。給定:

    不好的示例dart
    typedef bool TestNumber(num);

    大多數使用者期望這是一個接受 num 並返回 bool 的函式型別。實際上,這是一個接受“任何”物件(dynamic)並返回 bool 的函式型別。引數的“名稱”(在 typedef 中除了文件之外不用於任何目的)是“num”。這在 Dart 中一直是長期存在的錯誤來源。

新的語法如下所示:

好的示例dart
typedef Comparison<T> = int Function(T, T);

如果您想包含引數名稱,也可以:

好的示例dart
typedef Comparison<T> = int Function(T a, T b);

新語法可以表達舊語法可以表達的任何內容,甚至更多,並且沒有將單個識別符號視為引數名稱而不是其型別的容易出錯的特性。typedef 中 = 後面的相同函式型別語法也允許出現在任何型別註釋可能出現的地方,這為我們在程式中的任何地方編寫函式型別提供了一種一致的方式。

舊的 typedef 語法仍然受支援,以避免破壞現有程式碼,但它已被棄用。

PREFER 行內函數型別而非 typedef

#

Linter 規則:avoid_private_typedef_functions

在 Dart 中,如果你想將函式型別用於欄位、變數或泛型型別引數,可以為函式型別定義 typedef。但是,Dart 支援行內函數型別語法,可以在允許型別註釋的任何地方使用:

好的示例dart
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。但在大多數情況下,使用者希望在函式型別實際使用的地方看到它是什麼,並且函式型別語法提供了這種清晰性。

PREFER 為引數使用函式型別語法

#

Linter 規則:use_function_type_syntax_for_parameters

Dart 在定義型別為函式的引數時有一個特殊的語法。有點像 C,你用函式的返回型別和引數簽名圍繞引數名稱:

dart
Iterable<T> where(bool predicate(T element)) => ...

在 Dart 新增函式型別語法之前,這是為引數指定函式型別而不定義 typedef 的唯一方法。現在 Dart 有一個通用的函式型別表示法,你也可以將其用於函式型別引數:

好的示例dart
Iterable<T> where(bool Function(T) predicate) => ...

新語法稍微冗長一些,但與必須使用新語法的其他位置保持一致。

AVOID 使用 dynamic,除非你想停用靜態檢查

#

有些操作適用於任何可能的物件。例如,log() 方法可以接受任何物件並對其呼叫 toString()。Dart 中有兩種型別允許所有值:Object?dynamic。然而,它們傳達不同的意思。如果你只想宣告你允許所有物件,使用 Object?。如果你想允許所有物件“除了” null,則使用 Object

dynamic 型別不僅接受所有物件,還允許所有“操作”。在編譯時,對 dynamic 型別的值進行任何成員訪問都是允許的,但在執行時可能會失敗並丟擲異常。如果你想要這種有風險但靈活的動態分派,那麼 dynamic 是正確的型別。

否則,最好使用 Object?Object。在訪問成員之前,依靠 is 檢查和型別提升來確保值的執行時型別支援您要訪問的成員。

好的示例dart
/// 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.');
}

此規則的主要例外是在使用現有 API 時,尤其是泛型型別內部的 API,它們使用 dynamic。例如,JSON 物件的型別是 Map<String, dynamic>,你的程式碼需要接受相同的型別。即便如此,在使用這些 API 的值時,最好在訪問成員之前將其轉換為更精確的型別。

DO 對於不產生值的非同步成員,使用 Future<void> 作為返回型別

#

當有一個不返回值的同步函式時,你使用 void 作為返回型別。對於不產生值但呼叫者可能需要 await 的非同步方法的等價型別是 Future<void>

您可能會看到使用 FutureFuture<Null> 的程式碼,因為早期版本的 Dart 不允許將 void 作為型別引數。現在可以了,您應該使用它。這樣做更直接地匹配您如何為類似的同步函式指定型別,併為呼叫者和函式體提供更好的錯誤檢查。

對於不返回有用值且沒有呼叫者需要等待非同步工作或處理非同步失敗的非同步函式,使用 void 作為返回型別。

AVOID 使用 FutureOr<T> 作為返回型別

#

如果一個方法接受 FutureOr<int>,它在“接受”方面是“慷慨”的 (健壯性原則)。使用者可以使用 intFuture<int> 呼叫該方法,因此他們不需要將 int 包裝在您反正要解包的 Future 中。

如果你“返回”一個 FutureOr<int>,使用者在執行任何有用的操作之前需要檢查是獲得了 int 還是 Future<int>。(或者他們只會 await 該值,有效地總是將其視為 Future。)只需返回一個 Future<int>,這樣更簡潔。使用者更容易理解函式要麼總是非同步,要麼總是同步,而一個既可以是非同步又可以是同步的函式則很難正確使用。

好的示例dart
Future<int> triple(FutureOr<int> value) async => (await value) * 3;
不好的示例dart
FutureOr<int> triple(FutureOr<int> value) {
  if (value is int) return value * 3;
  return value.then((v) => v * 3);
}

此指導原則更精確的表述是“僅在 逆變 位置使用 FutureOr<T>”。引數是逆變的,返回型別是協變的。在巢狀函式型別中,這會反轉——如果您的引數型別本身是一個函式,則回撥的返回型別現在處於逆變位置,回撥的引數是協變的。這意味著回撥的型別返回 FutureOr<T> 是可以的:

好的示例dart
Stream<S> asyncMap<T, S>(
  Iterable<T> iterable,
  FutureOr<S> Function(T) callback,
) async* {
  for (final element in iterable) {
    yield await callback(element);
  }
}

引數

#

在 Dart 中,可選引數可以是位置引數或具名引數,但不能同時是兩者。

AVOID 位置布林引數

#

Linter 規則:avoid_positional_boolean_parameters

與其他型別不同,布林值通常以字面形式使用。像數字這樣的值通常被封裝在命名常量中,但我們通常直接傳遞 truefalse。如果布林值代表的含義不明確,這可能會使呼叫點難以閱讀。

不好的示例dart
new Task(true);
new Task(false);
new ListBox(false, true, true);
new Button(false);

而是優先使用命名引數、命名建構函式或命名常量來闡明呼叫的作用。

好的示例dart
Task.oneShot();
Task.repeating();
ListBox(scroll: true, showScrollbars: true);
Button(ButtonState.enabled);

請注意,這不適用於設定器(setters),在設定器中,其名稱已經清楚地表明瞭值所代表的含義。

好的示例dart
listBox.canScroll = true;
button.isEnabled = false;

AVOID 可選位置引數,如果使用者可能想省略前面的引數

#

可選的位置引數應具有邏輯上的遞進關係,以便靠前的引數比靠後的引數更常被傳遞。使用者幾乎不應該需要為了傳遞靠後的位置引數而顯式傳遞一個“空洞”來省略靠前的引數。在這種情況下,最好使用命名引數。

好的示例dart
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,
});

AVOID 接受特殊“無引數”值的強制引數

#

如果使用者在邏輯上省略一個引數,最好透過將該引數設為可選來讓他們真正省略它,而不是強制他們傳遞 null、空字串或表示“未傳遞”的其他特殊值。

省略引數更加簡潔,並有助於防止出現錯誤,例如使用者以為自己在提供一個實際值時卻意外傳遞了 null 這樣的標記值。

好的示例dart
var rest = string.substring(start);
不好的示例dart
var rest = string.substring(start, null);

DO 使用包含起始和不包含結束引數來接受範圍

#

如果你正在定義一個方法或函式,允許使用者從某個整數索引序列中選擇一個範圍內的元素或項,請接受一個起始索引(指向第一個項)和一個(可能是可選的)結束索引(比最後一個項的索引大一)。

這與執行相同操作的核心庫保持一致。

好的示例dart
[0, 1, 2, 3].sublist(1, 3) // [1, 2]
'abcd'.substring(1, 3) // 'bc'

在這裡保持一致尤其重要,因為這些引數通常是未命名的。如果你的 API 接受長度而不是結束點,那麼這種差異在呼叫點將完全不可見。

相等性

#

為類實現自定義的相等性(equality)行為可能很棘手。使用者對相等性的工作方式有很深的直覺,你的物件需要匹配這種直覺,並且像雜湊表這樣的集合型別有一些微妙的契約(contracts),它們期望元素遵循這些契約。

如果你覆寫(override)了 == 運算子,請 DO 同時覆寫 hashCode

#

Linter 規則:hash_and_equals

預設的雜湊碼(hash code)實現提供了身份雜湊(identity hash)——兩個物件通常只有在它們是完全相同的物件時才擁有相同的雜湊碼。同樣,== 的預設行為也是身份比較。

如果你覆寫 ==,這意味著你的類可能存在被認為是“相等”的不同物件。任何兩個相等的物件都必須擁有相同的雜湊碼。否則,對映(maps)和其他基於雜湊的集合將無法識別這兩個物件是等價的。

請 DO 讓你的 == 運算子遵守數學上的相等性規則

#

等價關係應該滿足:

  • 自反性(Reflexive): a == a 應始終返回 true

  • 對稱性(Symmetric): a == b 的返回值應與 b == a 相同。

  • 傳遞性(Transitive): 如果 a == bb == c 都返回 true,那麼 a == c 也應該返回 true

使用者和使用 == 的程式碼都期望這些規則得到遵守。如果你的類不能遵守這些規則,那麼 == 就不是你試圖表達的操作的正確名稱。

AVOID 為可變類定義自定義相等性

#

Linter 規則:avoid_equals_and_hash_code_on_mutable_classes

當你定義 == 時,你還必須定義 hashCode。這兩者都應考慮物件的欄位。如果這些欄位發生變化,那麼物件的雜湊碼也可能隨之變化。

大多數基於雜湊的集合並未預料到這一點——它們假設物件的雜湊碼將永遠保持不變,如果事實並非如此,它們的行為可能會變得不可預測。

DON'T 將 == 的引數設為可空(nullable)

#

Linter 規則:avoid_null_checks_in_equality_operators

語言規範規定 null 只等於它自身,並且只有在右側不為 null 時才會呼叫 == 方法。

好的示例dart
class Person {
  final String name;

  // ···

  bool operator ==(Object other) => other is Person && name == other.name;
}
不好的示例dart
class Person {
  final String name;

  // ···

  bool operator ==(Object? other) =>
      other != null && other is Person && name == other.name;
}