Effective Dart: 使用方法
これらのガイドラインは、Dartコードの本体で毎日使用できます。 ライブラリのユーザーは、ここで説明されているアイデアを内部化していることに気付かないかもしれませんが、メンテナンス担当者は間違いなく気付くでしょう。
ライブラリ
#これらのガイドラインは、プログラムを一貫性があり保守可能な方法で複数のファイルから構成するのに役立ちます。 これらのガイドラインを簡潔にするために、「インポート」を使用してimport
およびexport
ディレクティブを網羅しています。 ガイドラインは両方に同様に適用されます。
part of
ディレクティブには文字列を使用する
#リンタールール: use_string_in_part_of_directives
多くのDart開発者は、part
の使用を完全に避けています。 各ライブラリが単一のファイルである場合、コードについて推論しやすくなります。 part
を使用してライブラリの一部を別のファイルに分割することを選択した場合、Dartは、別のファイルがどのライブラリの一部であるかを示す必要があります。
Dartでは、part of
ディレクティブでライブラリの名前を使用できます。 ライブラリに名前を付けることは、レガシー機能であり、現在は推奨されていません。 ライブラリ名は、どのライブラリにpartが属するのかを判断する際に曖昧さを招く可能性があります。
推奨される構文は、ライブラリファイルへの直接ポイントするURI文字列を使用することです。 my_library.dart
というライブラリがある場合、
library my_library;
part 'some/other/file.dart';
partファイルはライブラリファイルのURI文字列を使用する必要があります。
part of '../../my_library.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.dart
がapi.dart
を2つの方法でインポートしているとします。
import 'package:my_package/api.dart';
import '../lib/api.dart';
Dartは、これらを完全に無関係な2つのライブラリのインポートであると考えています。 Dartと自分自身を混乱させるのを避けるために、次の2つのルールに従ってください。
- インポートパスに
/lib/
を使用しない。 ../
を使用してlib
ディレクトリからエスケープしない。
代わりに、パッケージのlib
ディレクトリにアクセスする必要がある場合(同じパッケージのtest
ディレクトリやその他のトップレベルディレクトリからでも)、package:
インポートを使用します。
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
さまざまなライブラリがお互いをインポートする方法は次のとおりです。
import 'src/stuff.dart';
import 'src/utils.dart';
import '../api.dart';
import 'stuff.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
に初期化して「安全」にする必要はありません。
Item? bestDeal(List<Item> cart) {
Item? bestItem;
for (final item in cart) {
if (bestItem == null || item.price < bestItem.price) {
bestItem = item;
}
}
return bestItem;
}
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
をデフォルトとして使用するため、明示的に記述する必要はありません。
void error([String? message]) {
stderr.write(message ?? '\n');
}
void error([String? message = null]) {
stderr.write(message ?? '\n');
}
等価演算子にtrue
またはfalse
を使用しないでください。
#null非許容ブール式をブールリテラルと等価演算子を使って評価するのは冗長です。等価演算子を削除し、必要に応じて単項否定演算子!
を使用する方が常に簡単です。
if (nonNullableBool) { ... }
if (!nonNullableBool) { ... }
if (nonNullableBool == true) { ... }
if (nonNullableBool == false) { ... }
null許容ブール式を評価するには、??
または明示的な!= null
チェックを使用する必要があります。
// 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) { ... }
// 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非許容変数にバインドされます。
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非許容として扱うことができます。
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チェックパターンを使用する方が、!
を毎回使用するよりもクリーンで安全です。
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行に収まらない長い文字列を作成する良い方法です。
raiseAlarm('ERROR: Parts of the spaceship are on fire. Other '
'parts are overrun by martians. Unclear which are which.');
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でもそれは機能しますが、ほとんどの場合、補間を使用する方がクリーンで短くなります。
'Hello, $name! You are ${year - birth} years old.';
'Hello, ' + name + '! You are ' + (year - birth).toString() + ' y...';
このガイドラインは、複数のリテラルと値を組み合わせる場合に適用されます。単一のオブジェクトを文字列に変換する場合は、.toString()
を使用しても問題ありません。
必要がない場合は、補間で中括弧を使用しない
#リンタールール:unnecessary_brace_in_string_interps
単純な識別子を補間していて、すぐに英数字が続かない場合は、{}
を省略する必要があります。
var greeting = 'Hi, $name! I love your ${decade}s costume.';
var greeting = 'Hi, ${name}! I love your ${decade}s costume.';
コレクション
#Dartは、リスト、マップ、キュー、セットの4つのコレクション型をサポートしています。次のベストプラクティスはコレクションに適用されます。
可能な限りコレクションリテラルを使用する
#リンタールール:prefer_collection_literals
Dartには、List、Map、Setの3つのコアコレクション型があります。MapとSetクラスは、ほとんどのクラスと同様に名前なしコンストラクターを持っています。しかし、これらのコレクションは非常に頻繁に使用されるため、Dartにはそれらを作成するためのより優れた組み込み構文があります。
var points = <Point>[];
var addresses = <String, Address>{};
var counts = <int>{};
var addresses = Map<String, Address>();
var counts = Set<int>();
このガイドラインは、これらのクラスの名前付きコンストラクターには適用されません。List.from()
、Map.fromIterable()
などは、すべて用途があります。(Listクラスにも名前なしコンストラクターがありますが、null安全なDartでは禁止されています。)
コレクションリテラルは、他のコレクションの内容を含めるためのスプレッド演算子と、内容を作成しながら制御フローを実行するためのif
とfor
にアクセスできるため、Dartでは特に強力です。
var arguments = [
...options,
command,
...?modeFlags,
for (var path in filePaths)
if (path.endsWith('.dart')) path.replaceAll('.dart', '.js')
];
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_empty、prefer_is_not_empty
Iterableコントラクトでは、コレクションがその長さを知っている、または一定時間でそれを提供できるとは要求されていません。コレクションに何かが含まれているかどうかを確認するためだけに.length
を呼び出すのは、非常に遅くなる可能性があります。
代わりに、より高速で読みやすいゲッターがあります:.isEmpty
と.isNotEmpty
。結果を否定する必要がない方を使用してください。
if (lunchBox.isEmpty) return 'so hungry...';
if (words.isNotEmpty) return words.join(' ');
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では、シーケンスを反復処理する場合は、ループを使用するのが慣用的な方法です。
for (final person in people) {
...
}
people.forEach((person) {
...
});
このガイドラインは、「関数リテラル」を具体的に言及していることに注意してください。各要素に対して既に存在する関数を呼び出したい場合は、forEach()
を使用しても問題ありません。
people.forEach(print);
また、Map.forEach()
を使用しても常に問題ないことに注意してください。マップは反復可能ではないため、このガイドラインは適用されません。
結果の型を変更する意図がない限り、List.from()
を使用しないでください。
#Iterableが与えられると、同じ要素を含む新しいListを生成する2つの明白な方法があります。
var copy1 = iterable.toList();
var copy2 = List.from(iterable);
明白な違いは、最初のものが短いことです。重要な違いは、最初のものが元のオブジェクトの型引数を保持することです。
// Creates a List<int>:
var iterable = [1, 2, 3];
// Prints "List<int>":
print(iterable.toList().runtimeType);
// Creates a List<int>:
var iterable = [1, 2, 3];
// Prints "List<dynamic>":
print(List.from(iterable).runtimeType);
型を変更したい場合は、List.from()
を呼び出すと便利です。
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()
を使用できます。
var objects = [1, 'a', 2, 'b', 3];
var ints = objects.where((e) => e is int);
これは冗長ですが、さらに悪いことに、おそらく望まない型のIterableを返します。この例では、フィルタリングする型がint
であるにもかかわらず、Iterable<Object>
を返します。
上記のエラーをcast()
を追加することで「修正」するコードを見かけることがあります。
var objects = [1, 'a', 2, 'b', 3];
var ints = objects.where((e) => e is int).cast<int>();
これは冗長で、2つのラッパーが作成され、2つの間接レベルと冗長なランタイムチェックが発生します。幸いにも、コアライブラリには、このユースケースに最適なwhereType()
メソッドがあります。
var objects = [1, 'a', 2, 'b', 3];
var ints = objects.whereType<int>();
whereType()
を使用すると簡潔で、目的の型のIterableが生成され、不要なラッピングレベルがありません。
近くの操作で済む場合は、cast()
を使用しないでください。
#Iterableやストリームを処理する場合、多くの場合、いくつかの変換を実行します。最後に、特定の型引数を持つオブジェクトを作成したいとします。cast()
の呼び出しを追加する代わりに、既存の変換のいずれかで型を変更できるかどうかを確認してください。
既にtoList()
を呼び出している場合は、それをList<T>.from()
(ここでT
は必要な結果リストの型)への呼び出しに置き換えてください。
var stuff = <dynamic>[1, 2];
var ints = List<int>.from(stuff);
var stuff = <dynamic>[1, 2];
var ints = stuff.toList().cast<int>();
map()
を呼び出す場合は、目的の型の反復可能オブジェクトを生成するように、明示的な型引数を指定してください。型推論は、map()
に渡す関数に基づいて、多くの場合正しい型を選択しますが、明示的にする必要がある場合もあります。
var stuff = <dynamic>[1, 2];
var reciprocals = stuff.map<double>((n) => 1 / n);
var stuff = <dynamic>[1, 2];
var reciprocals = stuff.map((n) => 1 / n).cast<double>();
cast()
の使用は避けてください
#これは前の規則のより緩やかな一般化です。オブジェクトの型を修正するために使用できる近くの操作がない場合があります。それでも、可能な限り、cast()
を使用してコレクションの型を「変更」することは避けてください。
代わりに、これらのいずれかのオプションを使用することをお勧めします。
正しい型で作成する。コレクションが最初に作成されるコードを変更して、正しい型になるようにします。
アクセス時に要素をキャストする。コレクションをすぐに反復処理する場合は、反復処理内で各要素をキャストします。
List.from()
を使用して積極的にキャストする。最終的にコレクションのほとんどの要素にアクセスし、元のライブオブジェクトによってバックアップされるオブジェクトを必要としない場合は、List.from()
を使用して変換します。cast()
メソッドは、すべての操作で要素型をチェックする遅延コレクションを返します。少数の要素に対して少数の操作しか実行しない場合、その遅延は良い場合がありますが、多くの場合、遅延検証とラッピングのオーバーヘッドは利点を上回ります。
正しい型で作成する例を次に示します。
List<int> singletonList(int value) {
var list = <int>[];
list.add(value);
return list;
}
List<int> singletonList(int value) {
var list = []; // List<dynamic>.
list.add(value);
return list.cast<int>();
}
アクセス時に各要素をキャストする例を次に示します。
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);
}
}
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()
を使用して積極的にキャストする例を次に示します。
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];
}
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
最新の言語は、ローカルネストされた関数とクロージャがいかに便利であるかを認識しています。関数の内部に関数を定義することは一般的です。多くの場合、この関数はすぐにコールバックとして使用され、名前を付ける必要はありません。関数式はそれに最適です。
しかし、名前を付ける必要がある場合は、ラムダを変数にバインドする代わりに、関数宣言文を使用してください。
void main() {
void localFunction() {
...
}
}
void main() {
var localFunction = () {
...
};
}
ティアオフで済む場合、ラムダを作成しない
#リンタールール: unnecessary_lambdas
関数、メソッド、または名前付きコンストラクターを括弧なしで参照すると、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);
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 で変数を最適に使用する方法について説明します。
ローカル変数では var
と final
に一貫したルールに従ってください。
#ほとんどのローカル変数には型注釈を含める必要がなく、var
または final
のみを宣言して使用する必要があります。どちらを使用するかについては、広く使用されている2つのルールがあります。
再代入されないローカル変数には
final
を、再代入される変数にはvar
を使用します。再代入されないローカル変数も含め、すべてのローカル変数に
var
を使用します。ローカル変数にはfinal
を決して使用しないでください。(フィールドおよびトップレベル変数にはfinal
を使用することをお勧めします)。
どちらのルールも許容されますが、1つを選択してコード全体に一貫して適用してください。そうすれば、リーダーが var
を見たときに、それが関数内で後で変数が代入されることを意味するかどうかがわかります。
計算できるものは保存しない
#クラスを設計する際には、同じ基礎となる状態への複数のビューを公開したいことがよくあります。コンストラクターですべてのビューを計算して保存するコードをよく目にします。
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
が変更可能であるにもかかわらず、そうすることはありません。異なる値を代入しても、area
と circumference
は以前の(現在は正しくない)値を保持します。
キャッシュの無効化を正しく処理するには、これを行う必要があります。
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;
}
}
これは、記述、保守、デバッグ、および読み取りに非常に多くのコードが必要です。代わりに、最初の実装は次のようになります。
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にはこの制限がありません。フィールドとゲッター/セッターは完全に区別できません。クラスでフィールドを公開し、後でゲッターとセッターでラップすることができます。そのフィールドを使用するコードに触れる必要はありません。
class Box {
Object? contents;
}
class Box {
Object? _contents;
Object? get contents => _contents;
set contents(Object? value) {
_contents = value;
}
}
読み取り専用のプロパティを作成するには、final
フィールドの使用を優先してください。
#外部コードが表示できるが割り当てできないフィールドがある場合、多くの場合に機能する簡単な解決策は、それをfinal
でマークすることです。
class Box {
final contents = [];
}
class Box {
Object? _contents;
Object? get contents => _contents;
}
もちろん、コンストラクターの外部で内部的にフィールドに代入する必要がある場合は、「プライベートフィールド、パブリックゲッター」パターンを使用する必要があるかもしれませんが、必要になるまで使用しないでください。
単純なメンバーには =>
の使用を検討してください。
#リンタールール: prefer_expression_function_bodies
関数式に =>
を使用することに加えて、Dart ではそれを用いてメンバーを定義することもできます。このスタイルは、値を計算して返すだけの単純なメンバーに適しています。
double get area => (right - left) * (bottom - top);
String capitalize(String name) =>
'${name[0].toUpperCase()}${name.substring(1)}';
コードを記述する人は =>
を好むようですが、それを乱用して、読み取りが困難なコードになってしまうことは非常に簡単です。宣言が数行を超える場合、または深くネストされた式(カスケードや条件演算子は一般的な違反者です)が含まれている場合は、自分自身とコードを読まなければならないすべての人のために、ブロックボディとステートメントを使用してください。
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;
}
Treasure? openChest(Chest chest, Point where) => _opened.containsKey(chest)
? null
: _opened[chest] = (Treasure(where)..addAll(chest.contents));
値を返さないメンバーにも =>
を使用できます。これは、セッターが小さく、=>
を使用する対応するゲッターがある場合に慣用句です。
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つは、同じ名前のローカル変数がアクセスしたいメンバーをシャドウイングする場合です。
class Box {
Object? value;
void clear() {
this.update(null);
}
void update(Object? value) {
this.value = value;
}
}
class Box {
Object? value;
void clear() {
update(null);
}
void update(Object? value) {
this.value = value;
}
}
もう1つのthis.
を使用する必要があるのは、名前付きコンストラクターにリダイレクトする場合です。
class ShadeOfGray {
final int brightness;
ShadeOfGray(int val) : brightness = val;
ShadeOfGray.black() : this(0);
// This won't parse or compile!
// ShadeOfGray.alsoBlack() : black();
}
class ShadeOfGray {
final int brightness;
ShadeOfGray(int val) : brightness = val;
ShadeOfGray.black() : this(0);
// But now it will!
ShadeOfGray.alsoBlack() : this.black();
}
コンストラクターパラメーターは、コンストラクターイニシャライザーリストでは決してフィールドをシャドウイングしません。
class Box extends BaseBox {
Object? value;
Box(Object? value)
: value = value,
super(value);
}
これは驚くように見えますが、期待どおりに機能します。幸いなことに、初期化フォーマルとスーパーイニシャライザーのおかげで、このようなコードは比較的まれです。
可能な限り、宣言時にフィールドを初期化する
#フィールドがコンストラクターパラメーターに依存しない場合は、宣言時に初期化でき、する必要があります。コードが少なくなり、クラスに複数のコンストラクターがある場合の重複が回避されます。
class ProfileMark {
final String name;
final DateTime start;
ProfileMark(this.name) : start = DateTime.now();
ProfileMark.unnamed()
: name = '',
start = DateTime.now();
}
class ProfileMark {
final String name;
final DateTime start = DateTime.now();
ProfileMark(this.name);
ProfileMark.unnamed() : name = '';
}
一部のフィールドは、たとえば他のフィールドを参照したり、メソッドを呼び出したりするためにthis
を参照する必要があるため、宣言時に初期化できません。ただし、フィールドがlate
でマークされている場合、イニシャライザーはthis
にアクセスできます。
もちろん、フィールドがコンストラクターパラメーターに依存している場合、または異なるコンストラクターによって異なる方法で初期化される場合は、このガイドラインは適用されません。
コンストラクタ
#次のベストプラクティスは、クラスのコンストラクターの宣言に適用されます。
可能な限り初期化形式を使用する
#リンタールール: prefer_initializing_formals
多くのフィールドは、次のようにコンストラクターパラメーターから直接初期化されます。
class Point {
double x, y;
Point(double x, double y)
: x = x,
y = y;
}
フィールドを定義するには、ここでx
を4回入力する必要があります。もっとうまくできます。
class Point {
double x, y;
Point(this.x, this.y);
}
コンストラクターパラメーターの前にあるこのthis.
構文は、「初期化フォーマル」と呼ばれます。常にそれを利用できるわけではありません。名前付きパラメーターの名前が初期化しているフィールドの名前と一致しないようにする場合があります。しかし、初期化フォーマルを使用できる場合は、使用する必要があります。
コンストラクターイニシャライザーリストを使用できる場合は、late
を使用しないでください。
#Dartでは、null 許容ではないフィールドは読み取られる前に初期化する必要があります。フィールドはコンストラクター本体内で読み取ることができるため、本体の実行前にnull 許容ではないフィールドを初期化しないと、エラーが発生します。
フィールドをlate
でマークすることにより、このエラーを解消できます。これにより、コンパイル時エラーが、フィールドが初期化される前にアクセスした場合の実行時エラーになります。これは、場合によっては必要なものですが、多くの場合、正しい修正は、コンストラクターイニシャライザーリストでフィールドを初期化することです。
class Point {
double x, y;
Point.polar(double theta, double radius)
: x = cos(theta) * radius,
y = sin(theta) * radius;
}
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 コンストラクターには必須です)。
class Point {
double x, y;
Point(this.x, this.y);
}
class Point {
double x, y;
Point(this.x, this.y) {}
}
new
は使用しないでください。
#リンタールール: unnecessary_new
コンストラクターを呼び出す場合、new
キーワードは省略可能です。ファクトリコンストラクターは、new
呼び出しが実際に新しいオブジェクトを返さない可能性があるため、その意味は明確ではありません。
言語はまだnew
を許可していますが、非推奨と見なして、コードで使用することは避けてください。
Widget build(BuildContext context) {
return Row(
children: [
RaisedButton(
child: Text('Increment'),
),
Text('Click!'),
],
);
}
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
を省略することを許可します。
const primaryColors = [
Color('red', [255, 0, 0]),
Color('green', [0, 255, 0]),
Color('blue', [0, 0, 255]),
];
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
はスタックトレースを最後にスローされた位置にリセットします。
try {
somethingRisky();
} catch (e) {
if (!canHandle(e)) throw e;
handle(e);
}
try {
somethingRisky();
} catch (e) {
if (!canHandle(e)) rethrow;
handle(e);
}
非同期処理
#Dartには、非同期プログラミングをサポートするためのいくつかの言語機能があります。次のベストプラクティスは、非同期コーディングに適用されます。
生のFutureを使用するよりもasync/awaitを優先する
#非同期コードは、Futureのような優れた抽象化を使用した場合でも、読み取りとデバッグが非常に困難です。async
/await
構文は可読性を向上させ、非同期コード内ですべての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;
}
}
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
を省略できる場合は、そうしてください。
Future<int> fastestBranch(Future<int> left, Future<int> right) {
return Future.any([left, right]);
}
Future<int> fastestBranch(Future<int> left, Future<int> right) async {
return Future.any([left, right]);
}
async
が役に立つケースには、以下が含まれます。
await
を使用している場合。(これは明らかなものです。)非同期的にエラーを返している場合。
async
とthrow
はreturn Future.error(...)
よりも短いです。値を返し、それをFutureで暗黙的にラップしたい場合。
async
はFuture.value(...)
よりも短いです。
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クラスを見つけて使用します。
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()
を使用する必要があります。
Future<bool> fileContainsBear(String path) {
return File(path).readAsString().then((contents) {
return contents.contains('bear');
});
}
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
を実装するため、T
がObject
でインスタンス化できる型パラメーターである場合、is Object
またはis T
は、オブジェクトがFutureの場合でもtrueを返します。代わりに、Future
のケースを明示的にテストします。
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;
}
}
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>
を渡すと、それを単純な同期値として誤って扱います。
特に記載がない限り、このサイトのドキュメントはDart 3.5.3を反映しています。ページは2024年10月16日に最後に更新されました。 ソースを表示 または 問題を報告する.