目次

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.

良い名前は、通常、いくつかの種類の動詞で始まります

  • 「である」の形: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);

ブール値のプロパティまたは変数には「肯定的な」名前を使用する

#

ほとんどのブール値の名前には、概念的に「肯定的」な形式と「否定的」な形式があり、前者は基本的な概念のように感じられ、後者はその否定です。「開いている」と「閉じている」、「有効」と「無効」などです。多くの場合、後者の名前には、前者を否定する接頭辞が文字通り付いています。「見える」と「*見えない」、「接続されている」と「*切断されている」、「ゼロ」と「*非*ゼロ」などです。

true が表す2つのケースのどちらを選択するか(つまり、プロパティの名前が付けられるケース)を選ぶ場合は、肯定的またはより基本的なケースを優先してください。ブール値のメンバーは、否定演算子を含む論理式の中にネストされることがよくあります。プロパティ自体が否定のように読める場合、読者が二重否定を精神的に実行してコードの意味を理解することはより困難になります。

良い例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();

このように、呼び出しは、その作業を実行するコマンドのように読めます。

値を返すことが主な目的である関数またはメソッドには、名詞句または命令形でない動詞句を使用する

#

他の呼び出し可能なメンバーは、副作用はほとんどありませんが、呼び出し元に有用な結果を返します。メンバーがそれを行うためにパラメータを必要としない場合、通常はゲッターにする必要があります。しかし、論理的な「プロパティ」にパラメータが必要になる場合があります。たとえば、elementAt() はコレクションからデータの一部を返しますが、*どの*データを返すかを知るためにパラメータが必要です。

これは、メンバーが*構文的には*メソッドですが、*概念的には*プロパティであり、メンバーが*何を*返すかを説明する句を使用して、そのように名前を付ける必要があることを意味します。

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

このガイドラインは、前のガイドラインよりも意図的に緩やかです。メソッドに副作用がない場合でも、list.take()string.split() のような動詞句で名前を付ける方が簡単な場合があります。

実行する処理を強調したい場合は、関数またはメソッドに命令形の動詞句を使用することを検討する

#

メンバーが副作用なしで結果を生成する場合、通常はゲッターまたは、それが返す結果を説明する名詞句名を持つメソッドにする必要があります。ただし、その結果を生成するために必要な作業が重要な場合があります。ランタイムエラーが発生しやすい、またはネットワークやファイルI/Oなどの重量リソースを使用する可能性があります。このような場合、呼び出し元にメンバーが行っている作業について考えてもらいたい場合は、その作業を説明する動詞句名をメンバーに付けてください。

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

ただし、このガイドラインは前の2つよりも緩やかであることに注意してください。操作が実行する作業は、多くの場合、呼び出し元には関係のない実装の詳細であり、パフォーマンスと堅牢性の境界は時間とともに変化します。ほとんどの場合、メンバーの名前は、*どのように*行うかではなく、呼び出し元のために*何を*行うかに基づいて付けてください。

メソッド名を get で始めるのは避けてください。

#

ほとんどの場合、メソッドは、名前から get を削除したゲッターにする必要があります。たとえば、getBreakfastOrder() という名前のメソッドの代わりに、breakfastOrder という名前のゲッターを定義します。

メンバーが引数を取るなどの理由でゲッターに適していないためにメソッドにする必要がある場合でも、get は避ける必要があります。前のガイドラインで述べたように、

  • 呼び出し元がメソッドが返す値を主に気にしている場合は、単に get を削除し、breakfastOrder() のような名詞句名を使用します。

  • 呼び出し元が行われている作業を気にしている場合は、動詞句名を使用しますが、get よりも正確に作業を説明する動詞(createdownloadfetchcalculaterequestaggregate など)を選択します。

オブジェクトの状態を新しいオブジェクトにコピーする場合、メソッドに to___() という名前を付けることをお勧めします。

#

リンタールール:use_to_and_as_if_applicable

