目錄

Effective Dart:設計

內容 keyboard_arrow_down keyboard_arrow_up
more_horiz

以下是為函式庫撰寫一致且可用的 API 的一些準則。

名稱

#

命名是撰寫可讀且可維護程式碼的重要部分。下列最佳實務範例可以協助您達成此目標。

務必一致使用術語

#

在整個程式碼中,使用相同的名稱表示相同的事物。如果您的 API 外部已存在使用者可能知道的先例,請遵循該先例。

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

目標是利用使用者已知的知識。這包括他們對問題領域本身、核心函式庫的慣例以及您自己 API 其他部分的了解。透過建立在這些基礎上,您可以減少他們在開始生產之前必須吸收的新知識量。

避免使用縮寫

#

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

gooddart
pageCount
buildRectangles
IOStream
HttpRequest
baddart
numPages    // "Num" is an abbreviation of "number (of)".
buildRects
InputOutputStream
HypertextTransferProtocolRequest

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

#

最後一個字詞應該是對事物最具描述性的字詞。您可以使用其他字詞(例如形容詞)作為前綴,進一步描述事物。

gooddart
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.
baddart
numPages                  // Not a collection of pages.
CanvasRenderingContext2D  // Not a "2D".
RuleFontFaceCss           // Not a CSS.

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

#

如果您不確定如何命名,請撰寫一些使用您的 API 的程式碼,並嘗試像句子一樣閱讀它。

gooddart
// "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);
baddart
// 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 並了解在程式碼中使用時如何「讀取」它很有幫助,但你可能做得太過頭。加入文章和其他詞性來強迫你的名稱實際上讀起來像語法正確的句子,這沒有幫助。

baddart
if (theCollectionOfErrors.isEmpty) ...

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

優先為非布林值屬性或變數使用名詞片語

#

讀者的重點在於屬性的是什麼。如果使用者更關心屬性如何被決定,那麼它可能應該是一個具有動詞短語名稱的方法。

gooddart
list.length
context.lineWidth
quest.rampagingSwampBeast
baddart
list.deleteItems

優先為布林值屬性或變數使用非命令式動詞片語

#

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

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

好的名稱往往以幾種動詞之一開頭

  • 「to be」的一種形式:isEnabledwasShownwillFire。這些是迄今為止最常見的。

  • 一個助動詞hasElementscanCloseshouldConsumemustSave

  • 一個主動動詞:ignoresInputwroteFile。這些很少見,因為它們通常是模稜兩可的。loggedResult 是個糟糕的名稱,因為它可能表示「結果是否已記錄」或「已記錄的結果」。同樣地,closingConnection 可能表示「連線是否正在關閉」或「正在關閉的連線」。當名稱只能被讀作謂詞時,允許使用主動動詞。

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

gooddart
isEmpty
hasElements
canClose
closesWindow
canShowPopup
hasShownPopup
baddart
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.

考慮省略命名布林參數的動詞

#

這改善了先前的規則。對於布林的命名參數,名稱通常在沒有動詞的情況下同樣清楚,而且程式碼在呼叫位置讀起來更好。

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

優先為布林值屬性或變數使用「正向」名稱

#

大多數布林名稱在概念上具有「肯定」和「否定」形式,其中前者感覺像是基本概念,而後者是其否定——「開啟」和「關閉」、「啟用」和「停用」等。後者名稱通常實際上有一個前綴來否定前者:「可見」和「可見」、「已連線」和「連線」、「零」和「零」。

在選擇true代表哪兩個情況之一時——因此屬性名稱代表哪個情況——優先選擇肯定或更基本的情況。布林成員通常會嵌套在邏輯表達式中,包括否定運算子。如果你的屬性本身讀起來像否定,讀者就更難在腦海中執行雙重否定並理解程式碼的含義。

gooddart
if (socket.isConnected && database.hasData) {
  socket.write(database.read());
}
baddart
if (!socket.isDisconnected && !database.isEmpty) {
  socket.write(database.read());
}

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

