目次

Effective Dart: 使用方法

目次 keyboard_arrow_down keyboard_arrow_up
more_horiz

これらのガイドラインは、Dartコードの本体で毎日使用できます。 ライブラリのユーザーは、ここで説明されているアイデアを内部化していることに気付かないかもしれませんが、メンテナンス担当者は間違いなく気付くでしょう。

ライブラリ

#

これらのガイドラインは、プログラムを一貫性があり保守可能な方法で複数のファイルから構成するのに役立ちます。 これらのガイドラインを簡潔にするために、「インポート」を使用してimportおよびexportディレクティブを網羅しています。 ガイドラインは両方に同様に適用されます。

part ofディレクティブには文字列を使用する

#

リンタールール: use_string_in_part_of_directives

多くのDart開発者は、partの使用を完全に避けています。 各ライブラリが単一のファイルである場合、コードについて推論しやすくなります。 partを使用してライブラリの一部を別のファイルに分割することを選択した場合、Dartは、別のファイルがどのライブラリの一部であるかを示す必要があります。

Dartでは、part ofディレクティブでライブラリの名前を使用できます。 ライブラリに名前を付けることは、レガシー機能であり、現在は推奨されていません。 ライブラリ名は、どのライブラリにpartが属するのかを判断する際に曖昧さを招く可能性があります。

推奨される構文は、ライブラリファイルへの直接ポイントするURI文字列を使用することです。 my_library.dartというライブラリがある場合、

my_library.dart
dart
library my_library;

part 'some/other/file.dart';

partファイルはライブラリファイルのURI文字列を使用する必要があります。

良い例dart
part of '../../my_library.dart';

ライブラリ名ではなく

悪い例dart
part of my_library;

別のパッケージのsrcディレクトリ内にあるライブラリをインポートしない

#

リンタールール: implementation_imports

libの下のsrcディレクトリは、指定されているとおり、パッケージ自身の内部実装専用のライブラリを含んでいます。 パッケージのメンテナンス担当者がパッケージのバージョンを管理する方法では、この規則が考慮されます。 彼らは、パッケージの破壊的な変更とはならず、srcの下のコードを大幅に変更できます。

つまり、別のパッケージのプライベートライブラリをインポートした場合、そのパッケージのマイナーな、理論的には破壊的ではないポイントリリースによって、コードが壊れる可能性があります。

インポートパスがlibの内外に到達することを許可しない

#

リンタールール: avoid_relative_lib_imports

package:インポートを使用すると、パッケージのlibディレクトリ内のライブラリにアクセスできます。コンピューター上のパッケージの保存場所を気にする必要はありません。 これが機能するには、ディスク上の他のファイルとの相対的な位置にlibが必要なインポートを持つことはできません。 つまり、lib内のファイルの相対インポートパスは、libディレクトリの外部のファイルにアクセスできず、libディレクトリの外部のライブラリは相対パスを使用してlibディレクトリにアクセスできません。 いずれかの操作を行うと、混乱を招くエラーやプログラムの破損につながります。

たとえば、ディレクトリ構造が次のようになっているとします。

my_package
└─ lib
   └─ api.dart
   test
   └─ api_test.dart

そして、api_test.dartapi.dartを2つの方法でインポートしているとします。

api_test.dart
悪い例dart
import 'package:my_package/api.dart';
import '../lib/api.dart';

Dartは、これらを完全に無関係な2つのライブラリのインポートであると考えています。 Dartと自分自身を混乱させるのを避けるために、次の2つのルールに従ってください。

  • インポートパスに/lib/を使用しない。
  • ../を使用してlibディレクトリからエスケープしない。

代わりに、パッケージのlibディレクトリにアクセスする必要がある場合(同じパッケージのtestディレクトリやその他のトップレベルディレクトリからでも)、package:インポートを使用します。

api_test.dart
良い例dart
import 'package:my_package/api.dart';

パッケージは、決してlibディレクトリの外部に到達して、パッケージ内の他の場所からライブラリをインポートしてはなりません。

相対インポートパスを優先する

#

リンタールール: prefer_relative_imports

前のルールが適用されない場合は、このルールに従ってください。 インポートがlibを跨がない場合は、相対インポートを使用することを優先します。それらはより短いです。 たとえば、ディレクトリ構造が次のようになっているとします。

my_package
└─ lib
   ├─ src
   │  └─ stuff.dart
   │  └─ utils.dart
   └─ api.dart
   test
   │─ api_test.dart
   └─ test_utils.dart

さまざまなライブラリがお互いをインポートする方法は次のとおりです。

lib/api.dart
良い例dart
import 'src/stuff.dart';
import 'src/utils.dart';
lib/src/utils.dart
良い例dart
import '../api.dart';
import 'stuff.dart';
test/api_test.dart
良い例dart
import 'package:my_package/api.dart'; // Don't reach into 'lib'.

import 'test_utils.dart'; // Relative within 'test' is fine.

Null

#

変数を明示的にnullに初期化しない

#

リンタールール: avoid_init_to_null

変数がnull許容でない型を持つ場合、初期化される前に使用しようとすると、Dartはコンパイルエラーを報告します。 変数がnull許容型の場合、暗黙的にnullに初期化されます。「初期化されていないメモリ」という概念はDartには存在せず、変数を明示的にnullに初期化して「安全」にする必要はありません。

良い例dart
Item? bestDeal(List<Item> cart) {
  Item? bestItem;

  for (final item in cart) {
    if (bestItem == null || item.price < bestItem.price) {
      bestItem = item;
    }
  }

  return bestItem;
}
悪い例dart
Item? bestDeal(List<Item> cart) {
  Item? bestItem = null;

  for (final item in cart) {
    if (bestItem == null || item.price < bestItem.price) {
      bestItem = item;
    }
  }

  return bestItem;
}