*変換*メソッドとは、レシーバーの状態のほぼすべてのコピーを含む新しいオブジェクトを返しますが、通常は異なる形式または表現で返します。コアライブラリには、これらのメソッドの名前が to で始まり、その後に結果の種類が続くという規則があります。

変換メソッドを定義する場合は、その規則に従うと役立ちます。

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

元のオブジェクトによって裏付けられた異なる表現を返す場合、メソッドに as___() という名前を付けることをお勧めします。

#

リンタールール: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

    良い例dart
    class IterableBase<E> {}
    class List<E> {}
    class HashSet<E> {}
    class RedBlackTree<E> {}
  • 連想コレクション内の**キー**と**値**タイプの KV

    良い例dart
    class Map<K, V> {}
    class Multimap<K, V> {}
    class MapEntry<K, V> {}
  • 関数またはクラスのメソッドの**戻り**型として使用される型の R。これは一般的ではありませんが、typedef やビジターパターンを実装するクラスに表示されることがあります。

    良い例dart
    abstract class ExpressionVisitor<R> {
      R visitBinary(BinaryExpression node);
      R visitLiteral(LiteralExpression node);
      R visitUnary(UnaryExpression node);
    }
  • それ以外の場合は、単一の型パラメータを持つジェネリックで、周囲の型がその意味を明確にする場合は、TS、および U を使用します。周囲の名前をシャドウイングすることなくネストできるように、ここに複数の文字があります。例えば

    良い例dart
    class Future<T> {
      Future<S> then<S>(FutureOr<S> onValue(T value)) => ...
    }

    ここで、ジェネリックメソッド then<S>() は、Future<T>T をシャドウイングしないように S を使用します。

上記のいずれのケースも適切でない場合は、別の単一文字のニーモニック名または説明的な名前のいずれかで問題ありません。

良い例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にはそのような制限はありません。ライブラリは、クラスとは別の個別のエンティティです。すべてが論理的に属している場合、単一のライブラリに複数のクラス、トップレベル変数、および関数が含まれていても問題ありません。

1つのライブラリに複数のクラスをまとめると、いくつかの便利なパターンが可能になります。Dartのプライバシーはクラスレベルではなくライブラリレベルで機能するため、これはC++のように「フレンド」クラスを定義する方法です。同じライブラリで宣言されたすべてのクラスは、互いのプライベートメンバーにアクセスできますが、そのライブラリの外部のコードはアクセスできません。

もちろん、このガイドラインは、*すべて*のクラスを巨大な一枚岩のライブラリに入れる*べき*であるという意味ではなく、単一のライブラリに複数のクラスを配置できるという意味です。

クラスとミックスイン

#

Dartは、すべてのオブジェクトがクラスのインスタンスであるという意味で、「純粋な」オブジェクト指向言語です。しかし、Dartではすべてのコードをクラス内で定義する必要はありません。手続き型言語や関数型言語のように、トップレベルの変数、定数、および関数を定義できます。

単純な関数で済む場合は、1つのメンバーを持つ抽象クラスを定義しない

#

リンタールール:one_member_abstracts

Javaとは異なり、Dartにはファーストクラス関数、クロージャ、そしてそれらを使用するための優れた軽量構文があります。コールバックのようなものだけが必要な場合は、関数を使用してください。クラスを定義していて、callinvoke のような無意味な名前の単一の抽象メンバーしかない場合は、関数が必要なだけである可能性が高くなります。

良い例dart
typedef Predicate<E> = bool Function(E element);
悪い例dart
abstract class Predicate<E> {
  bool test(E element);
}

静的メンバーのみを含むクラスを定義しない

#