例外:對於某些屬性,否定形式是使用者極需要使用的形式。選擇肯定情況會迫使他們在所有地方使用!來否定屬性。相反,最好為該屬性使用否定情況。

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

#

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

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

gooddart
list.add('element');
queue.removeFirst();
window.refresh();

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

如果主要目的是傳回值,則優先為函式或方法使用名詞片語或非命令式動詞片語

#

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

這表示該成員在語法上是一個方法,但在概念上是一個屬性,而且應該使用描述成員傳回內容的片語來命名。

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

這項準則故意比前一項寬鬆。有時,一個方法沒有副作用,但仍較適合使用動詞片語來命名,例如 list.take()string.split()

如果您想強調函式或方法執行的作業,請考慮使用命令式動詞片語

#

當一個成員產生結果而沒有任何副作用時,通常應為 getter 或使用名詞片語名稱描述其傳回結果的方法。不過,有時產生該結果所需的工作很重要。它可能容易發生執行時期失敗,或使用網路或檔案 I/O 等重量級資源。在這種情況下,當您希望呼叫者思考成員正在執行的作業時,請給予成員一個描述該作業的動詞片語名稱。

gooddart
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 開頭,後接結果類型。

如果您定義轉換方法,遵循這個慣例會很有幫助。

gooddart
list.toSet();
stackTrace.toString();
dateTime.toLocal();

優先將方法命名為 as___(),如果它傳回由原始物件支援的不同表示方式

#

Linter 規則:use_to_and_as_if_applicable

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

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

gooddart
var map = table.asMap();
var list = bytes.asFloat32List();
var future = subscription.asFuture();

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

#

使用者會在呼叫位置看到引數,因此通常不建議在名稱中也參考它,以提高可讀性。

gooddart
list.add(element);
map.remove(key);
baddart
list.addElement(element)
map.removeKey(key)

不過,提到參數以區別它與其他名稱相似的、採用不同類型的同名方法會很有用

gooddart
map.containsKey(key);
map.containsValue(value);

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

#

單字母名稱並非特別具有說明性,但幾乎所有泛型類型都使用它們。幸運的是,它們大多以一致且助記的方式使用它們。慣例如下

  • E 表示集合中的元素類型

    gooddart
    class IterableBase<E> {}
    class List<E> {}
    class HashSet<E> {}
    class RedBlackTree<E> {}
  • KV 表示關聯集合中的類型

    gooddart
    class Map<K, V> {}
    class Multimap<K, V> {}
    class MapEntry<K, V> {}
  • R 表示用作函式或類別方法的傳回類型的類型。這並不常見,但有時會出現在類型定義中,以及實作訪客模式的類別中

    gooddart
    abstract class ExpressionVisitor<R> {
      R visitBinary(BinaryExpression node);
      R visitLiteral(LiteralExpression node);
      R visitUnary(UnaryExpression node);
    }
  • 否則,對具有單一類型參數且周圍類型使其意義顯而易見的泛型,請使用 TSU。這裡有多個字母,允許巢狀而不遮蔽周圍的名稱。例如

    gooddart
    class Future<T> {
      Future<S> then<S>(FutureOr<S> onValue(T value)) => ...
    }

    在此,泛型方法 then<S>() 使用 S 以避免遮蔽 Future<T> 上的 T

如果上述情況都不合適,那麼另一個單字母助記符名稱或描述性名稱就可以了

gooddart
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++ 中可能做的那樣。在同一個程式庫中宣告的每個類別都可以存取彼此的私人成員,但程式庫外部的程式碼則不能。

當然,這個準則並不表示您應該將所有類別都放入一個巨大的單體程式庫中,只是表示您可以在單一程式庫中放置多個類別。

類別和 mixin

#

Dart 是一種「純」物件導向語言,其中所有物件都是類別的實例。但是 Dart 並不要求所有程式碼都定義在類別內——您可以定義頂層變數、常數和函式,就像您在程序或函式語言中可以做的那樣。

如果可以用簡單的函式,請避免定義只有一個成員的抽象類別

#

Linter 規則:one_member_abstracts