明示的なデフォルト値としてnullを使用しない

#

リンタールール: avoid_init_to_null

null許容パラメーターをオプションにしてもデフォルト値を指定しなければ、言語は暗黙的にnullをデフォルトとして使用するため、明示的に記述する必要はありません。

良い例dart
void error([String? message]) {
  stderr.write(message ?? '\n');
}
悪い例dart
void error([String? message = null]) {
  stderr.write(message ?? '\n');
}

等価演算子にtrueまたはfalseを使用しないでください。

#

null非許容ブール式をブールリテラルと等価演算子を使って評価するのは冗長です。等価演算子を削除し、必要に応じて単項否定演算子!を使用する方が常に簡単です。

良い例dart
if (nonNullableBool) { ... }

if (!nonNullableBool) { ... }
悪い例dart
if (nonNullableBool == true) { ... }

if (nonNullableBool == false) { ... }

null許容ブール式を評価するには、??または明示的な!= nullチェックを使用する必要があります。

良い例dart
// If you want null to result in false:
if (nullableBool ?? false) { ... }

// If you want null to result in false
// and you want the variable to type promote:
if (nullableBool != null && nullableBool) { ... }
悪い例dart
// Static error if null:
if (nullableBool) { ... }

// If you want null to be false:
if (nullableBool == true) { ... }

nullableBool == trueは有効な式ですが、いくつかの理由から使用すべきではありません。

  • このコードがnullと関連していることを示していません。

  • null関連であることが明らかではないため、等価演算子が冗長で削除できるnull非許容の場合と簡単に間違えられます。これは、左側のブール式がnullを生成する可能性がない場合にのみ当てはまり、可能性がある場合は当てはまりません。

  • ブールロジックが分かりにくくなっています。nullableBoolがnullの場合、nullableBool == trueは条件がfalseと評価されることを意味します。

??演算子を使用すると、nullに関する処理が行われていることが明確になるため、冗長な演算と間違われることはありません。ロジックもはるかに明確になり、式の結果がnullであることはブールリテラルと同じです。

条件内の変数に??などのnull aware演算子を使用しても、変数がnull非許容型に昇格するわけではありません。if文の本体内で変数を昇格させたい場合は、??ではなく明示的な!= nullチェックを使用する方が良いでしょう。

初期化済みかどうかを確認する必要がある場合は、late変数を避けてください。

#

Dartでは、late変数が初期化または代入済みかどうかを判断する方法がありません。アクセスすると、イニシャライザが実行されるか(存在する場合)、例外がスローされます。遅延初期化される状態がある場合、lateは適切な場合があります。しかし、初期化が既に完了しているかどうかを判別する必要もあります。

状態をlate変数に格納し、変数が設定されたかどうかを追跡する別のブール型フィールドを持つことで初期化を検出できますが、Dartは内部的にlate変数の初期化状態を保持しているため、これは冗長です。代わりに、通常は変数をlateではなくnull許容型にする方が明確です。そうすれば、nullをチェックすることで変数が初期化されているかどうかを確認できます。

もちろん、nullが変数の有効な初期化値である場合は、別のブール型フィールドを持つことが理にかなっています。

null許容型を使用するには、型プロモーションまたはnullチェックパターンを検討する

#

null許容変数がnullではないことを確認すると、変数はnull非許容型に昇格します。これにより、変数のメンバーにアクセスし、null非許容型を期待する関数に渡すことができます。

ただし、型昇格は、ローカル変数、パラメーター、およびプライベートfinalフィールドでのみサポートされています。操作可能な値は型昇格できません。

一般的に推奨されるように、メンバーをプライベートおよびfinalとして宣言することで、これらの制限を回避できることがよくありますが、常に可能なわけではありません。

型昇格の制限を回避するための1つのパターンは、nullチェックパターンを使用することです。これにより、メンバーの値がnullではないことが同時に確認され、その値が同じ基本型の新しいnull非許容変数にバインドされます。

良い例dart
class UploadException {
  final Response? response;

  UploadException([this.response]);

  @override
  String toString() {
    if (this.response case var response?) {
      return 'Could not complete upload to ${response.url} '
          '(error code ${response.errorCode}): ${response.reason}.';
    }
    return 'Could not upload (no response).';
  }
}

もう1つの回避策は、フィールドの値をローカル変数に代入することです。その変数に対するnullチェックは昇格されるため、安全にnull非許容として扱うことができます。

良い例dart
class UploadException {
  final Response? response;

  UploadException([this.response]);

  @override
  String toString() {
    final response = this.response;
    if (response != null) {
      return 'Could not complete upload to ${response.url} '
          '(error code ${response.errorCode}): ${response.reason}.';
    }
    return 'Could not upload (no response).';
  }
}

ローカル変数を使用する際には注意が必要です。フィールドに書き戻す必要がある場合は、代わりにローカル変数に書き戻さないようにしてください。(ローカル変数をfinalにすることで、そのような間違いを防ぐことができます。)また、ローカル変数のスコープ内にまだある間にフィールドが変更される可能性がある場合、ローカル変数は古い値を持つ可能性があります。

場合によっては、フィールドに!を使用するのが最善策です。ただし、場合によっては、ローカル変数またはnullチェックパターンを使用する方が、!を毎回使用するよりもクリーンで安全です。

悪い例dart
class UploadException {
  final Response? response;

  UploadException([this.response]);

  @override
  String toString() {
    if (response != null) {
      return 'Could not complete upload to ${response!.url} '
          '(error code ${response!.errorCode}): ${response!.reason}.';
    }

    return 'Could not upload (no response).';
  }
}

文字列

#

Dartで文字列を連結する際のベストプラクティスをいくつか紹介します。

隣接する文字列を使用して文字列リテラルを連結する

#