リンタールール: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` のようなわかりやすい名前をクラスに付けることで伝えることができます。クラスの作成者がそれを行わない場合は、クラスを拡張*しない*ことを前提とするのが最善です。そうしないと、後で変更するとコードが壊れる可能性があります。

クラスが拡張をサポートしている場合は、ドキュメント化する

#

これは上記のルールの当然の結果です。クラスのサブクラスを許可する場合は、それを明記してください。クラス名に `Base` を追加するか、クラスのドキュメントコメントに記載してください。

インターフェースとなることを意図していないクラスを実装しない

#

暗黙的なインターフェースは、Dart において強力なツールであり、そのコントラクトの実装のシグネチャから簡単に推測できる場合に、クラスのコントラクトを繰り返す必要を回避できます。

しかし、クラスのインターフェースを実装することは、そのクラスと非常に密接に結合しています。これは、インターフェースを実装しているクラスに*ほぼすべて*の変更を加えると、実装が壊れることを意味します。たとえば、クラスに新しいメンバーを追加することは、通常、安全で破壊的ではない変更です。ただし、そのクラスのインターフェースを実装している場合、クラスにはその新しいメソッドの実装がないため、静的エラーが発生します。

ライブラリのメンテナーは、ユーザーに影響を与えずに既存のクラスを進化させることができる必要があります。すべてのクラスを、ユーザーが自由に実装できるインターフェースを公開しているかのように扱うと、それらのクラスの変更が非常に困難になります。その困難さは、依存しているライブラリが新しいニーズへの対応と成長が遅くなることを意味します。

使用するクラスの作成者に、より多くの自由度を与えるために、実装されることが明確に意図されているクラスを除いて、暗黙的なインターフェースの実装は避けてください。そうしないと、作成者が意図しない結合が導入され、作成者が気づかないうちにコードが壊れる可能性があります。

クラスがインターフェースとして使用できる場合は、ドキュメント化する

#

クラスをインターフェースとして使用できる場合は、クラスのドキュメントコメントにその旨を記載してください。

`mixin class` よりも純粋な `mixin` または純粋な `class` の定義を推奨します

#

リンタールール: prefer_mixin

Dart は以前(言語バージョン 2.12 から 2.19)特定の制限(デフォルト以外のコンストラクタなし、スーパークラスなしなど)を満たすクラスを他のクラスにミックスインすることを許可していました。これは、クラスの作成者がミックスインすることを意図していなかった可能性があるため、混乱を招きました。

Dart 3.0.0 では、他のクラスにミックスインされることを意図し、通常のクラスとして扱われるすべての型は、`mixin class` 宣言で明示的に宣言する必要があります。

ただし、ミックスインとクラスの両方である必要がある型はまれなケースです。 `mixin class` 宣言は、主に 3.0.0 より前のミックスインとして使用されているクラスを、より明示的な宣言に移行するのに役立つことを目的としています。新しいコードは、純粋な `mixin` または純粋な `class` 宣言のみを使用することで、宣言の動作と意図を明確に定義し、ミックスインクラスのあいまいさを回避する必要があります。

`mixin` および `mixin class` 宣言の詳細については、ミックスインとしてのクラスの移行 を読んでください。

コンストラクタ

#

Dart コンストラクタは、クラスと同じ名前の関数と、オプションで追加の識別子を宣言することによって作成されます。後者は*名前付きコンストラクタ*と呼ばれます。

クラスがサポートしている場合は、コンストラクタを `const` にすることを検討してください

#

すべてのフィールドが final で、コンストラクタがそれらを初期化するだけのクラスがある場合、そのコンストラクタを `const` にすることができます。これにより、ユーザーは定数が required である場所(他の大きな定数、switch ケース、デフォルトパラメータ値など)でクラスのインスタンスを作成できます。

明示的に `const` にしない場合、それらを行うことはできません。

ただし、`const` コンストラクタはパブリック API におけるコミットメントであることに注意してください。後でコンストラクタを `const` 以外に変更すると、定数式でそれを呼び出しているユーザーに影響が出ます。コミットしたくない場合は、`const` にしないでください。実際には、`const` コンストラクタは、単純で不変の値のような型に最も役立ちます。

メンバー

#

メンバーはオブジェクトに属し、メソッドまたはインスタンス変数のいずれかになります。

フィールドとトップレベル変数を `final` にすることを推奨します

#

リンタールール: prefer_final_fields

*変更可能*でない状態、つまり時間とともに変化しない状態は、プログラマが推論しやすいものです。変更可能な状態の量を最小限に抑えるクラスとライブラリは、メンテナンスが容易になる傾向があります。もちろん、変更可能なデータを持つことはしばしば役に立ちます。ただし、必要ない場合は、デフォルトでフィールドとトップレベル変数を `final` にする必要があります。

インスタンスフィールドは初期化後に変更されませんが、インスタンスが構築されるまで初期化できない場合があります。たとえば、インスタンスの `this` または他のフィールドを参照する必要がある場合があります。そのような場合は、フィールドを `late final` にすることを検討してください。そうすると、宣言時にフィールドを初期化できる場合もあります。

概念的にプロパティにアクセスする操作にはゲッターを使用する

#

メンバーをゲッターにするかメソッドにするかを決定することは、優れた API 設計の微妙ですが重要な部分であるため、このガイドラインは非常に長くなっています。他の言語の文化では、ゲッターを敬遠するものがあります。操作がフィールドとほぼ同じである場合にのみ使用されます。つまり、オブジェクトに完全に存在する状態に対してごくわずかな計算を行います。それよりも複雑または重いものは、`。` の後の名前が「フィールド」を意味するため、「ここで計算が行われています!」を示すために名前の後に `()` が付きます。

Dart はそうではありません。Dart では、*すべて*のドット付きの名前は、計算を行う可能性のあるメンバー呼び出しです。フィールドは特別です。実装が言語によって提供されるゲッターです。言い換えれば、ゲッターは Dart では「特に遅いフィールド」ではありません。フィールドは「特に高速なゲッター」です。

それでも、メソッドよりもゲッターを選択すると、呼び出し側に重要なシグナルが送信されます。大まかに言って、シグナルは操作が「フィールドのような」ものであるということです。少なくとも原則として、呼び出し側が知っている限り、操作はフィールドを使用して実装*できる可能性があります*。これは、

  • 操作が引数を取らず、結果を返すことを意味します。

  • **呼び出し側は主に結果を気にします。** 呼び出し側が操作が結果を*どのように*生成するかを、生成される結果よりも心配するようにしたい場合は、操作に作業を説明する動詞の名前を付けて、メソッドにします。

    これは、操作がゲッターであるために特に高速である必要があることを意味する*わけではありません*。 `IterableBase.length` は `O(n)` であり、問題ありません。ゲッターが重要な計算を行うのは問題ありません。しかし、*驚くべき*量の作業を行う場合は、それを何をするかを説明する動詞である名前のメソッドにすることで、注意を引くことができます。

    悪い例dart
    connection.nextIncomingMessage; // Does network I/O.
    expression.normalForm; // Could be exponential to calculate.
  • **操作にはユーザーに見える副作用がありません。** 実際のフィールドにアクセスしても、オブジェクトまたはプログラム内の他の状態は変更されません。出力の生成、ファイルの書き込みなどは行いません。ゲッターもそれらを行うべきではありません。

    「ユーザーに見える」部分は重要です。ゲッターが隠れた状態を変更したり、帯域外の副作用を生成したりするのは問題ありません。ゲッターは、結果を遅延計算して保存したり、キャッシュに書き込んだり、ログを記録したりできます。呼び出し側が副作用を*気にしない*限り、おそらく問題ありません。

    悪い例dart
    stdout.newline; // Produces output.
    list.clear; // Modifies object.
  • **操作は*冪等*です。** 「冪等」とは、このコンテキストでは、基本的に、呼び出しの間に状態が明示的に変更されない限り、操作を複数回呼び出すと毎回同じ結果が生成されることを意味する奇妙な言葉です。(明らかに、呼び出しの間にリストに要素を追加すると、`list.length` は異なる結果を生成します。)

    ここでの「同じ結果」は、ゲッターが連続した呼び出しで文字通り同一のオブジェクトを生成する必要があることを意味するわけではありません。それを要求すると、多くのゲッターは脆いキャッシングを強制され、ゲッターを使用するという全体のポイントが無効になります。ゲッターが呼び出すたびに新しい future またはリストを返すのは一般的であり、 perfectly fine です。重要な部分は、future が同じ値で完了し、リストに同じ要素が含まれていることです。

    言い換えれば、結果の値は、*呼び出し側が気にする側面で*同じである必要があります。

    悪い例dart
    DateTime.now; // New result each time.
  • **結果のオブジェクトは、元のオブジェクトのすべての状態を公開しません。** フィールドは、オブジェクトの一部のみを公開します。操作が元のオブジェクトの全体の状態を公開する結果を返す場合、`to___()` または `as___()` メソッドにする方が良いでしょう。

上記のすべてが操作を説明している場合、それはゲッターである必要があります。生き残るメンバーは少ないように思えますが、驚くほど多くのメンバーが生き残ります。多くの操作は、いくつかの状態に対していくつかの計算を行うだけであり、それらのほとんどはゲッターにすることができますし、そうすべきです。

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

概念的にプロパティを変更する操作にはセッターを使用する

#

リンタールール: use_setters_to_change_properties

セッターとメソッドのどちらを選択するかは、ゲッターとメソッドのどちらを選択するかと似ています。どちらの場合も、操作は「フィールドのような」ものである必要があります。

セッターの場合、「フィールドのような」とは

  • 操作が単一の引数を取り、結果値を生成しないことを意味します。

  • 操作はオブジェクトの状態を変更します。

  • **操作は冪等です。** 同じセッターを同じ値で 2 回呼び出すと、呼び出し側に関する限り、2 回目は何も実行されません。内部的には、キャッシュの無効化またはログ記録が行われている可能性があります。それは問題ありません.しかし、呼び出し側の観点からは、2 回目の呼び出しは何もしないようです。

良い例dart
rectangle.width = 3;
button.visible = false;

対応するゲッターがないセッターを定義しない

#

リンタールール: avoid_setters_without_getters

ユーザーはゲッターとセッターをオブジェクトの可視プロパティと見なします。書き込み可能であるが、見えない「ドロップボックス」プロパティは混乱を招き、プロパティの動作に関する直感を混乱させます。たとえば、ゲッターのないセッターは、`=` を使用して変更できますが、`+=` は使用できません。

このガイドラインは、追加したいセッターを許可するためだけにゲッターを追加する必要があることを意味する*わけではありません*。オブジェクトは、一般的に必要な状態よりも多くの状態を公開するべきではありません。オブジェクトの状態の一部で、変更できるが同じ方法で公開できないものがある場合は、代わりにメソッドを使用してください。

オーバーロードを偽装するために実行時型テストを使用しない

#

API が異なる型のパラメータで同様の操作をサポートすることはよくあります。その類似性を強調するために、一部の言語は *オーバーロード* をサポートしています。これは、同じ名前で異なるパラメータリストを持つ複数のメソッドを定義できる機能です。コンパイル時に、コンパイラは実際の引数の型を見て、どのメソッドを呼び出すかを決定します。

Dart にはオーバーロードがありません。1 つのメソッドを定義し、本体内で `is` 型テストを使用して引数のランタイム型を調べ、適切な動作を実行することで、オーバーロードのように見える API を定義できます。ただし、このようにオーバーロードを偽装すると、*コンパイル時* のメソッド選択が *ランタイム* で行われる選択に変わります。

呼び出し元が通常、どの型を持っているか、どの特定の操作を実行したいかを知っている場合は、異なる名前の別々のメソッドを定義して、呼び出し元が適切な操作を選択できるようにする方が良いでしょう。これにより、静的型チェックが改善され、ランタイム型テストが回避されるため、パフォーマンスが向上します。

ただし、ユーザーが不明な型のオブジェクトを持っている可能性があり、API が内部で `is` を使用して適切な操作を選択することを *望む* 場合は、パラメータがサポートされているすべての型のスーパータイプである単一のメソッドが合理的かもしれません。

イニシャライザのないパブリックな `late final` フィールドは避けてください

#

他の `final` フィールドとは異なり、イニシャライザのない `late final` フィールドはセッターを定義します。そのフィールドがパブリックの場合、セッターもパブリックになります。これは、めったに望ましいことではありません。フィールドは通常、インスタンスのライフタイム中のどこかで、多くの場合コンストラクタ本体内で *内部的に* 初期化できるように `late` とマークされます。

ユーザーにセッターを呼び出してもらいたい場合を除き、次のいずれかの解決策を選択することをお勧めします

  • `late` を使用しないでください。
  • ファクトリコンストラクタを使用して `final` フィールドの値を計算します。
  • `late` を使用しますが、`late` フィールドを宣言時に初期化します。
  • `late` を使用しますが、`late` フィールドをプライベートにして、パブリックゲッターを定義します。

Null 許容型の `Future`、`Stream`、およびコレクション型を返すことは避けてください

#

API がコンテナ型を返す場合、データがないことを示す方法は 2 つあります。空のコンテナを返すか、`null` を返すことができます。ユーザーは一般的に、空のコンテナを使用して「データなし」を示すことを想定し、好んでいます。そうすれば、`isEmpty` のようなメソッドを呼び出すことができる実際のオブジェクトが得られます。

API が提供するデータがないことを示すには、空のコレクション、Null 許容型の非 Null 許容 Future、または値を発行しないストリームを返すことをお勧めします。

**例外:** `null` を返すことが空のコンテナを返すことと *異なる意味を持つ* 場合は、Null 許容型を使用するのが理にかなっている場合があります。

流れるようなインターフェースを有効にするためだけにメソッドから `this` を返すことは避けてください

#

リンタールール: avoid_returning_this

メソッドカスケードは、メソッド呼び出しを連鎖させるためのより良い解決策です。

良い例dart
var buffer = StringBuffer()
  ..write('one')
  ..write('two')
  ..write('three');
悪い例dart
var buffer = StringBuffer()
    .write('one')
    .write('two')
    .write('three');

#

プログラムで型を記述すると、コードのさまざまな部分に流れる値の種類が制約されます。型は、宣言の *型注釈* と *ジェネリック呼び出し* の型引数の 2 つの場所に表示されます。

型注釈は、「静的型」と聞いて通常思い浮かべるものです。変数、パラメータ、フィールド、または戻り値の型に注釈を付けることができます。次の例では、`bool` と `String` は型注釈です。これらはコードの静的な宣言構造に付随し、ランタイム時には「実行」されません。

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

ジェネリック呼び出しとは、コレクションリテラル、ジェネリッククラスのコンストラクタの呼び出し、またはジェネリックメソッドの呼び出しです。次の例では、`num` と `int` はジェネリック呼び出しの型引数です。これらは型ですが、実体化されてランタイム時に呼び出しに渡されるファーストクラスのエンティティです。

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` か他の型かとは直交します。