與 Java 不同,Dart 具有第一類函式、封閉和一個友善的輕量級語法來使用它們。如果您只需要類似回呼的東西,請使用函式。如果您正在定義一個類別,並且它只有一個抽象成員,其名稱毫無意義,例如 callinvoke,那麼您很有可能只想要一個函式。

gooddart
typedef Predicate<E> = bool Function(E element);
baddart
abstract class Predicate<E> {
  bool test(E element);
}

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

#

Linter 規則:avoid_classes_with_only_static_members

在 Java 和 C# 中,每個定義必須在類別內,因此經常看到「類別」僅存在於填充靜態成員的地方。其他類別用作命名空間——一種為一堆成員提供共用前綴的方法,以將它們相互關聯或避免名稱衝突。

Dart 具有頂層函式、變數和常數,因此您不需要類別來定義某個東西。如果您想要的是命名空間,那麼程式庫更合適。程式庫支援匯入前綴和顯示/隱藏組合器。這些是強大的工具,讓您的程式碼使用者可以以最適合他們的方式處理名稱衝突。

如果函式或變數在邏輯上與類別無關,請將其放在頂層。如果您擔心名稱衝突,請給它一個更精確的名稱或將它移到可以透過前綴匯入的獨立程式庫中。

gooddart
DateTime mostRecent(List<DateTime> dates) {
  return dates.reduce((a, b) => a.isAfter(b) ? a : b);
}

const _favoriteMammal = 'weasel';
baddart
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 中,類別定義物件種類。從未實例化的類型是一種程式碼臭味。

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

gooddart
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 或純粹的 class,而非 mixin class

#

Linter 規則:prefer_mixin

Dart 先前(語言版本 2.122.19)允許符合特定限制(沒有非預設建構函式、沒有超類別等)的任何類別混入其他類別。這很令人困惑,因為類別的作者可能無意讓它被混入。

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

不過,需要同時是 mixin 和類別的類型應為罕見案例。mixin class 宣告主要用於協助將 3.0.0 之前的類別(用作 mixin)移轉至更明確的宣告。新程式碼應僅使用純粹的 mixin 或純粹的 class 宣告,清楚定義其宣告的行為和意圖,並避免混用類別的歧義。

請閱讀 將類別移轉為 mixin,以取得關於 mixinmixin class 宣告的更多指南。

建構函式

#

Dart 建構函式是透過宣告一個與類別同名的函式,以及一個額外的識別碼(如果需要)來建立的。後者稱為命名建構函式

如果類別支援,請考慮將建構函式設為 const

#

如果您有一個類別,其中所有欄位都是最終的,而建構函式只會初始化它們,您可以將該建構函式設為 const。這讓使用者可以在需要常數的地方建立您類別的實例,例如其他較大的常數、switch 案例、預設參數值等。

如果您沒有明確將其設為 const,他們就無法這麼做。

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

成員

#

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

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

#

Linter 規則:prefer_final_fields

不會變動的狀態(mutable)對程式設計師來說較容易理解。盡量減少可變狀態的類別和函式庫通常較容易維護。當然,可變資料通常很有用。但是,如果你不需要的話,預設應該將欄位和頂層變數設為 final

有時,實例欄位在初始化後不會變動,但必須在實例建構後才能初始化。例如,它可能需要參照 this 或實例中的其他欄位。在這種情況下,請考慮將欄位設為 late final。這樣一來,你也可以在宣告時初始化欄位

對於概念上存取屬性的操作,請使用 getter

#

決定成員應該是 getter 還是方法是良好 API 設計中微妙但重要的部分,因此有這條很長的準則。其他語言的文化傾向於避開 getter。他們只在操作幾乎完全像欄位時使用 getter,也就是對完全存在於物件中的狀態進行極少的計算。比這更複雜或更重大的任何東西都會在名稱後加上 () 來表示「這裡正在進行運算!」,因為 . 後面的裸名稱表示「欄位」。