リンタールール:prefer_adjacent_string_concatenation

2つの文字列リテラル(値ではなく、実際の引用符付きリテラル形式)がある場合、それらを連結するために+を使用する必要はありません。CやC++と同様に、それらを並べるだけで連結できます。これは、1行に収まらない長い文字列を作成する良い方法です。

良い例dart
raiseAlarm('ERROR: Parts of the spaceship are on fire. Other '
    'parts are overrun by martians. Unclear which are which.');
悪い例dart
raiseAlarm('ERROR: Parts of the spaceship are on fire. Other ' +
    'parts are overrun by martians. Unclear which are which.');

文字列と値を合成するには、補間を優先する

#

リンタールール:prefer_interpolation_to_compose_strings

他の言語から移行してきた場合、リテラルやその他の値から文字列を作成するために長い+の連鎖を使用することに慣れているかもしれません。Dartでもそれは機能しますが、ほとんどの場合、補間を使用する方がクリーンで短くなります。

良い例dart
'Hello, $name! You are ${year - birth} years old.';
悪い例dart
'Hello, ' + name + '! You are ' + (year - birth).toString() + ' y...';

このガイドラインは、複数のリテラルと値を組み合わせる場合に適用されます。単一のオブジェクトを文字列に変換する場合は、.toString()を使用しても問題ありません。

必要がない場合は、補間で中括弧を使用しない

#

リンタールール:unnecessary_brace_in_string_interps

単純な識別子を補間していて、すぐに英数字が続かない場合は、{}を省略する必要があります。

良い例dart
var greeting = 'Hi, $name! I love your ${decade}s costume.';
悪い例dart
var greeting = 'Hi, ${name}! I love your ${decade}s costume.';

コレクション

#

Dartは、リスト、マップ、キュー、セットの4つのコレクション型をサポートしています。次のベストプラクティスはコレクションに適用されます。

可能な限りコレクションリテラルを使用する

#

リンタールール:prefer_collection_literals

Dartには、List、Map、Setの3つのコアコレクション型があります。MapとSetクラスは、ほとんどのクラスと同様に名前なしコンストラクターを持っています。しかし、これらのコレクションは非常に頻繁に使用されるため、Dartにはそれらを作成するためのより優れた組み込み構文があります。

良い例dart
var points = <Point>[];
var addresses = <String, Address>{};
var counts = <int>{};
悪い例dart
var addresses = Map<String, Address>();
var counts = Set<int>();

このガイドラインは、これらのクラスの名前付きコンストラクターには適用されません。List.from()Map.fromIterable()などは、すべて用途があります。(Listクラスにも名前なしコンストラクターがありますが、null安全なDartでは禁止されています。)

コレクションリテラルは、他のコレクションの内容を含めるためのスプレッド演算子と、内容を作成しながら制御フローを実行するためのifforにアクセスできるため、Dartでは特に強力です。

良い例dart
var arguments = [
  ...options,
  command,
  ...?modeFlags,
  for (var path in filePaths)
    if (path.endsWith('.dart')) path.replaceAll('.dart', '.js')
];
悪い例dart
var arguments = <String>[];
arguments.addAll(options);
arguments.add(command);
if (modeFlags != null) arguments.addAll(modeFlags);
arguments.addAll(filePaths
    .where((path) => path.endsWith('.dart'))
    .map((path) => path.replaceAll('.dart', '.js')));

コレクションが空かどうかを確認するために.lengthを使用しないでください。

#

リンタールール:prefer_is_emptyprefer_is_not_empty

Iterableコントラクトでは、コレクションがその長さを知っている、または一定時間でそれを提供できるとは要求されていません。コレクションに何かが含まれているかどうかを確認するためだけに.lengthを呼び出すのは、非常に遅くなる可能性があります。

代わりに、より高速で読みやすいゲッターがあります:.isEmpty.isNotEmpty。結果を否定する必要がない方を使用してください。

良い例dart
if (lunchBox.isEmpty) return 'so hungry...';
if (words.isNotEmpty) return words.join(' ');
悪い例dart
if (lunchBox.length == 0) return 'so hungry...';
if (!words.isEmpty) return words.join(' ');

関数リテラルでIterable.forEach()を使用しないでください。

#

リンタールール:avoid_function_literals_in_foreach_calls

forEach()関数はJavaScriptで広く使用されています。なぜなら、組み込みのfor-inループは通常必要なことを行わないからです。Dartでは、シーケンスを反復処理する場合は、ループを使用するのが慣用的な方法です。

良い例dart
for (final person in people) {
  ...
}
悪い例dart
people.forEach((person) {
  ...
});

このガイドラインは、「関数リテラル」を具体的に言及していることに注意してください。各要素に対して既に存在する関数を呼び出したい場合は、forEach()を使用しても問題ありません。

良い例dart
people.forEach(print);

また、Map.forEach()を使用しても常に問題ないことに注意してください。マップは反復可能ではないため、このガイドラインは適用されません。

結果の型を変更する意図がない限り、List.from()を使用しないでください。

#

Iterableが与えられると、同じ要素を含む新しいListを生成する2つの明白な方法があります。

dart
var copy1 = iterable.toList();
var copy2 = List.from(iterable);

明白な違いは、最初のものが短いことです。重要な違いは、最初のものが元のオブジェクトの型引数を保持することです。

良い例dart
// Creates a List<int>:
var iterable = [1, 2, 3];

// Prints "List<int>":
print(iterable.toList().runtimeType);
悪い例dart
// Creates a List<int>:
var iterable = [1, 2, 3];

// Prints "List<dynamic>":
print(List.from(iterable).runtimeType);

型を変更したい場合は、List.from()を呼び出すと便利です。

良い例dart
var numbers = [1, 2.3, 4]; // List<num>.
numbers.removeAt(1); // Now it only contains integers.
var ints = List<int>.from(numbers);