推論は、明白または重要でない型の記述と読み取りの手間を省くための強力なツールです。これにより、読者の注意はコード自体の動作に集中できます。明示的な型は、堅牢で保守しやすいコードの重要な部分でもあります。これらは API の静的な形状を定義し、プログラムのさまざまな部分にどのような種類の値が到達できるかを文書化および強制するための境界を作成します。

もちろん、推論は魔法ではありません。推論が成功して型が選択されても、それが目的の型ではない場合があります。一般的なケースは、後で他の型の値を変数に代入することを意図している場合に、変数のイニシャライザから過度に正確な型を推論することです。このような場合は、型を明示的に記述する必要があります。

ここでのガイドラインは、簡潔さと制御、柔軟性と安全性のバランスを最適にしています。さまざまなケースをすべて網羅するための具体的なガイドラインがありますが、大まかな概要は次のとおりです。

  • `dynamic` が目的の型であっても、推論に十分なコンテキストがない場合は注釈を付けます。

  • 必要な場合を除き、ローカル変数とジェネリック呼び出しに注釈を付けないでください。

  • イニシャライザで型が明らかでない限り、トップレベル変数とフィールドに注釈を付けることをお勧めします。

初期化子がない変数には型注釈を付ける

#

リンタールール: 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;
}

型が明らかでない場合は、フィールドとトップレベル変数に型注釈を付ける

