跳至主要內容
目錄

Effective Dart:設計

目錄 keyboard_arrow_down keyboard_arrow_up
more_horiz

以下是一些關於為函式庫編寫一致、可用的 API 的指南。

名稱

#

命名是編寫可讀、可維護程式碼的重要環節。以下最佳做法可協助您達成此目標。

務必一致地使用術語

#

在整個程式碼中,針對相同的物件使用相同的名稱。若在您的 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 的其他部分。藉由以此為基礎,您可以減少他們在發揮生產力之前必須學習的新知識量。

避免縮寫

#

除非縮寫比非縮寫詞彙更常見,否則請勿縮寫。若要縮寫,請正確地將其大寫

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

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

#

最後一個字詞應最能描述物件的本質。您可以在其前面加上其他字詞 (例如形容詞) 以進一步描述物件。

良好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.

考慮讓程式碼讀起來像個句子

#

若對命名感到猶豫,請編寫一些使用您 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);

偏好為非布林屬性或變數使用名詞詞組

#

讀者的重點在於屬性「是什麼」。若使用者更在意屬性「如何」決定,則它可能應該是一個具有動詞詞組名稱的方法。

良好dart
list.length
context.lineWidth
quest.rampagingSwampBeast
不佳dart
list.deleteItems

偏好為布林屬性或變數使用非祈使動詞詞組

#

布林名稱通常在控制流程中用作條件,因此您會希望名稱在那裡讀起來順暢。請比較

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.

考慮為具名的布林「參數」省略動詞

#

這完善了先前的規則。對於屬於布林值的具名參數,即使沒有動詞,名稱通常也一樣清楚,而且程式碼在呼叫點讀起來更順暢。

良好dart
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 代表的兩種情況中的哪一種 (以及屬性命名的情況) 時,偏好肯定或更基本的情況。布林成員通常會巢狀於邏輯運算式 (包括否定運算子) 內。若您的屬性本身讀起來像是否定,讀者會更難以在心裡執行雙重否定並理解程式碼的含義。

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

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

例外狀況:對於某些屬性,使用者絕大多數需要使用否定形式。選擇肯定情況會迫使他們到處都使用 ! 來否定屬性。相反地,為該屬性使用否定情況可能會更好。

偏好為主要目的是副作用的函式或方法使用祈使動詞詞組

#

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

這些成員應使用祈使動詞詞組命名,以釐清成員執行的工作。

良好dart
list.add('element');
queue.removeFirst();
window.refresh();

這樣一來,調用讀起來就像執行該工作的命令。

若傳回值是函式或方法的主要目的,則偏好使用名詞詞組或非祈使動詞詞組

#

其他可調用成員的副作用很少,但會將有用的結果傳回呼叫者。若成員不需要任何參數即可執行此操作,則它通常應該是一個 getter。但有時邏輯「屬性」需要一些參數。例如,elementAt() 會從集合中傳回一筆資料,但它需要一個參數才能知道要傳回哪一筆資料。

這表示成員在語法上是一個方法,但在概念上它是一個屬性,並且應該如此命名,使用一個描述成員傳回「什麼」的詞組。

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

此指南刻意比前一個指南更寬鬆。有時方法沒有副作用,但仍以動詞詞組命名更簡單,例如 list.take()string.split()

若想讓使用者注意到函式或方法執行的工作,請考慮使用祈使動詞詞組

#

當成員產生結果而沒有任何副作用時,它通常應該是一個 getter 或一個具有名詞詞組名稱的方法,用以描述它傳回的結果。然而,有時產生該結果所需的工作很重要。它可能容易發生執行階段失敗,或使用網路或檔案 I/O 等耗用大量資源的資源。在這種情況下,若您希望呼叫者思考成員正在執行的工作,請為成員提供一個動詞詞組名稱,以描述該工作。

良好dart
var table = database.downloadData();
var packageVersions = packageGraph.solveConstraints();

不過請注意,此指南比前兩個指南更寬鬆。作業執行的工作通常是與呼叫者無關的實作細節,且效能和穩健性界限會隨時間而改變。大多數時候,請根據成員為呼叫者做「什麼」來命名成員,而不是「如何」做。

避免以 get 開頭命名方法

#

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