Dart 不是那樣的。在 Dart 中,所有 點分隔名稱都是可能會進行運算的成員呼叫。欄位很特別,它們是語言提供的 getter 實作。換句話說,getter 在 Dart 中不是「特別慢的欄位」;欄位是「特別快的 getter」。

即使如此,選擇 getter 而非方法會向呼叫者傳遞一個重要的訊號。這個訊號大致上是說這個操作是「欄位式的」。至少在原則上,這個操作可以使用欄位來實作,就呼叫者所知。這表示

  • 這個操作不接受任何引數,並傳回一個結果。

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

    表示操作必須特別快才能成為 getter。IterableBase.lengthO(n),而且沒問題。getter 執行大量計算是沒問題的。但如果執行驚人的大量工作,您可能想要透過使其成為方法(其名稱是描述其執行的動詞)來吸引他們的注意力。

    baddart
    connection.nextIncomingMessage; // Does network I/O.
    expression.normalForm; // Could be exponential to calculate.
  • 操作沒有使用者可見的副作用。存取真實欄位不會變更物件或程式中的任何其他狀態。它不會產生輸出、寫入檔案等。getter 也不應該執行這些動作。

    「使用者可見」的部分很重要。getter 修改隱藏狀態或產生頻外副作用是沒問題的。getter 可以延遲計算並儲存其結果、寫入快取、記錄資料等。只要呼叫者不在乎副作用,這可能沒問題。

    baddart
    stdout.newline; // Produces output.
    list.clear; // Modifies object.
  • 操作是冪等「冪等」是一個奇怪的字,在此背景下,基本上表示多次呼叫操作會在每次產生相同的結果,除非在這些呼叫之間明確修改某些狀態。(顯然,如果您在呼叫之間新增元素至清單,list.length 會產生不同的結果。)

    此處的「相同結果」並不表示 getter 必須在連續呼叫時產生完全相同的物件。要求這樣做會強制許多 getter 具有脆弱的快取,這會否定使用 getter 的整個目的。getter 在每次呼叫時傳回新的未來或清單是很常見且完全沒問題的。重要的是,未來會完成為相同的值,而清單會包含相同的元素。

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

    baddart
    DateTime.now; // New result each time.
  • 結果物件不會公開原始物件的所有狀態。欄位只會公開物件的一部分。如果您的操作傳回公開原始物件整個狀態的結果,則它很可能更適合作為 to___()as___() 方法。

如果上述所有內容都描述您的操作,則它應該是 getter。看起來很少成員會通過該挑戰,但令人驚訝的是,許多成員會通過。許多操作只針對某些狀態執行一些計算,而其中大多數可以且應該成為 getter。

gooddart
rectangle.area;
collection.isEmpty;
button.canShow;
dataSet.minimumValue;

對於概念上變更屬性的操作,請使用 setter

#

Linter 規則:use_setters_to_change_properties

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

對於 setter,「類欄位」表示

  • 操作只接受一個引數,而且不會產生結果值。

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

  • 操作是冪等的。使用相同的設定值呼叫相同設定程式兩次,就呼叫者而言,第二次不應執行任何動作。在內部,您可能執行快取失效或記錄。這很好。但從呼叫者的角度來看,第二次呼叫似乎沒有執行任何動作。

gooddart
rectangle.width = 3;
button.visible = false;

不要定義沒有對應 getter 的 setter

#

Linter 規則:avoid_setters_without_getters

使用者將 getter 和 setter 視為物件的公開屬性。可以寫入但無法看到的「dropbox」屬性令人困惑,並會混淆他們對屬性運作方式的直覺。例如,沒有 getter 的 setter 表示您可以使用 = 來修改它,但不能使用 +=

此準則並非表示您應該只為了允許您要新增的 setter 而新增 getter。一般而言,物件不應公開比它們所需的更多狀態。如果您有某個物件狀態可以修改,但無法以相同方式公開,請改用方法。

避免使用執行時期類型測試來偽造重載

#

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

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

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

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

避免使用沒有初始值的公開 late final 欄位

#

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

除非您確實希望使用者呼叫 setter,否則最好選取下列其中一個解決方案

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

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