#

リンタールール: 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 の型注釈がコードの *ユーザー* に役立つように、プライベートメンバーの型は *保守担当者* に役立ちます。

初期化されたローカル変数に冗長な型注釈を付けない

#

リンタールール: 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!';
}

このガイドラインは、*非ローカル* 関数宣言(トップレベル、静的、およびインスタンスメソッドとゲッター)にのみ適用されることに注意してください。ローカル関数と匿名関数式は、本体から戻り値の型を推論します。実際、匿名関数構文では戻り値の型注釈を付けることさえできません。

関数宣言のパラメータの型に注釈を付ける

#

関数の parameter リストは、外部との境界を決定します。パラメータの型に注釈を付けると、その境界が明確に定義されます。デフォルトのパラメータ値は変数のイニシャライザのように見えますが、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);
  }
}

**例外:** 関数式と初期化形式には、次の 2 つのガイドラインで説明するように、異なる型注釈規則があります。

関数式の推論されたパラメータ型に注釈を付けない

#

リンタールール: 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);

言語が関数式の引数に望ましい型を推論できる場合は、注釈を付けないでください。まれに、周囲のコンテキストが正確ではなく、関数の1つ以上の引数の型を提供できない場合があります。このような場合は、注釈を付ける必要があるかもしれません。(関数がすぐに使用されない場合は、通常は名前付き宣言にする方が適切です。)