しかし、目的が単にIterableをコピーして元の型を保持すること、または型を気にしないことだけである場合は、toList()を使用してください。

コレクションを型でフィルタリングするにはwhereType()を使用してください。

#

リンタールール:prefer_iterable_whereType

さまざまなオブジェクトを含むリストがあり、その中から整数だけを取得したいとします。次のようにwhere()を使用できます。

悪い例dart
var objects = [1, 'a', 2, 'b', 3];
var ints = objects.where((e) => e is int);

これは冗長ですが、さらに悪いことに、おそらく望まない型のIterableを返します。この例では、フィルタリングする型がintであるにもかかわらず、Iterable<Object>を返します。

上記のエラーをcast()を追加することで「修正」するコードを見かけることがあります。

悪い例dart
var objects = [1, 'a', 2, 'b', 3];
var ints = objects.where((e) => e is int).cast<int>();

これは冗長で、2つのラッパーが作成され、2つの間接レベルと冗長なランタイムチェックが発生します。幸いにも、コアライブラリには、このユースケースに最適なwhereType()メソッドがあります。

良い例dart
var objects = [1, 'a', 2, 'b', 3];
var ints = objects.whereType<int>();

whereType()を使用すると簡潔で、目的の型のIterableが生成され、不要なラッピングレベルがありません。

近くの操作で済む場合は、cast()を使用しないでください。

#

Iterableやストリームを処理する場合、多くの場合、いくつかの変換を実行します。最後に、特定の型引数を持つオブジェクトを作成したいとします。cast()の呼び出しを追加する代わりに、既存の変換のいずれかで型を変更できるかどうかを確認してください。

既にtoList()を呼び出している場合は、それをList<T>.from()(ここでTは必要な結果リストの型)への呼び出しに置き換えてください。

良い例dart
var stuff = <dynamic>[1, 2];
var ints = List<int>.from(stuff);
悪い例dart
var stuff = <dynamic>[1, 2];
var ints = stuff.toList().cast<int>();

map() を呼び出す場合は、目的の型の反復可能オブジェクトを生成するように、明示的な型引数を指定してください。型推論は、map() に渡す関数に基づいて、多くの場合正しい型を選択しますが、明示的にする必要がある場合もあります。

良い例dart
var stuff = <dynamic>[1, 2];
var reciprocals = stuff.map<double>((n) => 1 / n);
悪い例dart
var stuff = <dynamic>[1, 2];
var reciprocals = stuff.map((n) => 1 / n).cast<double>();

cast() の使用は避けてください

#

これは前の規則のより緩やかな一般化です。オブジェクトの型を修正するために使用できる近くの操作がない場合があります。それでも、可能な限り、cast() を使用してコレクションの型を「変更」することは避けてください。

代わりに、これらのいずれかのオプションを使用することをお勧めします。

  • 正しい型で作成する。コレクションが最初に作成されるコードを変更して、正しい型になるようにします。

  • アクセス時に要素をキャストする。コレクションをすぐに反復処理する場合は、反復処理内で各要素をキャストします。

  • List.from() を使用して積極的にキャストする。最終的にコレクションのほとんどの要素にアクセスし、元のライブオブジェクトによってバックアップされるオブジェクトを必要としない場合は、List.from() を使用して変換します。

    cast() メソッドは、すべての操作で要素型をチェックする遅延コレクションを返します。少数の要素に対して少数の操作しか実行しない場合、その遅延は良い場合がありますが、多くの場合、遅延検証とラッピングのオーバーヘッドは利点を上回ります。

正しい型で作成する例を次に示します。

良い例dart
List<int> singletonList(int value) {
  var list = <int>[];
  list.add(value);
  return list;
}
悪い例dart
List<int> singletonList(int value) {
  var list = []; // List<dynamic>.
  list.add(value);
  return list.cast<int>();
}

アクセス時に各要素をキャストする例を次に示します。

良い例dart
void printEvens(List<Object> objects) {
  // We happen to know the list only contains ints.
  for (final n in objects) {
    if ((n as int).isEven) print(n);
  }
}
悪い例dart
void printEvens(List<Object> objects) {
  // We happen to know the list only contains ints.
  for (final n in objects.cast<int>()) {
    if (n.isEven) print(n);
  }
}

List.from() を使用して積極的にキャストする例を次に示します。

良い例dart
int median(List<Object> objects) {
  // We happen to know the list only contains ints.
  var ints = List<int>.from(objects);
  ints.sort();
  return ints[ints.length ~/ 2];
}
悪い例dart
int median(List<Object> objects) {
  // We happen to know the list only contains ints.
  var ints = objects.cast<int>();
  ints.sort();
  return ints[ints.length ~/ 2];
}

もちろん、これらの代替案が常に機能するわけではなく、場合によっては cast() が正しい答えになります。しかし、そのメソッドはやや危険で望ましくないことを考慮してください。遅くなる可能性があり、注意しないと実行時に失敗する可能性があります。

関数

#

Dartでは、関数もオブジェクトです。関数に関するいくつかのベストプラクティスを次に示します。

関数宣言を使用して関数を名前にバインドする

#

リンタールール: prefer_function_declarations_over_variables

最新の言語は、ローカルネストされた関数とクロージャがいかに便利であるかを認識しています。関数の内部に関数を定義することは一般的です。多くの場合、この関数はすぐにコールバックとして使用され、名前を付ける必要はありません。関数式はそれに最適です。

しかし、名前を付ける必要がある場合は、ラムダを変数にバインドする代わりに、関数宣言文を使用してください。

良い例dart
void main() {
  void localFunction() {
    ...
  }
}
悪い例dart
void main() {
  var localFunction = () {
    ...
  };
}

ティアオフで済む場合、ラムダを作成しない