即使成員確實需要是一個方法,因為它需要引數,或因為其他原因而不適合作為 getter,您仍然應該避免使用 get。如同先前的指南所述,您可以

  • 只需捨棄 get使用名詞詞組名稱,例如 breakfastOrder(),若呼叫者主要關心方法傳回的值。

  • 使用動詞詞組名稱,若呼叫者關心正在執行的工作,但選擇一個比 get 更精確描述工作的動詞,例如 createdownloadfetchcalculaterequestaggregate 等。

若方法將物件的狀態複製到新物件,則偏好將方法命名為 to___()

#

Linter 規則:use_to_and_as_if_applicable

「轉換」方法是一種傳回新物件的方法,其中包含接收器幾乎所有狀態的複本,但通常採用不同的形式或表示法。核心函式庫有一個慣例,這些方法會以 to 開頭命名,後面接著結果的種類。

若您定義轉換方法,遵循該慣例會很有幫助。

良好dart
list.toSet();
stackTrace.toString();
dateTime.toLocal();

若方法傳回由原始物件支援的不同表示法,則偏好將方法命名為 as___()

#

Linter 規則:use_to_and_as_if_applicable

轉換方法是「快照」。產生的物件具有原始物件狀態的自身複本。還有其他類似轉換的方法會傳回「檢視」—它們提供一個新物件,但該物件會參照回原始物件。稍後對原始物件所做的變更會反映在檢視中。

您應遵循的核心函式庫慣例是 as___()

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

避免在函式或方法的名稱中描述參數

#

使用者會在呼叫點看到引數,因此在名稱本身也參照它通常無助於提高可讀性。

良好dart
list.add(element);
map.remove(key);
不佳dart
list.addElement(element)
map.removeKey(key)

然而,提及參數可用於將其與其他名稱相似但採用不同類型的方法區分開來

良好dart
map.containsKey(key);
map.containsValue(value);

命名類型參數時,務必遵循現有的助記符號慣例

#

單字母名稱並非完全具有啟發性,但幾乎所有泛型類型都會使用它們。幸運的是,它們大多以一致、助記的方式使用它們。慣例如下

  • E 代表集合中的「元素」(element) 類型

    良好dart
    class IterableBase<E> {}
    class List<E> {}
    class HashSet<E> {}
    class RedBlackTree<E> {}
  • KV 代表關聯式集合中的「鍵」(key) 和「值」(value) 類型

    良好dart
    class Map<K, V> {}
    class Multimap<K, V> {}
    class MapEntry<K, V> {}
  • R 代表用作函式或類別方法「傳回」(return) 類型的類型。這並不常見,但有時會出現在 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 = [];
}

實際上,現有的慣例涵蓋了大多數類型參數。

函式庫

#

前導底線字元 ( _ ) 表示成員對其函式庫是私有的。這不僅僅是慣例,而是內建於語言本身。

偏好將宣告設為私有

#

函式庫中的公開宣告 (頂層或類別中) 表示其他函式庫可以且應該存取該成員。這也是您的函式庫承諾支援該成員並在發生時正常運作。

若這不是您的本意,請加上小小的 _ 並感到高興。狹窄的公開介面更易於您維護,也更易於使用者學習。作為一個額外的好處,分析器會告訴您未使用的私有宣告,以便您可以刪除無效程式碼。若成員是公開的,則分析器無法執行此操作,因為它不知道其檢視範圍之外是否有任何程式碼正在使用它。

考慮在同一個函式庫中宣告多個類別

#

某些語言 (例如 Java) 將檔案的組織與類別的組織連結在一起—每個檔案可能只定義一個頂層類別。Dart 沒有這種限制。函式庫是與類別不同的獨立實體。若多個類別、頂層變數和函式在邏輯上都屬於一起,則單一函式庫包含它們是完全沒問題的。

將多個類別放在同一個函式庫中可以啟用一些有用的模式。由於 Dart 中的私有性在函式庫層級而非類別層級運作,因此這是一種定義「友元」類別的方式,就像您在 C++ 中可能做的那樣。在同一個函式庫中宣告的每個類別都可以存取彼此的私有成員,但該函式庫之外的程式碼則不行。

當然,此指南並不表示您「應該」將所有類別都放在一個龐大的單體函式庫中,只是您「可以」在單一函式庫中放置多個類別。

類別和 Mixins (混入)

#

Dart 是一種「純粹」的物件導向語言,因為所有物件都是類別的執行個體。但 Dart 並不要求所有程式碼都必須在類別內定義—您可以像在程序式或函式語言中一樣定義頂層變數、常數和函式。