初期化形式に型注釈を付けない

#

リンタールール: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では、これらは「raw型」と呼ばれます。例えば

悪い例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) => ...

例外:複数の異なる関数型の和集合を表す型が必要になる場合があります。たとえば、1つのパラメータを受け取る関数または2つのパラメータを受け取る関数を受け入れる場合があります。共用体型がないため、正確に型指定する方法がなく、通常は dynamic を使用する必要があります。Function は少なくともそれよりも少し役立ちます。

良い例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.');
    }
  }
}

セッターに戻り値の型を指定しない

#

リンタールール:avoid_return_types_on_setters

セッターは Dart では常に void を返します。記述するのは無意味です。

悪い例dart
void set foo(Foo value) { ... }
良い例dart
set foo(Foo value) { ... }

従来のtypedef構文を使用しない

#

リンタールール:prefer_generic_function_type_aliases

Dart には、関数型の名前付き typedef を定義するための2つの表記法があります。元の構文は次のようになります。

悪い例dart
typedef int Comparison<T>(T a, T b);

この構文にはいくつかの問題があります。

  • *ジェネリック*関数型に名前を割り当てる方法はありません。上記の例では、typedef 自体がジェネリックです。コード内で型引数なしで Comparison を参照すると、暗黙的に関数型 int Function<T>(T, T) *ではなく* int Function(dynamic, dynamic) が取得されます。これは実際にはあまり発生しませんが、特定のコーナーケースでは重要です。

  • パラメータ内の単一の識別子は、パラメータの*型*ではなく*名前*として解釈されます。以下を指定した場合

    悪い例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よりもインライン関数型を使用する