#

リンタールール: unnecessary_lambdas

関数、メソッド、または名前付きコンストラクターを括弧なしで参照すると、Dart はティアオフを作成します。これは、関数と同じパラメーターを受け取り、呼び出すと基礎となる関数を呼び出すクロージャーです。クロージャーが受け入れるものと同じパラメーターを使用して名前付き関数を呼び出すクロージャーが必要な場合は、呼び出しをラムダでラップしないでください。ティアオフを使用してください。

良い例dart
var charCodes = [68, 97, 114, 116];
var buffer = StringBuffer();

// Function:
charCodes.forEach(print);

// Method:
charCodes.forEach(buffer.write);

// Named constructor:
var strings = charCodes.map(String.fromCharCode);

// Unnamed constructor:
var buffers = charCodes.map(StringBuffer.new);
悪い例dart
var charCodes = [68, 97, 114, 116];
var buffer = StringBuffer();

// Function:
charCodes.forEach((code) {
  print(code);
});

// Method:
charCodes.forEach((code) {
  buffer.write(code);
});

// Named constructor:
var strings = charCodes.map((code) => String.fromCharCode(code));

// Unnamed constructor:
var buffers = charCodes.map((code) => StringBuffer(code));

変数

#

次のベストプラクティスでは、Dart で変数を最適に使用する方法について説明します。

ローカル変数では varfinal に一貫したルールに従ってください。

#

ほとんどのローカル変数には型注釈を含める必要がなく、var または final のみを宣言して使用する必要があります。どちらを使用するかについては、広く使用されている2つのルールがあります。

  • 再代入されないローカル変数には final を、再代入される変数には var を使用します。

  • 再代入されないローカル変数も含め、すべてのローカル変数に var を使用します。ローカル変数には final を決して使用しないでください。(フィールドおよびトップレベル変数には final を使用することをお勧めします)。

どちらのルールも許容されますが、1つを選択してコード全体に一貫して適用してください。そうすれば、リーダーが var を見たときに、それが関数内で後で変数が代入されることを意味するかどうかがわかります。

計算できるものは保存しない

#

クラスを設計する際には、同じ基礎となる状態への複数のビューを公開したいことがよくあります。コンストラクターですべてのビューを計算して保存するコードをよく目にします。

悪い例dart
class Circle {
  double radius;
  double area;
  double circumference;

  Circle(double radius)
      : radius = radius,
        area = pi * radius * radius,
        circumference = pi * 2.0 * radius;
}

このコードには2つの問題があります。まず、メモリを無駄にしている可能性が高いことです。厳密に言えば、面積と周囲の長さはキャッシュです。これらは、既に持っている他のデータから再計算できる、保存された計算です。これらは、CPU 使用量の削減のためにメモリ増加をトレードオフしています。そのトレードオフに値するパフォーマンスの問題があることがわかっていますか?

さらに悪いことに、コードは間違っています。キャッシュの問題は無効化です。キャッシュが古くなって再計算が必要になったかどうかを知るにはどうすればよいですか?ここでは、radius が変更可能であるにもかかわらず、そうすることはありません。異なる値を代入しても、areacircumference は以前の(現在は正しくない)値を保持します。

キャッシュの無効化を正しく処理するには、これを行う必要があります。

悪い例dart
class Circle {
  double _radius;
  double get radius => _radius;
  set radius(double value) {
    _radius = value;
    _recalculate();
  }

  double _area = 0.0;
  double get area => _area;

  double _circumference = 0.0;
  double get circumference => _circumference;

  Circle(this._radius) {
    _recalculate();
  }

  void _recalculate() {
    _area = pi * _radius * _radius;
    _circumference = pi * 2.0 * _radius;
  }
}

これは、記述、保守、デバッグ、および読み取りに非常に多くのコードが必要です。代わりに、最初の実装は次のようになります。

良い例dart
class Circle {
  double radius;

  Circle(this.radius);

  double get area => pi * radius * radius;
  double get circumference => pi * 2.0 * radius;
}

このコードは短く、メモリ使用量が少なく、エラーが発生しにくいものです。円を表すために必要な最小限のデータが保存されます。単一の真実のソースしかないため、同期されないフィールドはありません。

場合によっては、遅い計算の結果をキャッシュする必要がある場合がありますが、パフォーマンスの問題が発生したことがわかった後にのみ実行し、注意深く実行し、最適化について説明するコメントを残してください。

メンバ

#

Dartでは、オブジェクトは関数(メソッド)またはデータ(インスタンス変数)である可能性のあるメンバーを持っています。オブジェクトのメンバーには、次のベストプラクティスが適用されます。

フィールドをgetterとsetterで不必要にラップしない

#

リンタールール: unnecessary_getters_setters

JavaとC#では、実装が単にフィールドに転送される場合でも、すべてのフィールドをゲッターとセッター(またはC#のプロパティ)で隠すのが一般的です。そうすれば、それらのメンバーでより多くの作業を行う必要がある場合は、呼び出しサイトに触れる必要なく行うことができます。これは、Javaではゲッターメソッドを呼び出すこととフィールドにアクセスすることが異なり、C#ではプロパティにアクセスすることと生のフィールドにアクセスすることがバイナリ互換性がないためです。

Dartにはこの制限がありません。フィールドとゲッター/セッターは完全に区別できません。クラスでフィールドを公開し、後でゲッターとセッターでラップすることができます。そのフィールドを使用するコードに触れる必要はありません。

良い例dart
class Box {
  Object? contents;
}
悪い例dart
class Box {
  Object? _contents;
  Object? get contents => _contents;
  set contents(Object? value) {
    _contents = value;
  }
}

読み取り専用のプロパティを作成するには、final フィールドの使用を優先してください。

#