#

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

若要指出您的 API 沒有資料可提供,建議傳回空集合、可為空的類型之不可為空的未來,或不發出任何值的串流。

例外:如果傳回 null 產生空容器意義不同,使用可為空的類型可能比較合理。

避免從方法傳回 this,只為了啟用流暢介面

#

Linter 規則:avoid_returning_this

方法串接是串接方法呼叫的較佳解決方案。

gooddart
var buffer = StringBuffer()
  ..write('one')
  ..write('two')
  ..write('three');
baddart
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 自行成功找出類型。推論可能會失敗,在這種情況下,指南不認為那是推論的。

  • 如果代碼是動態的,則其靜態類型是特殊動態類型。代碼可以明確註解為動態,或可以推論出來。

換句話說,某些代碼是否註解或推論與其是動態或其他類型無關。

推論是一個強大的工具,可以省去您撰寫和閱讀顯而易見或無趣類型的麻煩。它使讀者的注意力集中在代碼本身的行為上。明確的類型也是健全、可維護代碼的關鍵部分。它們定義 API 的靜態形狀,並建立邊界來記錄和強制執行允許哪些類型的值到達程式不同的部分。

當然,推論不是魔術。有時推論會成功並選取類型,但它不是您想要的類型。常見的情況是從變數的初始化程式推論出過於精確的類型,而您打算稍後將其他類型的值指定給變數。在這種情況下,您必須明確寫出類型。

這裡的指南取得了我們在簡潔性和控制、靈活性與安全性之間找到的最佳平衡。有具體的指南涵蓋所有不同的情況,但粗略的摘要是

  • 即使您想要的類型是動態,當推論沒有足夠的內容時,請註解。

  • 除非需要,否則不要註解區域變數和一般呼叫。

  • 除非初始化程式使類型顯而易見,否則優先註解頂層變數和欄位。

請為沒有初始化項目的變數加上類型註解

#

Linter 規則:prefer_typing_uninitialized_variables

變數的類型(頂層、區域、靜態欄位或執行個體欄位)通常可以從其初始化程式推論出來。但是,如果沒有初始化程式,則推論會失敗。

gooddart
List<AstNode> parameters;
if (node is Constructor) {
  parameters = node.signature;
} else if (node is Method) {
  parameters = node.parameters;
}
baddart
var parameters;
if (node is Constructor) {
  parameters = node.signature;
} else if (node is Method) {
  parameters = node.parameters;
}

如果類型不顯而易見,請為欄位和頂層變數加上類型註解

#

Linter 規則:type_annotate_public_apis

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

baddart
install(id, destination) => ...

在此,不清楚id是什麼。字串?destination是什麼?字串或檔案物件?這個方法是同步的還是非同步的?這比較清楚

gooddart
Future<bool> install(PackageId id, String destination) => ...

在某些情況下,類型非常明顯,以至於寫出來毫無意義

gooddart
const screenWidth = 640; // Inferred as int.

「明顯」沒有明確的定義,但以下都是很好的候選者

  • 文字
  • 建構函式呼叫
  • 明確指定類型的其他常數的參照
  • 數字和字串的簡單表達式
  • 預期讀者熟悉的工廠方法,例如 int.parse()Future.wait()

如果您認為初始化器表達式(無論是什麼)足夠清楚,則可以省略註解。但如果您認為註解有助於讓程式碼更清楚,請新增一個。

有疑問時,請新增類型註解。即使類型很明顯,您可能還是希望明確註解。如果推論的類型依賴於其他函式庫中的值或宣告,您可能想要為您的宣告加上類型註解,這樣其他函式庫的變更就不會在您不知情的情況下,默默地變更您自己 API 的類型。

此規則適用於公開和私人宣告。就像 API 上的類型註解有助於使用者使用您的程式碼,私人成員上的類型有助於維護人員

不要重複為已初始化的局部變數加上類型註解

#

Linter 規則:omit_local_variable_types

局部變數,特別是在函式趨於較小的現代程式碼中,範圍非常小。省略類型會讓讀者專注於更重要的變數名稱及其初始化值。

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