當簡單的函式即可完成時,避免定義單一成員的抽象類別

#

Linter 規則:one_member_abstracts

與 Java 不同,Dart 具有第一級函式、閉包和用於它們的簡潔語法。若您只需要類似回呼函式的東西,只需使用函式即可。若您正在定義一個類別,且它只有一個名稱無意義的抽象成員 (例如 callinvoke),則您很可能只是想要一個函式。

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

避免定義僅包含靜態成員的類別

#

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';
}

避免擴充不打算作為子類別的類別

#

若建構子從生成式建構子變更為工廠建構子,則任何呼叫該建構子的子類別建構子都會中斷。此外,若類別變更了它在 this 上調用的自身方法,則可能會中斷覆寫這些方法並期望在特定時間點呼叫它們的子類別。

這兩者都表示類別需要審慎考慮是否要允許子類別化。這可以透過文件註解傳達,或為類別提供一個明顯的名稱 (例如 IterableBase)。若類別的作者沒有這樣做,最好假設您「不應」擴充類別。否則,稍後對其所做的變更可能會中斷您的程式碼。

務必使用類別修飾詞來控制您的類別是否可擴充

#

類別修飾詞 (例如 finalinterfacesealed) 限制了類別的擴充方式。例如,使用 final class A {}interface class B {} 來防止在目前函式庫外部進行擴充。使用這些修飾詞來傳達您的意圖,而不是依賴文件。

避免實作不打算作為介面的類別

#

隱含介面是 Dart 中一個強大的工具,可避免在可以從合約實作的簽名中輕鬆推斷類別的合約時,必須重複該合約。

但實作類別的介面是與該類別非常緊密的耦合。這表示幾乎對您正在實作其介面的類別所做的「任何」變更都會中斷您的實作。例如,將新成員新增至類別通常是安全的、非破壞性的變更。但若您正在實作該類別的介面,則您的類別現在會發生靜態錯誤,因為它缺少該新方法的實作。

函式庫維護者需要能夠演進現有類別,而不會中斷使用者。若您將每個類別都視為公開使用者可以自由實作的介面,則變更這些類別會變得非常困難。這種困難反過來表示您依賴的函式庫成長和適應新需求的步調會變慢。

為了讓您使用的類別作者有更多餘地,請避免實作隱含介面,除非類別顯然打算被實作。否則,您可能會引入作者不打算建立的耦合,而且他們可能會在沒有意識到的情況下中斷您的程式碼。

務必使用類別修飾詞來控制您的類別是否可作為介面

#

設計函式庫時,請使用類別修飾詞 (例如 finalbasesealed) 來強制執行預期的用法。例如,使用 final class C {}base class D{} 來防止在目前函式庫外部進行實作。雖然理想情況是所有函式庫都使用這些修飾詞來強制執行設計意圖,但開發人員可能仍會遇到未套用它們的情況。在這種情況下,請注意非預期的實作問題。

偏好定義純 mixin 或純 class,而非 mixin class

#

Linter 規則:prefer_mixin

Dart 先前 (語言版本 2.122.19) 允許將任何符合特定限制 (沒有非預設建構子、沒有父類別等) 的類別混入其他類別。這令人困惑,因為類別的作者可能不打算將其混入。

Dart 3.0.0 現在要求任何打算混入其他類別的類型,以及被視為一般類別的類型,都必須使用 mixin class 宣告明確宣告為此類型。

然而,需要同時作為 mixin 和類別的類型應該是罕見的情況。mixin class 宣告主要旨在協助將 3.0.0 之前的類別 (用作 mixin) 遷移到更明確的宣告。新程式碼應僅使用純 mixin 或純類別宣告來清楚定義其宣告的行為和意圖,並避免 mixin 類別的模糊性。

請參閱「將類別遷移為 Mixin」以取得關於 mixin 和 mixin class 宣告的更多指引。

建構子

#

Dart 建構子是透過宣告與類別同名且可選地帶有額外識別碼的函式來建立的。後者稱為具名建構子。

若類別支援,請考慮將建構子設為 const

#

若您的類別中所有欄位都是 final,且建構子只執行初始化它們的操作,則您可以將該建構子設為 const。這可讓使用者在需要常數的位置 (在其他更大的常數、switch 語句、預設參數值等內部) 建立類別的執行個體。

若您未明確將其設為 const,他們就無法這樣做。