外部コードが表示できるが割り当てできないフィールドがある場合、多くの場合に機能する簡単な解決策は、それをfinalでマークすることです。

良い例dart
class Box {
  final contents = [];
}
悪い例dart
class Box {
  Object? _contents;
  Object? get contents => _contents;
}

もちろん、コンストラクターの外部で内部的にフィールドに代入する必要がある場合は、「プライベートフィールド、パブリックゲッター」パターンを使用する必要があるかもしれませんが、必要になるまで使用しないでください。

単純なメンバーには => の使用を検討してください。

#

リンタールール: prefer_expression_function_bodies

関数式に => を使用することに加えて、Dart ではそれを用いてメンバーを定義することもできます。このスタイルは、値を計算して返すだけの単純なメンバーに適しています。

良い例dart
double get area => (right - left) * (bottom - top);

String capitalize(String name) =>
    '${name[0].toUpperCase()}${name.substring(1)}';

コードを記述する人は => を好むようですが、それを乱用して、読み取りが困難なコードになってしまうことは非常に簡単です。宣言が数行を超える場合、または深くネストされた式(カスケードや条件演算子は一般的な違反者です)が含まれている場合は、自分自身とコードを読まなければならないすべての人のために、ブロックボディとステートメントを使用してください。

良い例dart
Treasure? openChest(Chest chest, Point where) {
  if (_opened.containsKey(chest)) return null;

  var treasure = Treasure(where);
  treasure.addAll(chest.contents);
  _opened[chest] = treasure;
  return treasure;
}
悪い例dart
Treasure? openChest(Chest chest, Point where) => _opened.containsKey(chest)
    ? null
    : _opened[chest] = (Treasure(where)..addAll(chest.contents));

値を返さないメンバーにも => を使用できます。これは、セッターが小さく、=> を使用する対応するゲッターがある場合に慣用句です。

良い例dart
num get x => center.x;
set x(num value) => center = Point(value, center.y);

名前付きコンストラクターにリダイレクトする場合、またはシャドウイングを回避する場合を除いて、this. を使用しないでください。

#

リンタールール: unnecessary_this

JavaScriptでは、現在実行されているオブジェクトのメンバーを参照するには明示的なthis.が必要です。しかし、DartはC ++、Java、C#と同様に、その制限がありません。

this. を使用する必要があるのは2つの場合のみです。1つは、同じ名前のローカル変数がアクセスしたいメンバーをシャドウイングする場合です。

悪い例dart
class Box {
  Object? value;

  void clear() {
    this.update(null);
  }

  void update(Object? value) {
    this.value = value;
  }
}
良い例dart
class Box {
  Object? value;

  void clear() {
    update(null);
  }

  void update(Object? value) {
    this.value = value;
  }
}

もう1つのthis.を使用する必要があるのは、名前付きコンストラクターにリダイレクトする場合です。

悪い例dart
class ShadeOfGray {
  final int brightness;

  ShadeOfGray(int val) : brightness = val;

  ShadeOfGray.black() : this(0);

  // This won't parse or compile!
  // ShadeOfGray.alsoBlack() : black();
}
良い例dart
class ShadeOfGray {
  final int brightness;

  ShadeOfGray(int val) : brightness = val;

  ShadeOfGray.black() : this(0);

  // But now it will!
  ShadeOfGray.alsoBlack() : this.black();
}

コンストラクターパラメーターは、コンストラクターイニシャライザーリストでは決してフィールドをシャドウイングしません。

良い例dart
class Box extends BaseBox {
  Object? value;

  Box(Object? value)
      : value = value,
        super(value);
}

これは驚くように見えますが、期待どおりに機能します。幸いなことに、初期化フォーマルとスーパーイニシャライザーのおかげで、このようなコードは比較的まれです。

可能な限り、宣言時にフィールドを初期化する

#

フィールドがコンストラクターパラメーターに依存しない場合は、宣言時に初期化でき、する必要があります。コードが少なくなり、クラスに複数のコンストラクターがある場合の重複が回避されます。

悪い例dart
class ProfileMark {
  final String name;
  final DateTime start;

  ProfileMark(this.name) : start = DateTime.now();
  ProfileMark.unnamed()
      : name = '',
        start = DateTime.now();
}
良い例dart
class ProfileMark {
  final String name;
  final DateTime start = DateTime.now();

  ProfileMark(this.name);
  ProfileMark.unnamed() : name = '';
}

一部のフィールドは、たとえば他のフィールドを参照したり、メソッドを呼び出したりするためにthisを参照する必要があるため、宣言時に初期化できません。ただし、フィールドがlateでマークされている場合、イニシャライザーはthisにアクセスできます。

もちろん、フィールドがコンストラクターパラメーターに依存している場合、または異なるコンストラクターによって異なる方法で初期化される場合は、このガイドラインは適用されません。

コンストラクタ

#

次のベストプラクティスは、クラスのコンストラクターの宣言に適用されます。

可能な限り初期化形式を使用する

#

リンタールール: prefer_initializing_formals

多くのフィールドは、次のようにコンストラクターパラメーターから直接初期化されます。

悪い例dart
class Point {
  double x, y;
  Point(double x, double y)
      : x = x,
        y = y;
}

フィールドを定義するには、ここでx4回入力する必要があります。もっとうまくできます。

良い例dart
class Point {
  double x, y;
  Point(this.x, this.y);
}

コンストラクターパラメーターの前にあるこのthis.構文は、「初期化フォーマル」と呼ばれます。常にそれを利用できるわけではありません。名前付きパラメーターの名前が初期化しているフィールドの名前と一致しないようにする場合があります。しかし、初期化フォーマルを使用できる場合は、使用する必要があります。

コンストラクターイニシャライザーリストを使用できる場合は、late を使用しないでください。

#