有時推論的類型並非您希望變數具有的類型。例如,您可能打算稍後指定其他類型的值。在這種情況下,請為變數註解您想要的類型。

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

請為函式宣告加上回傳類型註解

#

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

gooddart
String makeGreeting(String who) {
  return 'Hello, $who!';
}
baddart
makeGreeting(String who) {
  return 'Hello, $who!';
}

請注意,此準則僅適用於命名函式宣告:頂層函式、方法和局部函式。匿名函式表達式會從其主體推論回傳類型。事實上,語法甚至不允許回傳類型註解。

請為函式宣告加上參數類型註解

#

函式的參數清單決定其與外界的界線。註解參數類型會讓該界線定義明確。請注意,即使預設參數值看起來像變數初始化器,Dart 也不會從其預設值推論出可選參數的類型。

gooddart
void sayRepeatedly(String message, {int count = 2}) {
  for (var i = 0; i < count; i++) {
    print(message);
  }
}
baddart
void sayRepeatedly(message, {count = 2}) {
  for (var i = 0; i < count; i++) {
    print(message);
  }
}

例外:函式表達式和初始化形式參數有不同的類型註解慣例,如下兩個準則所述。

不要為函式表達式加上推論出的參數類型註解

#

Linter 規則:avoid_types_on_closure_parameters

匿名函式幾乎總是立即傳遞給採用某種類型回呼的函式。當在類型化內容中建立函式表達式時,Dart 會根據預期的類型嘗試推論函式的參數類型。例如,當您將函式表達式傳遞給 Iterable.map() 時,函式的參數類型會根據 map() 預期的回呼類型推論

gooddart
var names = people.map((person) => person.name);
baddart
var names = people.map((Person person) => person.name);

如果語言能夠推論出函式表達式中參數所需的類型,則不要加上註解。在少數情況下,周遭的內容不夠精確,無法提供函式參數的類型。在這些情況下,您可能需要加上註解。(如果函式並未立即使用,通常最好讓它成為命名宣告。)

不要為初始化形式參數加上類型註解

#

Linter 規則:type_init_formals

如果建構函式參數使用 this. 來初始化欄位,或使用 super. 來轉送超參數,則參數的類型會推論為與欄位或超建構函式參數相同的類型。

gooddart
class Point {
  double x, y;
  Point(this.x, this.y);
}

class MyWidget extends StatelessWidget {
  MyWidget({super.key});
}
baddart
class Point {
  double x, y;
  Point(double this.x, double this.y);
}

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

請為未推論出的泛型呼叫寫入類型引數

#

Dart 非常聰明,可以推論出泛型呼叫中的類型引數。它會檢視表達式出現位置的預期類型,以及傳遞給呼叫的數值的類型。不過,有時這些資訊不足以完全決定類型引數。在這種情況下,請明確寫出完整的類型引數清單。

gooddart
var playerScores = <String, int>{};
final events = StreamController<Event>();
baddart
var playerScores = {};
final events = StreamController();

有時呼叫會作為變數宣告的初始化項。如果變數不是區域變數,則可以將類型引數清單放在宣告上,而不是寫在呼叫本身上。

gooddart
class Downloader {
  final Completer<String> response = Completer();
}
baddart
class Downloader {
  final response = Completer();
}

為變數加上註解也能符合這項準則,因為現在可以推論出類型引數。

不要為已推論出的泛型呼叫寫入類型引數

#

這是前一項規則的相反情況。如果呼叫的類型引數清單確實可以正確推論出您想要的類型,則省略類型,讓 Dart 為您執行這項工作。

gooddart
class Downloader {
  final Completer<String> response = Completer();
}
baddart
class Downloader {
  final Completer<String> response = Completer<String>();
}

在此,欄位上的類型註解提供了周遭的內容,用於推論初始化項中建構函式呼叫的類型引數。

gooddart
var items = Future.value([1, 2, 3]);
baddart
var items = Future<List<int>>.value(<int>[1, 2, 3]);