請注意,const 建構式是您公開 API 中的一種承諾。如果您稍後將建構式變更為非 const,則會破壞在常數表達式中呼叫它的使用者。如果您不想承諾這一點,就不要將其設為 const。實際上,const 建構式最適合用於簡單、不可變的值類型的型別。

成員

#

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

偏好將欄位和頂層變數設為 final

#

Linter 規則:prefer_final_fields

可變(不會隨時間改變)的狀態更容易讓程式設計師理解。盡可能減少使用可變狀態的類別和程式庫往往更容易維護。當然,擁有可變資料通常很有用。但是,如果您不需要它,您的預設值應該是盡可能將欄位和頂層變數設為 final

有時,實例欄位在初始化後不會改變,但在實例建構完成後才能初始化。例如,它可能需要參考 this 或實例上的其他欄位。在這種情況下,請考慮將欄位設為 late final。當您這樣做時,您也可能可以在其宣告時初始化欄位

對於概念上會存取屬性的作業,務必使用 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 或清單是很常見且完全可以接受的。重要的是 future 完成後的值相同,並且清單包含相同的元素。

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

    不佳dart
    DateTime.now; // New result each time.
  • 結果物件不會公開原始物件的所有狀態。 欄位僅公開物件的一部分。如果您的操作傳回的結果公開了原始物件的完整狀態,那麼最好將其作為 to___()as___() 方法。

如果以上所有描述都適用於您的操作,則它應該是一個 getter。看起來似乎很少有成員能夠通過這種考驗,但令人驚訝的是,許多成員都通過了。許多操作只是對某些狀態進行一些計算,而其中大多數可以而且應該是 getter。

良好dart
rectangle.area;
collection.isEmpty;
button.canShow;
dataSet.minimumValue;

對於概念上會變更屬性的作業,務必使用 setter

#

Linter 規則:use_setters_to_change_properties

決定 setter 與方法之間的區別,類似於決定 getter 與方法之間的區別。在這兩種情況下,操作都應該是「類似欄位的」。

對於 setter,「類似欄位的」表示

  • 該操作接受單一引數,且不產生結果值。

  • 該操作會變更物件中的某些狀態。

  • 該操作是等冪的。 使用相同的值兩次呼叫相同的 setter,就呼叫者而言,第二次呼叫應該不做任何事情。在內部,也許您正在進行一些快取失效或記錄。這沒關係。但從呼叫者的角度來看,第二次呼叫似乎沒有做任何事情。

良好dart
rectangle.width = 3;
button.visible = false;

請勿在沒有對應 getter 的情況下定義 setter

#

Linter 規則:avoid_setters_without_getters

使用者將 getter 和 setter 視為物件的 visible properties(可見屬性)。一個可以寫入但看不到的「dropbox」屬性會讓人感到困惑,並混淆他們對屬性如何運作的直覺。例如,沒有 getter 的 setter 表示您可以使用 = 來修改它,但不能使用 +=

本指南並表示您應該新增 getter 只是為了允許您想要新增的 setter。物件通常不應公開超過其需要的狀態。如果您物件的某些狀態可以修改,但不能以相同的方式公開,請改用方法。

避免使用執行階段類型測試來偽造多載

#

API 通常支援對不同類型的參數執行類似的操作。為了強調相似性,某些語言支援多載,這允許您定義多個具有相同名稱但參數清單不同的方法。在編譯時,編譯器會查看實際的引數類型以確定要呼叫哪個方法。

Dart 沒有多載。您可以定義一個看起來像多載的 API,方法是定義一個單一方法,然後在主體內使用 is 類型測試來查看引數的執行階段類型並執行適當的行為。但是,以這種方式偽造多載會將編譯時方法選擇變成在執行階段發生的選擇。

如果呼叫者通常知道他們擁有的類型以及他們想要的特定操作,則最好定義具有不同名稱的單獨方法,以讓呼叫者選擇正確的操作。由於避免了任何執行階段類型測試,因此可以提供更好的靜態類型檢查和更快的效能。

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

避免使用沒有初始設定式的公開 late final 欄位

#

與其他 final 欄位不同,沒有初始設定式的 late final 欄位確實定義了一個 setter。如果該欄位是公開的,則 setter 是公開的。這很少是您想要的。欄位通常標記為 late,以便它們可以在實例生命週期的某個時間點在內部初始化,通常在建構式主體內。

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

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