Dartでは、null 許容ではないフィールドは読み取られる前に初期化する必要があります。フィールドはコンストラクター本体内で読み取ることができるため、本体の実行前にnull 許容ではないフィールドを初期化しないと、エラーが発生します。

フィールドをlateでマークすることにより、このエラーを解消できます。これにより、コンパイル時エラーが、フィールドが初期化される前にアクセスした場合の実行時エラーになります。これは、場合によっては必要なものですが、多くの場合、正しい修正は、コンストラクターイニシャライザーリストでフィールドを初期化することです。

良い例dart
class Point {
  double x, y;
  Point.polar(double theta, double radius)
      : x = cos(theta) * radius,
        y = sin(theta) * radius;
}
悪い例dart
class Point {
  late double x, y;
  Point.polar(double theta, double radius) {
    x = cos(theta) * radius;
    y = sin(theta) * radius;
  }
}

イニシャライザーリストを使用すると、コンストラクターパラメーターにアクセスでき、読み取る前にフィールドを初期化できます。したがって、イニシャライザーリストを使用できる場合は、フィールドをlateにして一部の静的安全性とパフォーマンスを失うよりも優れています。

空のコンストラクター本体には ;{} の代わりに使用してください。

#

リンタールール: empty_constructor_bodies

Dartでは、空の本体を持つコンストラクターは、セミコロンだけで終了できます。(実際、const コンストラクターには必須です)。

良い例dart
class Point {
  double x, y;
  Point(this.x, this.y);
}
悪い例dart
class Point {
  double x, y;
  Point(this.x, this.y) {}
}

new は使用しないでください。

#

リンタールール: unnecessary_new

コンストラクターを呼び出す場合、newキーワードは省略可能です。ファクトリコンストラクターは、new呼び出しが実際に新しいオブジェクトを返さない可能性があるため、その意味は明確ではありません。

言語はまだnewを許可していますが、非推奨と見なして、コードで使用することは避けてください。

良い例dart
Widget build(BuildContext context) {
  return Row(
    children: [
      RaisedButton(
        child: Text('Increment'),
      ),
      Text('Click!'),
    ],
  );
}
悪い例dart
Widget build(BuildContext context) {
  return new Row(
    children: [
      new RaisedButton(
        child: new Text('Increment'),
      ),
      new Text('Click!'),
    ],
  );
}

const を冗長に使用しないでください。

#

リンタールール: unnecessary_const

式が必ず定数である必要があるコンテキストでは、constキーワードは暗黙的であり、記述する必要がなく、記述するべきではありません。これらのコンテキストは、次のいずれかの式です。

  • const コレクションリテラル。
  • const コンストラクター呼び出し。
  • メタデータアノテーション。
  • const 変数宣言のイニシャライザー。
  • switch case 式(caseの直後から:の手前まで、caseの本体ではありません)。

(デフォルト値は、Dartの将来のバージョンでconst以外のデフォルト値がサポートされる可能性があるため、このリストには含まれていません)。

基本的に、constの代わりにnewを書くのがエラーになる場所では、Dartはconstを省略することを許可します。

良い例dart
const primaryColors = [
  Color('red', [255, 0, 0]),
  Color('green', [0, 255, 0]),
  Color('blue', [0, 0, 255]),
];
悪い例dart
const primaryColors = const [
  const Color('red', const [255, 0, 0]),
  const Color('green', const [0, 255, 0]),
  const Color('blue', const [0, 0, 255]),
];

エラー処理

#

Dartは、プログラムでエラーが発生した場合に例外を使用します。例外のキャッチとスローには、次のベストプラクティスが適用されます。

on句のないcatchは避けてください。

#

リンタールール: avoid_catches_without_on_clauses

on 修飾子を持たないキャッチ句は、try ブロック内のコードによってスローされたあらゆる例外をキャッチします。ポケモン例外処理 は、おそらく望ましいものではありません。あなたのコードはStackOverflowErrorまたはOutOfMemoryErrorを正しく処理していますか?tryブロック内でメソッドに間違った引数を渡した場合、デバッガーで間違いを指摘してもらいたいですか、それとも役に立つArgumentErrorを無視したいですか?スローされたAssertionErrorをキャッチするため、そのコード内のassert()文を事実上無効にしたいですか?

答えはおそらく「いいえ」であり、その場合はキャッチする型をフィルタリングする必要があります。ほとんどの場合、認識していて正しく処理できるランタイムエラーの種類に限定するon句を使用する必要があります。

まれに、任意のランタイムエラーをキャッチしたい場合があります。これは通常、任意のアプリケーションコードが問題を引き起こすのを防ごうとするフレームワークまたは低レベルのコードで行われます。ここでも、すべての型をキャッチするよりもExceptionをキャッチする方が通常は優れています。Exceptionはすべてのランタイムエラーの基底クラスであり、コードのプログラミング上のバグを示すエラーは除外されます。

on句のないcatch句からのエラーを破棄しないでください

#

コードの領域からスローされる可能性のあるすべてをキャッチする必要があると感じた場合でも、キャッチしたものを処理してください。ログに記録するか、ユーザーに表示するか、再スローするかしますが、静かに破棄しないでください。

Errorを実装するオブジェクトは、プログラミングエラーに対してのみスローしてください

#

Errorクラスは、プログラミング上のエラーの基底クラスです。この型またはArgumentErrorのようなそのサブインターフェースのオブジェクトがスローされると、コードにバグがあることを意味します。APIが呼び出し元に、正しく使用されていないことを報告する場合、Errorをスローすると、その信号が明確に送信されます。

逆に、例外がコードのバグを示さない何らかのランタイムエラーである場合、Errorをスローするのは誤解を招きます。代わりに、コアExceptionクラスまたはその他の型をスローしてください。

Errorまたはそれを実装する型を明示的にキャッチしないでください