在此,可以從集合和執行個體的元素和引數,自下而上推論出類型。

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

#

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

baddart
List numbers = [1, 2, 3];
var completer = Completer<Map>();

在此,數字具有類型註解,但註解未提供類型參數給泛型 清單。同樣地,完成器地圖 類型參數並未完全指定。在這種情況下,Dart 不會 嘗試使用周圍的內容為您「填入」其餘的類型。相反,它會靜默地使用 動態(或類別有綁定的話,使用綁定)填入任何遺失的類型參數。這很少是你想要的。

相反,如果您在類型註解中或在某個呼叫中的類型參數中撰寫泛型類型,請務必撰寫完整的類型

gooddart
List<num> numbers = [1, 2, 3];
var completer = Completer<Map<String, int>>();

動態 註解,而不是讓推論失敗

#

當推論未填入類型時,通常會預設為 動態。如果 動態 是您想要的類型,這在技術上是獲得它的最簡潔方式。然而,這不是最 清楚 的方式。如果一位隨意閱讀您程式碼的人看到註解遺失,他們無法得知您是要將它設為 動態、預期推論會填入其他類型,還是單純忘記撰寫註解。

動態 是您想要的類型時,請明確寫出以表達您的意圖,並強調這段程式碼的靜態安全性較低。

gooddart
dynamic mergeJson(dynamic original, dynamic changes) => ...
baddart
mergeJson(original, changes) => ...

請注意,當 Dart 成功 推論出 動態 時,可以省略類型。

gooddart
Map<String, dynamic> readJson() => ...

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

在此,Dart 為 json 推論出 Map<字串,動態>,然後從中為 使用者 推論出 動態。讓 使用者 沒有類型註解是可以的。區別有點微妙。允許推論從其他地方的 動態 類型註解中將 動態 傳播 到您的程式碼中是可以的,但您不希望它在您的程式碼未指定的地方注入 動態 類型註解。

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

在函式類型註解中優先使用簽章

#

識別碼 Function 本身沒有任何回傳類型或參數簽章,是指特別的 Function 類型。此類型只比使用 dynamic 稍微有用一點。如果你要加上註解,請優先使用包含函式參數和回傳類型的完整函式類型。

gooddart
bool isValid(String value, bool Function(String) test) => ...
baddart
bool isValid(String value, Function test) => ...

例外:有時候,你想要一個代表多個不同函式類型的聯集的類型。例如,你可能會接受一個帶有一個參數的函式或一個帶有兩個參數的函式。由於我們沒有聯集類型,因此無法精確地輸入類型,你通常必須使用 dynamicFunction 至少比這更有用一點

gooddart
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.');
    }
  }
}

不要為設定器指定回傳類型

#

Linter 規則:avoid_return_types_on_setters

在 Dart 中,setter 始終回傳 void。寫這個字沒有意義。

baddart
void set foo(Foo value) { ... }
gooddart
set foo(Foo value) { ... }

不要使用舊式 typedef 語法

#

Linter 規則:prefer_generic_function_type_aliases

Dart 有兩種表示法來定義函式類型的命名 typedef。原始語法如下所示

baddart
typedef int Comparison<T>(T a, T b);

該語法有一些問題

  • 沒有辦法為泛型函式類型指定名稱。在上述範例中,typedef 本身是泛型的。如果你在程式碼中參照 Comparison,而沒有類型引數,你會隱式取得函式類型 int Function(dynamic, dynamic)而不是 int Function<T>(T, T)。這在實務上並不常發生,但在某些特殊情況下很重要。

  • 參數中的單一識別碼會被解釋為參數的名稱,而不是它的類型。假設

    baddart
    typedef bool TestNumber(num);

    大多數使用者預期這是一個函式類型,它會帶入 num 並回傳 bool。它實際上是一個函式類型,它會帶入任何物件 (dynamic) 並回傳 bool。參數的名稱 (除了在 typedef 中用於文件之外,沒有用於任何其他用途) 是「num」。這一直是 Dart 中錯誤的長期來源。