避免傳回可為 null 的 FutureStream 和集合類型

#

當 API 傳回容器類型時,它有兩種方式來指示缺少資料:它可以傳回空容器,也可以傳回 null。使用者通常假設並偏好您使用空容器來指示「沒有資料」。這樣,他們就有一個真實的物件,他們可以在其上呼叫方法,例如 isEmpty

為了指示您的 API 沒有資料可提供,請偏好傳回空集合、可為 null 類型的非 null future,或不發出任何值的 stream。

例外情況: 如果傳回 null 表示與產生空容器不同的含義,則使用可為 null 的類型可能是有意義的。

避免從方法傳回 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 是您想要的類型,也要進行註解。

  • 除非您需要,否則不要註解區域變數和泛型調用。

  • 除非初始設定式使類型顯而易見,否則偏好註解頂層變數和欄位。

務必為沒有初始設定式的變數加上類型註解

#

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;
}

若類型不明顯,務必為欄位和頂層變數加上類型註解

#

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 上的類型註解有助於程式碼的使用者一樣,私有成員上的類型有助於維護者

請勿多餘地為已初始化的區域變數加上類型註解

#

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;
}

務必在函式宣告中為傳回類型加上註解

#

與其他一些語言不同,Dart 通常不會從函數宣告的主體推斷其傳回類型。這表示您應該自己為傳回類型寫入類型註解。

良好dart
String makeGreeting(String who) {
  return 'Hello, $who!';
}
不佳dart
makeGreeting(String who) {
  return 'Hello, $who!';
}

請注意,本指南僅適用於非區域函數宣告:頂層、靜態和實例方法以及 getter。區域函數和匿名函數表達式會從其主體推斷傳回類型。實際上,匿名函數語法甚至不允許傳回類型註解。

務必在函式宣告中為參數類型加上註解

#

函數的參數清單決定了它與外界的邊界。註解參數類型使該邊界得到明確定義。請注意,即使預設參數值看起來像變數初始設定式,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);
  }
}

例外情況: 函數表達式和初始化形式參數具有不同的類型註解慣例,如下兩個指南中所述。

請勿為函式運算式中推論的參數類型加上註解

#

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);

如果語言能夠為函數表達式中的參數推斷出您想要的類型,則不要註解。在極少數情況下,周圍的上下文不夠精確,無法為函數的一個或多個參數提供類型。在這些情況下,您可能需要註解。(如果函數不是立即使用,通常最好將其設為具名宣告。)

請勿為初始化形式參數加上類型註解

#

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});
}

務必在未推論的泛型調用中寫入類型引數

#

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();
}

註解變數也可以解決此指南,因為現在類型引數推斷出來的。

請勿在已推論的泛型調用中寫入類型引數

#

這與先前的規則相反。如果調用的類型引數清單使用您想要的類型正確推斷出來,則省略類型並讓 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]);

在這裡,集合和實例的類型可以從它們的元素和引數自下而上地推斷出來。

避免寫入不完整的泛型類型

#

寫入類型註解或類型引數的目標是確定完整的類型。但是,如果您寫入泛型類型的名稱但省略其類型引數,則您尚未完全指定類型。在 Java 中,這些稱為「原始類型」。例如

不佳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>>();

執行 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 類型註解。

例外情況:未使用參數 (_) 上的類型註解可以省略。

偏好在函式類型註解中使用簽名

#

僅憑識別碼 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.');
    }
  }
}

請勿為 setter 指定傳回類型

#

Linter 規則:avoid_return_types_on_setters

在 Dart 中,Setter 始終傳回 void。寫入這個詞是沒有意義的。

不佳dart
void set foo(Foo value) {
   ...
}
良好dart
set foo(Foo value) {
   ...
}

請勿使用舊版的 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 語法,以避免破壞現有程式碼,但它已被棄用。

偏好使用內聯函式類型而非 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 可能仍然值得。但在大多數情況下,使用者希望在函數類型實際使用的地方看到函數類型是什麼,而函數類型語法為他們提供了這種清晰度。

偏好為參數使用函式類型語法

#

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) => ...

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

避免使用 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.');
}

此規則的主要例外情況是與使用 dynamic 的現有 API 合作時,尤其是在泛型類型內部。例如,JSON 物件的類型為 Map<String, dynamic>,您的程式碼將需要接受相同的類型。即便如此,當使用來自這些 API 之一的值時,通常最好先將其轉換為更精確的類型,然後再存取成員。

