メインコンテンツにスキップ
目次

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

ブール値のプロパティや変数は「肯定的な」名前を推奨する

#

ほとんどのブール値の名前は、概念的に「肯定」と「否定」の形を持っています。前者は基本的な概念のように感じられ、後者はその否定です。「open」と「closed」、「enabled」と「disabled」など。しばしば、後者の名前は文字通り前者と否定する接頭辞を持ちます。「visible」と「in-visible」、「connected」と「dis-connected」、「zero」と「non-zero」。

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

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

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

メソッド名で get で始まることを避ける

#

ほとんどの場合、メソッドは get を名前から削除した getter であるべきです。たとえば、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);

型パラメータの名前付けにおいては、既存の記憶術的な規則に従う

#

1 文字の名前は正確にはわかりやすいものではありませんが、ほとんどすべてのジェネリック型でそれらが使用されています。幸いなことに、それらはほとんどの場合、一貫した記憶術的な方法で使用されています。規則は次のとおりです。

  • E はコレクション内の*要素*の型

    gooddart
    class IterableBase<E> {}
    class List<E> {}
    class HashSet<E> {}
    class RedBlackTree<E> {}
  • K および V は連想コレクション内の*キー*および*値*の型

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

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

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

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

上記のいずれのケースも適切でない場合は、別の 1 文字の記憶術的な名前または説明的な名前のいずれかを使用してください。

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 のような一部の言語では、ファイルの編成をクラスの編成に結び付けています。各ファイルは 1 つのトップレベルクラスのみを定義できます。Dart にはそのような制限はありません。ライブラリはクラスとは別個のエンティティです。単一のライブラリに複数のクラス、トップレベル変数、および関数が含まれていても、それらすべてが論理的にまとまっていれば問題ありません。

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

もちろん、このガイドラインは、すべてのクラスを巨大なモノリシックライブラリに入れるべきという意味ではありません。単に 1 つのライブラリに複数のクラスを配置できるという意味です。

クラスとミックスイン

#

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

単純な関数で済む場合に、1メンバーの抽象クラスを定義することを避ける

#

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 にはトップレベル関数、変数、および定数があるため、何かを定義するためにクラスを必要としません。名前空間が必要な場合は、ライブラリがより適しています。ライブラリは、インポートプレフィックス、および show/hide コンビネータをサポートしています。これらは、コードのコンシューマーが最も効果的な方法で名前の衝突を処理できるようにする強力なツールです。

関数または変数が論理的にクラスに結び付いていない場合は、トップレベルに配置してください。名前の衝突を心配している場合は、より正確な名前を付けるか、プレフィックス付きでインポートできる別のライブラリに移動してください。

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 で呼び出すメソッドを変更した場合、それらのメソッドをオーバーライドし、特定の時点で呼び出されることを期待しているサブクラスが壊れる可能性があります。

これら両方により、クラスはサブクラス化を許可するかどうかを慎重に決定する必要があります。これは doc コメントで伝えたり、IterableBase のようなわかりやすい名前を付けたりできます。クラスの作成者がそれを行わない場合は、クラスを拡張しないと想定するのが最善です。そうしないと、後続の変更によってコードが壊れる可能性があります。

クラスが拡張できるかどうかを制御するために、クラス修飾子を使用する

#

finalinterface、または sealed のようなクラス修飾子は、クラスがどのように拡張できるかを制限します。たとえば、final class A {} または interface class B {} を使用して、現在のライブラリ外への拡張を防ぎます。ドキュメントに依存するのではなく、これらの修飾子を使用して意図を伝えてください。

インターフェースを意図されていないクラスを実装することを避ける

#

暗黙的なインターフェースは、Dart における強力なツールであり、契約の実装から容易に推論できる場合に、クラスの契約を繰り返す必要を回避します。

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

ライブラリの保守者は、ユーザーを壊すことなく既存のクラスを進化させる能力が必要です。クラスのインターフェースを実装していると見なすと、それらのクラスを変更することは非常に困難になります。その困難さは、依存しているライブラリの成長と新しいニーズへの適応が遅くなることにつながります。

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