新的語法如下所示

gooddart
typedef Comparison<T> = int Function(T, T);

如果你想要包含參數的名稱,你也可以這麼做

gooddart
typedef Comparison<T> = int Function(T a, T b);

新的語法可以表達舊語法可以表達的任何內容,而且更多,並且沒有容易出錯的錯誤功能,其中單一識別碼被視為參數的名稱,而不是它的類型。typedef 中 = 之後的相同函式類型語法也允許在任何可能出現類型註解的地方,讓我們在程式中的任何地方都能使用單一一致的方式來撰寫函式類型。

舊的 typedef 語法仍受支援,以避免中斷現有程式碼,但已棄用。

優先使用內嵌函式類型,而不是 typedef

#

Linter 規則:avoid_private_typedef_functions

在 Dart 中,如果您想要對欄位、變數或泛型類型參數使用函式類型,您可以為函式類型定義 typedef。不過,Dart 支援內嵌函式類型語法,可以在允許類型註解的任何地方使用

gooddart
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 有函式類型的通用表示法,您也可以將它用於函式類型參數

gooddart
Iterable<T> where(bool Function(T) predicate) => ...

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

避免使用 dynamic,除非您想要停用靜態檢查

#

有些操作適用於任何可能的物件。例如,log() 方法可以採用任何物件並對其呼叫 toString()。Dart 中有兩種允許所有值的類型:Object?dynamic。不過,它們傳達了不同的概念。如果您只是想要表示允許所有物件,請使用 Object?。如果您想要允許所有物件(null 除外),請使用 Object

dynamic 類型不僅接受所有物件,也允許所有運算。在編譯期間,允許對 dynamic 類型的值進行任何成員存取,但可能會失敗並在執行期間擲回例外。如果您確實需要這種有風險但靈活的動態調度,那麼 dynamic 就是正確的類型。

否則,建議使用 Object?Object。依賴 is 檢查和類型提升,以確保在存取值之前,該值的執行期間類型支援您要存取的成員。

gooddart
/// 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 時,特別是在泛型類型內部使用 dynamic。例如,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>,這樣比較乾淨。使用者更容易了解函式總是是非同步或總是同步,但可以是兩者的函式很難正確使用。

gooddart
Future<int> triple(FutureOr<int> value) async => (await value) * 3;
baddart
FutureOr<int> triple(FutureOr<int> value) {
  if (value is int) return value * 3;
  return value.then((v) => v * 3);
}

此準則更精確的表述是僅在反變位置使用 FutureOr<T>參數是反變的,而回傳類型是協變的。在巢狀函式類型中,這會被翻轉——如果您有一個參數的類型本身是一個函式,那麼回呼的回傳類型現在處於反變位置,而回呼的參數是協變的。這表示回呼的類型可以回傳 FutureOr<T>

gooddart
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。如果布林值代表的內容不清楚,可能會使呼叫位置難以閱讀

baddart
new Task(true);
new Task(false);
new ListBox(false, true, true);
new Button(false);

相反地,建議使用命名參數、命名建構函數或命名常數來釐清呼叫的用意。

gooddart
Task.oneShot();
Task.repeating();
ListBox(scroll: true, showScrollbars: true);
Button(ButtonState.enabled);

請注意,這不適用於設定器,因為名稱已清楚說明值代表的意義

gooddart
listBox.canScroll = true;
button.isEnabled = false;

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

#

選擇性位置參數應具有邏輯進程,例如較早參數比較晚參數更常傳遞。使用者幾乎不需要明確傳遞「空洞」來省略較早位置參數,以便傳遞較晚參數。建議使用命名參數來執行此動作。

gooddart
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)。

gooddart
var rest = string.substring(start);
baddart
var rest = string.substring(start, null);

確實使用包含開始和不包含結束的參數來接受範圍

#

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

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

gooddart
[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 時才會被呼叫。

gooddart
class Person {
  final String name;

  // ···

  bool operator ==(Object other) => other is Person && name == other.name;
}
baddart
class Person {
  final String name;

  // ···

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