執行 Future<void> 作為不產生值的非同步成員的傳回類型

#

當您有一個不傳回值的同步函數時,您可以使用 void 作為傳回類型。對於不產生值但呼叫者可能需要等待的方法,非同步等效項是 Future<void>

您可能會看到使用 FutureFuture<Null> 的程式碼,因為舊版本的 Dart 不允許將 void 作為類型引數。現在可以了,您應該使用它。這樣做更直接地符合您對類似同步函數進行定型別的方式,並為呼叫者和函數主體提供更好的錯誤檢查。

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

避免使用 FutureOr<T> 作為傳回類型

#

如果一個方法接受 FutureOr<int>,則它在接受的內容方面是慷慨的。使用者可以使用 intFuture<int> 呼叫該方法,因此他們不需要將 int 包裝在您無論如何都要解包的 Future 中。

如果您傳回 FutureOr<int>,使用者需要在執行任何有用的操作之前檢查是否取得 intFuture<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 中,可選參數可以是位置參數或具名參數,但不能同時是兩者。

避免位置布林參數

#

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);

請注意,這不適用於 setter,在 setter 中,名稱清楚地表明了該值代表什麼

良好dart
listBox.canScroll = true;
button.isEnabled = false;

若使用者可能想要省略較早的參數,請避免選用性位置參數

#

選用性的位置參數應具有邏輯上的順序,讓越前面的參數比後面的參數更常被傳遞。使用者幾乎不應該需要為了傳遞後面的位置引數而顯式地傳遞「空位」來省略前面的位置引數。您最好為此使用具名引數。

良好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,
});

避免接受特殊「無引數」值的必要參數

#

如果使用者在邏輯上省略了參數,最好讓他們實際省略它,方法是將參數設為選用性,而不是強迫他們傳遞 null、空字串或其他表示「未傳遞」的特殊值。

省略參數更簡潔,並有助於防止因使用者以為他們提供的是真實值,卻意外傳遞了像 null 這樣的哨兵值而導致的錯誤。

良好dart
var rest = string.substring(start);
不佳dart
var rest = string.substring(start, null);

務必使用包含起點和排除終點的參數來接受範圍

#

如果您正在定義一個方法或函式,讓使用者從某些整數索引序列中選擇元素或項目的範圍,請採用一個起始索引,它指的是第一個項目,以及一個(可能是選用性的)結束索引,它比最後一個項目的索引大一。

這與執行相同操作的核心函式庫一致。

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

在這裡保持一致性尤其重要,因為這些參數通常是未命名的。如果您的 API 採用長度而不是終點,則在呼叫端根本看不到差異。

相等性

#

為類別實作自訂的相等行為可能很棘手。使用者對於相等性如何運作有深刻的直覺,您的物件需要符合這些直覺,而且像雜湊表這樣的集合類型有微妙的契約,它們期望元素遵循。

如果您覆寫了 ==,請務必同時覆寫 hashCode

#

Linter 規則:hash_and_equals

預設的雜湊碼實作提供了一個身分雜湊—兩個物件通常只有在它們是完全相同的物件時才具有相同的雜湊碼。同樣地,== 的預設行為也是身分。

如果您正在覆寫 ==,這表示您可能有不同的物件被您的類別認為是「相等」的。任何兩個相等的物件都必須具有相同的雜湊碼。 否則,映射和其他基於雜湊的集合將無法識別這兩個物件是等價的。

務必讓您的 == 運算子遵守數學上的相等性規則

#

等價關係應該是

  • 自反性a == a 應始終返回 true

  • 對稱性a == b 應返回與 b == a 相同的結果。

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

使用者和使用 == 的程式碼期望遵循所有這些定律。如果您的類別無法遵守這些規則,那麼 == 就不是您試圖表達的操作的正確名稱。

避免為可變類別定義自訂相等性

#

Linter 規則:avoid_equals_and_hash_code_on_mutable_classes

當您定義 == 時,您也必須定義 hashCode。這兩者都應該將物件的欄位納入考量。如果這些欄位變更,則表示物件的雜湊碼可能會變更。

大多數基於雜湊的集合並未預料到這一點—它們假設物件的雜湊碼將永遠相同,如果情況並非如此,則可能會表現出不可預測的行為。

請勿使 == 的參數可為空值

#

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;
}