#

リンタールール:avoid_catching_errors

これは上記のことに従います。Errorはコードのバグを示しているため、呼び出しスタック全体を巻き戻し、プログラムを停止し、スタックトレースを出力して、バグの位置を特定し修正する必要があります。

これらの型のエラーをキャッチすると、そのプロセスが中断され、バグがマスクされます。この例外を処理するためにエラー処理コードを追加する代わりに、それをスローしているコードを最初に修正してください。

キャッチされた例外を再スローするにはrethrowを使用してください

#

リンタールール:use_rethrow_when_possible

例外を再スローすることにした場合、throwを使用して同じ例外オブジェクトをスローする代わりに、rethrowステートメントを使用することをお勧めします。rethrowは、例外の元のスタックトレースを保持します。一方、throwはスタックトレースを最後にスローされた位置にリセットします。

悪い例dart
try {
  somethingRisky();
} catch (e) {
  if (!canHandle(e)) throw e;
  handle(e);
}
良い例dart
try {
  somethingRisky();
} catch (e) {
  if (!canHandle(e)) rethrow;
  handle(e);
}

非同期処理

#

Dartには、非同期プログラミングをサポートするためのいくつかの言語機能があります。次のベストプラクティスは、非同期コーディングに適用されます。

生のFutureを使用するよりもasync/awaitを優先する

#

非同期コードは、Futureのような優れた抽象化を使用した場合でも、読み取りとデバッグが非常に困難です。async/await構文は可読性を向上させ、非同期コード内ですべてのDart制御フロー構造を使用できます。

良い例dart
Future<int> countActivePlayers(String teamName) async {
  try {
    var team = await downloadTeam(teamName);
    if (team == null) return 0;

    var players = await team.roster;
    return players.where((player) => player.isActive).length;
  } catch (e) {
    log.error(e);
    return 0;
  }
}
悪い例dart
Future<int> countActivePlayers(String teamName) {
  return downloadTeam(teamName).then((team) {
    if (team == null) return Future.value(0);

    return team.roster.then((players) {
      return players.where((player) => player.isActive).length;
    });
  }).catchError((e) {
    log.error(e);
    return 0;
  });
}

役に立たない効果がある場合はasyncを使用しないでください

#

非同期に関連する処理を行う関数にasyncを使用する習慣になりやすいです。しかし、場合によっては冗長です。関数の動作を変更せずにasyncを省略できる場合は、そうしてください。

良い例dart
Future<int> fastestBranch(Future<int> left, Future<int> right) {
  return Future.any([left, right]);
}
悪い例dart
Future<int> fastestBranch(Future<int> left, Future<int> right) async {
  return Future.any([left, right]);
}

asyncが役に立つケースには、以下が含まれます。

  • awaitを使用している場合。(これは明らかなものです。)

  • 非同期的にエラーを返している場合。asyncthrowreturn Future.error(...)よりも短いです。

  • 値を返し、それをFutureで暗黙的にラップしたい場合。asyncFuture.value(...)よりも短いです。

良い例dart
Future<void> usesAwait(Future<String> later) async {
  print(await later);
}

Future<void> asyncError() async {
  throw 'Error!';
}

Future<String> asyncValue() async => 'value';

Streamを変換するには、高階関数を使用することを検討する

#

これは反復可能オブジェクトに関する上記の提案と並行しています。ストリームは多くの同じメソッドをサポートし、エラーの送信、クローズなども正しく処理します。

Completerを直接使用しない

#

非同期プログラミングの初心者の中には、Futureを生成するコードを書きたいと考えている人が多くいます。Futureのコンストラクタはニーズに合っていないように見えるため、最終的にCompleterクラスを見つけて使用します。

悪い例dart
Future<bool> fileContainsBear(String path) {
  var completer = Completer<bool>();

  File(path).readAsString().then((contents) {
    completer.complete(contents.contains('bear'));
  });

  return completer.future;
}

Completerは、新しい非同期プリミティブと、Futureを使用しない非同期コードとのインターフェースという2種類の低レベルコードに必要です。ほとんどの他のコードは、より明確でエラー処理が容易になるため、async/awaitまたはFuture.then()を使用する必要があります。

良い例dart
Future<bool> fileContainsBear(String path) {
  return File(path).readAsString().then((contents) {
    return contents.contains('bear');
  });
}
良い例dart
Future<bool> fileContainsBear(String path) async {
  var contents = await File(path).readAsString();
  return contents.contains('bear');
}

型引数がObjectになる可能性のあるFutureOr<T>を曖昧にする場合は、Future<T>をテストしてください

#

FutureOr<T>で何か役に立つことを実行する前に、通常はisチェックを実行して、Future<T>と単純なTのどちらを持っているかを確認する必要があります。型引数がFutureOr<int>のように特定の型である場合、使用するテスト(is intまたはis Future<int>)は問題ありません。これら2つの型は互いに素であるため、どちらでも機能します。

ただし、値型がObjectであるか、Objectでインスタンス化できる可能性のある型パラメーターである場合、2つの分岐は重複します。Future<Object>自体はObjectを実装するため、TObjectでインスタンス化できる型パラメーターである場合、is Objectまたはis Tは、オブジェクトがFutureの場合でもtrueを返します。代わりに、Futureのケースを明示的にテストします。

良い例dart
Future<T> logValue<T>(FutureOr<T> value) async {
  if (value is Future<T>) {
    var result = await value;
    print(result);
    return result;
  } else {
    print(value);
    return value;
  }
}
悪い例dart
Future<T> logValue<T>(FutureOr<T> value) async {
  if (value is T) {
    print(value);
    return value;
  } else {
    var result = await value;
    print(result);
    return result;
  }
}

悪い例では、Future<Object>を渡すと、それを単純な同期値として誤って扱います。