クラスがインターフェースとなるかどうかを制御するために、クラス修飾子を使用する

#

ライブラリを設計する際には、finalbase、または sealed のようなクラス修飾子を使用して、意図された使用法を強制します。たとえば、final class C {} または base class D{} を使用して、現在のライブラリ外での実装を防ぎます。すべてのライブラリがこれらの修飾子を使用して設計意図を強制することが理想的ですが、開発者はそれらが適用されていない場合でも、状況に遭遇する可能性があります。そのような場合は、意図しない実装の問題に注意してください。

ピュアな mixin またはピュアな classmixin class よりも優先する

#

Linter ルール: 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 にできます。これにより、ユーザーは定数が必要な場所 (他のより大きな定数の中、switch ケース、デフォルトのパラメータ値など) でクラスのインスタンスを作成できます。

明示的に const にしないと、それを行うことができません。

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

メンバー

#

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

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

#

Linter ルール: prefer_final_fields

ミュータブルでない (時間とともに変化しない) 状態は、プログラマーにとって理解しやすいです。ミュータブルな状態の量を最小限に抑えるクラスとライブラリは、保守が容易になる傾向があります。もちろん、ミュータブルなデータを持つことはしばしば有用です。しかし、必要がない場合は、フィールドとトップレベル変数を可能な限り final にすることがデフォルトであるべきです。

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

概念的にプロパティにアクセスする操作には、getter を使用する

#

メンバーが getter であるべきかメソッドであるべきかの決定は、優れた API 設計の微妙だが重要な部分です。したがって、この非常に長いガイドラインです。他の言語文化では getter を避ける傾向があります。操作がフィールドにほぼ正確に似ている場合、つまりオブジェクト全体に存在する状態のわずかな計算を行う場合にのみ、getter を使用します。それよりも複雑または重いものは、() を名前に付けて「計算中!」という信号を送ります。なぜなら、. の後の単純な名前は「フィールド」を意味するからです。

Dart はそのようではありません。Dart では、すべてのドット付き名前は、計算を行う可能性のあるメンバーの呼び出しです。フィールドは特別です。それらは言語によって実装が提供される getter です。言い換えると、フィールドは Dart では「特に遅い getter」ではなく、フィールドは「特に速い 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 は異なる結果を生成します)。

    ここでの「同じ結果」とは、successive な呼び出しで getter が同一のオブジェクトを生成しなければならないという意味ではありません。それを要求すると、多くの getter が脆いキャッシュを持つことになり、getter を使用する意味がなくなります。getter が呼び出されるたびに新しい Future またはリストを返すことは一般的で、まったく問題ありません。重要なのは、Future が同じ値に完了し、リストに同じ要素が含まれていることです。

    言い換えると、結果の値は、呼び出し元が気にする側面においては同じであるべきです。

    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 の場合、「フィールドライク」とは次のことを意味します。

  • 操作は 1 つの引数を取り、結果の値を生成しません。

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

  • 操作は冪等です。 同じ setter を同じ値で 2 回呼び出しても、呼び出し元にとっては 2 回目は何も起こらないはずです。内部的には、キャッシュの無効化やログ記録が行われているかもしれません。それは問題ありません。しかし、呼び出し元の視点からは、2 回目の呼び出しは何も行わないように見えます。

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

対応する getter なしで setter を定義しない

#

Linter ルール: avoid_setters_without_getters

ユーザーは getter と setter をオブジェクトの可視的なプロパティとして考えます。「ドロップボックス」プロパティで、書き込みはできるが読み取りができないのは、混乱を招き、プロパティの動作に関する直感を損ないます。たとえば、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 を定義する。

null 許容の FutureStream、およびコレクション型を返すことを避ける

#

API がコンテナ型を返す場合、データの不在を示す 2 つの方法があります。空のコンテナを返すか、null を返します。ユーザーは通常、データがないことを示すために空のコンテナを使用することを期待し、好みます。そのようにすると、isEmpty のようなメソッドを呼び出すことができる実際のオブジェクトが手に入ります。

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

例外: null を返すことが空のコンテナを返すことと意味が異なる場合、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');