#

リンタールール: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 を定義する価値があるかもしれません。しかし、ほとんどの場合、ユーザーは関数型が実際に使用されている場所を確認したいと考えており、関数型構文はそれを明確にします。

パラメータには関数型構文を使用する

#

リンタールール: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 には、すべての値を許可する2つの型があります: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> です。

古いバージョンの Dart では型引数として void が許可されていなかったため、代わりに Future または Future<Null> を使用するコードが表示される場合があります。現在は許可されているため、使用する必要があります。そうすることで、同様の同期関数を型指定する方法とより直接的に一致し、呼び出し元と関数本体のエラーチェックが向上します。

有用な値を返さず、呼び出し元が非同期作業を待機したり、非同期エラーを処理したりする必要がない非同期関数の場合、戻り値の型として void を使用します。

戻り値の型として FutureOr<T> を使用することは避けてください。

#

メソッドが FutureOr<int> を受け入れる場合、受け入れる内容が寛大です。ユーザーは int または Future<int> のいずれかでメソッドを呼び出すことができるため、とにかくラップ解除する Futureint をラップする必要はありません。

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

これは、名前によって値が何を表しているかが明確になるセッターには適用されません。

良い例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);

範囲を受け入れるには、開始パラメータと終了パラメータを使用する(開始は含み、終了は含まない)