#

プログラムに型を書き込むと、コードのさまざまな部分に流れる値の種類が制約されます。型は 2 種類の場所に出現します: 宣言の型注釈ジェネリック呼び出しの型引数。

型注釈は、通常「静的型」と考えるものです。変数値、パラメータ、フィールド、または戻り値の型に型注釈を付けることができます。次の例では、boolString は型注釈です。それらはコードの静的な宣言構造にぶら下がっており、実行時に「実行」されません。

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

ジェネリック呼び出しは、コレクションリテラル、ジェネリッククラスのコンストラクタへの呼び出し、またはジェネリックメソッドの呼び出しです。次の例では、numint はジェネリック呼び出しの型引数です。それらは型ですが、実行時にリイ(reified)され、呼び出しに渡されるファーストクラスエンティティです。

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

変数の型 (トップレベル、ローカル、静的フィールド、またはインスタンスフィールド) は、初期化子から推論されることがよくあります。ただし、初期化子がない場合、推論は失敗します。

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 は? 文字列または File オブジェクトですか? このメソッドは同期ですか、非同期ですか? これはより明確です。

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

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

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

#

関数のパラメータリストは、外部世界との境界を決定します。パラメータ型を注釈付けすることで、その境界が明確になります。デフォルトのパラメータ値は変数の初期化子のように見えますが、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);
  }
}

例外: 関数式と初期化フォーマルには、次の 2 つのガイドラインで説明されているように、異なる型注釈規則があります。

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

#

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

言語が関数式のパラメータに望む型を推論できる場合は、注釈を付けないでください。まれに、周囲のコンテキストが、関数の 1 つ以上のパラメータの型を提供するのに十分正確でない場合があります。その場合、注釈を付ける必要がある場合があります。(関数がすぐに使用されない場合は、名前付き宣言にする方が良いでしょう)。

初期化フォーマルを型注釈付けしない

#

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

ここでは、numbers には型注釈がありますが、注釈はジェネリック List に型引数を提供していません。同様に、Completer への Map 型引数も完全に指定されていません。これらの場合、Dart は周囲のコンテキストを使用して型の残りを「補完」しようとしません。代わりに、不足している型引数を (クラスにバウンドがある場合は) dynamic でサイレントに補完します。これはほとんど望ましくないことです。

代わりに、型注釈または呼び出し内の型引数としてジェネリック型を記述する場合は、完全な型を記述するようにしてください。

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

推論の失敗を避けるために dynamic で注釈を付ける

#

推論が型を補完しない場合、通常は dynamic がデフォルトになります。dynamic が望む型である場合、これは技術的には最も簡潔な方法です。しかし、最も明確な方法ではありません。コードを casual に読む人は、注釈が欠落しているのを見て、dynamic を意図したのか、推論が他の型を補完することを期待したのか、単に注釈を書き忘れたのかを知る方法がありません。

dynamic が望む型である場合は、意図を明確にするために明示的に記述し、このコードの静的安全性が低いことを強調してください。

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

Dart が dynamic正常に推論した場合、型を省略しても問題ないことに注意してください。

gooddart
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 を使用するよりもわずかに有用です。注釈を付ける場合は、パラメータと関数の戻り値を含む完全な関数型を優先してください。

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

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

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

setter の戻り値の型を指定しない

#

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 を定義するための 2 つの構文があります。元の構文は次のようになります。

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 の 2 つの型は、すべての値を許可します: 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.');
}

このルールの主な例外は、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>返す場合、ユーザーは何か有用なことをする前に、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);

これは setter には適用されないことに注意してください。setter では、名前が値が何を表しているかを明確にします。

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

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

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

== 演算子は数学的な等価性の規則に従うようにする

#

同値関係は次のようであるべきです。

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

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

  • 推移性: a == bb == c が両方とも true を返す場合、a == c もそうである必要があります。

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

ミュータブルクラスのカスタム等価性を定義することを避ける

#

Linter ルール: avoid_equals_and_hash_code_on_mutable_classes

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

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

== のパラメータを null 許容にしない

#

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