#

ユーザーが整数インデックスのシーケンスから要素またはアイテムの範囲を選択できるようにするメソッドまたは関数を定義する場合は、最初のアイテムを参照する開始インデックスと、最後のアイテムのインデックスよりも1大きい(おそらくオプションの)終了インデックスを取得します。

これは、同じことを行うコアライブラリと一致しています。

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

これらのパラメータは通常名前が付けられていないため、ここで一貫性を保つことが特に重要です。APIがエンドポイントではなく長さを取る場合、呼び出し側ではその違いはまったく見えません。

同値性

#

クラスのカスタム等価性動作を実装するのは難しい場合があります。ユーザーは、オブジェクトが一致する必要がある等価性の仕組みについて深い直感を持っており、ハッシュテーブルなどのコレクション型には、要素が従うことを期待する微妙な規約があります。

==をオーバーライドする場合は、hashCodeをオーバーライドしてください。

#

リンタールール:hash_and_equals

デフォルトのハッシュコード実装は、*同一性*ハッシュを提供します。2つのオブジェクトは、まったく同じオブジェクトである場合にのみ、通常同じハッシュコードを持ちます。同様に、==のデフォルトの動作は同一性です。

==をオーバーライドする場合、クラスによって「等しい」と見なされる異なるオブジェクトが存在する可能性があることを意味します。**等しい2つのオブジェクトは、同じハッシュコードを持つ必要があります。**そうでない場合、マップやその他のハッシュベースのコレクションは、2つのオブジェクトが等価であることを認識できません。

==演算子が等価性の数学的規則に従うようにしてください。

#

同値関係は次のようになります。

  • **反射的**:a == aは常にtrueを返す必要があります。

  • **対称的**:a == bb == aと同じものを返す必要があります。

  • **推移的**:a == bb == cの両方がtrueを返す場合、a == ctrueを返す必要があります。

==を使用するユーザーとコードは、これらのすべての法則が守られることを期待しています。クラスがこれらのルールに従うことができない場合、==は表現しようとしている操作の正しい名前ではありません。

可変クラスのカスタム同値性を定義しない

#

リンタールール:avoid_equals_and_hash_code_on_mutable_classes

==を定義する場合、hashCodeも定義する必要があります。どちらもオブジェクトのフィールドを考慮に入れる必要があります。これらのフィールドが*変更*された場合、オブジェクトのハッシュコードが変更される可能性があることを意味します。

ほとんどのハッシュベースのコレクションはそれを予期していません。オブジェクトのハッシュコードは永遠に同じであると想定しており、そうでない場合は予期しない動作をする可能性があります。

==へのパラメータをnull可能にしないでください。

#

リンタールール: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;
}