Flutter Advent Calendar の5日目です。
Effective Dartを意訳してまとめました。
他の言語でも共通で言えるようなことも多く記載されています。その辺りは一部割愛しましたが、Dartを書くにあたって知っておいた方がよいと私が感じた部分をまとめました。(それでもかなり長いですが…)
また、ここでは割愛しますが、Flutterとしてのスタイルガイドはこちらに記載されているので、併せて読むと良いと思います。
Style guide for Flutter repo
Style Guide
命名規則
対応style guide
-
クラス名:UpperCamelCase
- 例)
SampleDataClass
- 例)
-
ファイル/ディレクトリ/パッケージ名:lowercase_with_underscores
- 例)
page/sample_first/sample_first
- 例)
- フィールド名:lowerCamelCase
- 例)
familyName
- 例)
-
定数名:lowerCamelCase
- プレフィックスも使わない
- OK:
parameter
- NG:
kParameter
- OK:
- プレフィックスも使わない
- import prefix: lowercase_with_underscores
import 'dart:math' as math; // DO NOT `Math`
import 'package:angular_components/angular_components'
as angular_components;// DO NOT `angularComponents`
import 'package:js/js.dart' as js; // DO NOT `JS`
略語、頭文字
大文字の略語が連続すると読みづらくなるため、通常の単語のように先頭のみを大文字にします。
例:HTTP > Http
ただし、2文字の場合は以下の例外が適用されます
- 2文字の頭文字の場合は大文字で表示
- 例)input/output → IO
- 2文字の略語の場合は、先頭のみ大文字
- 例)identification → Id
HttpConnectionInfo // do NOT HTTPConnection
uiHandler // do NOT UiHandler
IOStream // do NOT IoStream
HttpRequest // do NOT HTTPRequest
Id // do NOT ID
DB // do NOT Db
※ドキュメントが分かりやすく更新されているとのコメントをいただきましたので、修正いたしました。
import順(directives_ordering)
-
'dart:'
を最初にもってくる - 次に
'package:'
- 自分のパッケージより先に外部パッケージを先に書く
- 最後に
export
を書く - アルファベット順で並べる
- 各セクションの間に空行を入れて視認性を確保する
フォーマット
1行の文字数は80文字まで(lines_longer_than_80_chars)
- URIやpathなどは例外として認められる
- 逆に、長い分探しやすくなる
- 長い文字列も除外される
Android Studioで80文字以上を入力出来る様にする方法
Android Studio > Preferences...
を開き、Editor > Code Style > Dart
のWrapping and Braces
タブのHard wrap at
の数値を変更すれば反映されます。
すべての制御分にカッコをつける(curly_braces_in_flow_control_structures)
if (isWeekDay) {
print('Bike to work!');
} else {
print('Go dancing or read a book!');
}
// 1行のシンプルな文はカッコなしでもOK
if (arg == null) return defaultValue;
※1行で収まるようなシンプルな文はカッコがなくても良い、とあるが、analysis_options.yaml
では怒られた気がする
Documentation Guide
コメント
一般的なコメントは//
で書き、ドキュメントコメントは///
を使います。swiftと同じですね。
// Not if there is nothing before it.
if (_chunks.isEmpty) {
return false;
}
/// The number of characters in this chunk when unsplit.
int get length => ...
一方、/** ... */
はコードブロックの一時的なコメントアウトに利用し、コメントには使わないようにします。
/** 一時的にコメントアウト
void doSomething() {
...
}
*/
ドキュメントコメント (slash_for_doc_comments)
- 型や関数は
[]
で囲む - サマリーを1行目に書き、2行目は開ける
- コードサンプルを書く場合は```で囲む
- 他の言語でよくある
@param
などは使用せず、文章で説明する
/// Defines a flag.
///
/// Throws an [ArgumentError] if there is already an option named [name] or
/// there is already an option using abbreviation [abbr]. Returns the new flag.
///
/// ```
/// addFlag('sampleName', '.....');
/// ```
Flag addFlag(String name, String abbr) => ...
Usage Guide
Libraries
part of
ではファイルを直接指すURIを指定する
part
とpart of
は対の構文になっていて、簡単に言うとファイル分割の手段として利用できます。
レガシーな理由から、part of
には親のファイル名だけを指定できますが、分かりにくくなるため、直接ファイルへのpathを記述するようにしましょう。
// [part of my_library]と省略しない
part of "../../my_library.dart";
Booleans
オプショナルな場合は??
を使いましょう
// bad
if (optionalThing?.isEnabled) {
print("Have enabled thing.");
}
// good(nullをtrueとして扱いたい場合)
if (optionalThing?.isEnabled ?? true) {
print("Have enabled thing.");
}
// good(nullをfalseとして扱いたい場合)
if (optionalThing?.isEnabled ?? false) {
print("Have enabled thing.");
}
Strings
長い文字列の連結にも+
は不要(prefer_adjacent_string_concatenation)
改行した場合、+
がなくても文字列連結は可能です。
// bad
message = 'ERROR: Parts of the spaceship are on fire. Other ' +
'parts are overrun by martians. Unclear which are which.';
// good
message = 'ERROR: Parts of the spaceship are on fire. Other '
'parts are overrun by martians. Unclear which are which.';
値を繋げる場合は$
、${}
を使う(prefer_interpolation_to_compose_strings)
// good
'Hello, $name! You are ${year - birth} years old.';
// bad
'Hello, ' + name + '! You are ' + (year - birth).toString() + ' y...';
{}
の付ける・付けないの基準は以下の通りです
-
$
:途中にドットや式などが入らない場合 -
${}
:式が入る場合や、スペースなどなしで文字列が続く場合
'Hi, $name!'
"Wear your wildest $decade's outfit." // [']は区切り文字になるので、[$]でOK
'Wear your wildest ${decade}s outfit.' // 複数系の[s]がすぐ後ろに繋がるため、[${}]を使う
Collections
List/Mapの生成は[]/{}(prefer_collection_literals)
var points = []; // List()は使わない
var addresses = {}; // Map()は使わない
// 型を宣言する場合
var points = <Point>[];
var addresses = <String, Address>{};
CollectionがemptyかどうかはisEmpty/isNotEmptyを使う
// length == 0とは書かない
if (lunchBox.isEmpty) return 'so hungry...';
// [!words.isEmpty]とは書かない
if (words.isNotEmpty) return words.join(' ');
forEachの中で関数リテラルは避ける(avoid_function_literals_in_foreach_calls)
Dartではシーケンスを反復処理する場合、forEachではなく、for-loopが慣用的とのこと。
// bad
people.forEach((person) {
...
});
// good
for (var person in people) {
...
}
// good:各要素で既存の関数を呼び出したい場合は、下記のようにスマートに書ける
people.forEach(print);
List.from() は型を変更したいとき以外は使わない
Listをコピーする場合、List.from(iterable)
とiterable.toList()
がありますが、違いは以下の通りです
iterable.toList()
:コピーされたリストに型は引き継がれる&シンプル
List.from(iterable)
:コピーされたリストの型はdynamicになる
なので、基本はtoList()
が推奨されますが、以下のような特殊な場合はList.from
が便利です。
var numbers = [1, 2.3, 4]; // List<num>(intとdoubleが混在している)
numbers.removeAt(1); // doubleの[2.3]がremoveされたため、intだけのListになっている
// この場合はList.fromの方が型を変更できるため有用
var ints = List<int>.from(numbers);
Collectionから特定の型だけ抜き出す場合はwhereType
を使う(prefer_iterable_whereType)
var objects = [1, "a", 2, "b", 3];
// bad
var ints = objects.where((e) => e is int);
// good
var ints = objects.whereType<int>();
iterableやstreamを変換する際、極力cast()
は避ける
var stuff = <dynamic>[1, 2];
// bad
var reciprocals = stuff.map((n) => 1 / n).cast<double>();
// good:途中のオペレータで型を指定できるのであれば、cast()の使用を避けられる
var reciprocals = stuff.map<double>((n) => 1 / n);
Functions
lambdaの引数と関数の引数が同じ場合は、lambdaを省略することができる(tear-off
)(unnecessary_lambdas)
これはみた方が早いです。
// bad
names.forEach((name) {
print(name);
});
// good
names.forEach(print);
ラムダで渡される引数と、関数に渡す引数が同じ場合は、ラムダの部分の記述を省略できます。
*tear-offについてのご指摘がありましたので、一部表現を修正しました
名前付き引数のデフォルト値には=
を使う(prefer_equal_for_default_values)
Dartでは過去の経緯から=
と:
が許容されていますが、=
を使ってくださいとのことです。
// good
void insert(Object item, {int at = 0}) { ... }
// bad
void insert(Object item, {int at: 0}) { ... }
オプショナルな引数のデフォルト値に、nullを明示的にセットしないで良い(avoid_init_to_null)
デフォルトでnullになるため、わざわざnullをデフォルト値として書かなくても良いです。
// good
void error([String message]) {
stderr.write(message ?? '\n');
}
// bad
void error([String message = null]) {
stderr.write(message ?? '\n');
}
Variables
変数の初期化に= null
は不要
何も指定しない場合はデフォルトでnullになるため、初期化にわざわざnullを記述する必要はありません。
あとから計算できる値を保持しない
パフォーマンスのために計算結果などを予めデータクラスの変数に持たせる場合がありますが、極力避ける様にしてください。実際にパフォーマンスの問題などがある場合は、コメントなどを残してください。
// bad:CPU使用量を減らす代わりに、メモリを浪費している
class Circle {
num radius;
num area;
num circumference;
Circle(num radius)
: radius = radius,
area = pi * radius * radius,
circumference = pi * 2.0 * radius;
}
// good
class Circle {
num radius;
Circle(this.radius);
num get area => pi * radius * radius;
num get circumference => pi * 2.0 * radius;
}
また、厳密にはbad
の例はradius
を後から変更できるようになっていて、その場合area
とcircumference
の再計算が必要になるため、厳密なコードはより複雑になります。
Members
不必要にgetter/setterを作らない(unnecessary_getters_setters)
Javaのように、privateなメンバーのsetter/getterは作らない。
// bad
class Box {
var _contents;
get contents => _contents;
set contents(value) {
_contents = value;
}
}
// good
class Box {
var contents;
}
不変なメンバーにはfinalを付ける(unnecessary_getters)
単純なメンバーには=>
を使う(prefer_expression_function_bodies)
値を計算して返すだけの単純なメンバーには=>
の採用も考えてください。
// good
double get area => (right - left) * (bottom - top);
bool isReady(num time) => minTime == null || minTime <= time;
複数行になるような処理などは、可読性が下がるため推奨されません。
this.
は極力使わない(unnecessary_this)
冗長になるので、省略できる場合はthis.
を書かないようにしましょう。
例外は以下の2つです。
(1)同じ名前のローカル変数がある場合
class Box {
var value;
void update(value) {
// この場合は区別するために[this]が必要
this.value = value;
}
}
ただし、コンストラクタの中ではthis
が不要です。
class Box extends BaseBox {
var value;
Box(value)
: value = value, // ここに[this]は不要。左辺が[this.value]であるのは自明だから。
super(value);
}
(2)Named Constructorにアクセスする場合
class ShadeOfGray {
final int brightness;
ShadeOfGray(int val) : brightness = val;
ShadeOfGray.black() : this(0);
// ここは名前付きコンストラクタにアクセスするため[this]が必要
ShadeOfGray.alsoBlack() : this.black();
}
フィールドの初期化は極力宣言時に行う
コンストラクタに依存しない場合などは宣言時に初期化しましょう。
コンストラクタが複数ある場合など、初期化漏れを防ぐことができます。
Constructors
initializing formal
を使う(prefer_initializing_formals)
コンストラクタの引数を直接フィールドに代入できます。
class Point {
num x, y;
Point(this.x, this.y);
}
ちなみに、initializing formal
に型は不要です。
// bad
class Point {
int x, y;
Point(int this.x, int this.y);
}
emptyなコンストラクタのbodyは{}
ではなく;
を使う(empty_constructor_bodies)
class Point {
int x, y;
Point(this.x, this.y);
// Point(this.x, this.y){} とは書かない
}
new
は使わない(unnecessary_new)
Dart 2からnew
は不要になったので使わないようにしましょう。
重複するconst
は消しましょう(unnecessary_const)
親オブジェクトがconst
の場合はその中身も必然的にconst
になるので、その場合は親のみconst
指定にしましょう。
// good
const primaryColors = [
Color("red", [255, 0, 0]),
];
// bad
const primaryColors = const [ // 右側のconstは重複のため削除できます
const Color("red", const [255, 0, 0]), // Colorと配列の前の2箇所の[const]は重複なので削除できます
];
エラーハンドリング
on句のないcatchは避け、意図したExceptionのみcatchすること(avoid_catches_without_on_clauses)
Javaでも一般的に言われていることです。try-catchの際にExceptionをすべてcatchするのではなく、意図したExceptionのみをcatchするようにしましょう。
また、万が一on句のないcatchを使った場合も、そこで捉えたエラーを破棄することはしないようにしましょう。
try {
somethingRisky()
}
on Exception catch(e) { // catch(e)とはせず、catchするExceptionを明示する
doSomething(e);
}
ErrorとException
Error
はプログラムエラーに対して発行します。この場合はcatchせずにコード上で解決させます。
一方、Exception
はランタイムで起こりうる例外で、適切にcatchしてあげる必要があります。
※stachOverflowより
rethrow
を使う
例外をcatchして、それを再度throwする場合はrethrow
を使ってください。
こうすると、スタックトレースを保持することができます。
一方、throwし直した場合のスタックトレースは、最後にthrowした部分までしか追跡できません。
try {
somethingRisky();
} catch (e) {
if (!canHandle(e)) {
// [throw e]と書くと、今までのスタックトレースが追跡できなくなります
rethrow;
}
handle(e);
}
非同期処理
async/await
を使って、直接Future
を操作しないようにしましょう
async/await
を使った方が可読性やデバッグのしやすさが向上します。
Future<int> countActivePlayers(String teamName) async { // ここで[async]
try {
var team = await downloadTeam(teamName); // ここで[await]
if (team == null) return 0;
var players = await team.roster; // ここで[await]
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が有用なケース
- コード内で
await
を使っている場合 - 戻り値が
Future
で、Error
を非同期で返却する場合 - 戻り値が
Future
で、Future
で暗黙的にラップしたい場合
// 2. [Error]を非同期で返却する場合
// →[Future.error(...)]と書くより簡潔
Future asyncError() async {
throw 'Error!';
}
// 3. [Future]でラップしたい場合
// →[Future.value(...)]と書くより簡潔
Future asyncValue() async => 'value';
Completer
は直接使用しない
Completer
で実現できることは、だいたいFuture.then()
やasync/await
で実現できます。また、後者の方が簡潔に書けるため、そちらを推奨します。
Effective Dartで指摘されているのは、初学者はFutureのコンストラクタに適したものが見つからないため、Completerを使ってしまう傾向があるが、より簡潔な書き方を学びましょうとのことです。
Future<bool> fileContainsBear(String path) {
var completer = Completer<bool>();
File(path).readAsString().then((contents) {
completer.complete(contents.contains('bear'));
});
return completer.future;
}
Future<bool> fileContainsBear(String path) {
return File(path).readAsString().then((contents) {
return contents.contains('bear');
});
}
// async/awaitでも書ける
Future<bool> fileContainsBear(String path) async {
var contents = await File(path).readAsString();
return contents.contains('bear');
}
FutureOr<T>
をcastする際、is Future<T>
を先にチェックする
型引数がFutureOr<T>
の場合、一般的には以下のようにFuture<T>
またはT
にcastして処理を行うことがあります。
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 as T;
}
}
このとき、T
がint
などプリミティブ型であれば問題はないのですが、Objectの場合は以下のコードでは問題が発生します。
Future<T> logValue<T>(FutureOr<T> value) async {
if (value is T) { // 先に[T]で型チェックをしている
print(value);
return value;
} else {
var result = await value;
print(result);
return result;
}
}
このとき、実はFuture<Object>
自体がObject
を実装してしまいます。
すると上記の例では、渡された引数がFuture<Object>
だった場合、if (value is T)
の条件が正となってしまうため、非同期値Future<Object>
が、同期値であるObject
として扱われてしまいます。
ですので、型のチェックを行う際は必ずFuture<T>
を先に行うようにしましょう。
Design Guide
Names
Bool型の名前付きパラメータの場合、動詞を省略することを検討する
Bool値の場合、動詞が明確であることも多く、記述量も減って分かりやすくなります。
Isolate.spawn(entryPoint, message, paused: false);
var copy = List.from(elements, growable: true);
var regExp = RegExp(pattern, caseSensitive: false);
Bool型のパラメータはPositiveな意味を優先する
Positiveな意味を優先した方がコードの可読性は上がります。
ただし、ネガティブな意味を頻繁に使う場合などは、ネガティブな意味の命名をしても構いません。
要は、コードで!
を頻繁に使わなくて済むような命名を心がけます。
get
から始まるメソッドは避ける
getBreakfastOrder()
というメソッドがあるとしたら、get breakfastOrder
として、getterを用意すればよくなります。
一方、getの中の処理が重要な場合は、downloadやcalculateなどの動詞を付与することを検討しましょう。
オブジェクトの状態をコピーして別のオブジェクトを生成する場合はtoXXXX
とする(use_to_and_as_if_applicable)
list.toSet();
stackTrace.toString();
dateTime.toLocal();
オブジェクトの型変換をする場合はasXXX
とする(use_to_and_as_if_applicable)
var map = table.asMap();
var list = bytes.asFloat32List();
var future = subscription.asFuture();
Libraries
宣言はできるだけprivateにする
Dartでは先頭に_
を付けることでprivateになります。privateにしておくと、未使用の場合に静的解析で警告が表示されます。publicの場合は、外部のどこで使用されているかわからないため、静的解析の対象外となります。
また、privateにすることで出来ることが制限されるため、使用者側の理解も容易になります。
Classes/Mixins
ひとつのメンバーだけを持つabstractクラスは定義しない(one_member_abstracts)
必要なものがコールバックのようなものであれば、abstractクラスではなく、typedefで関数を定義した方が簡潔に済みます。
abstract class Predicate<E> {
bool test(E element);
}
typedef Predicate<E> = bool Function(E element);
静的メンバーのみのクラスは定義しない(avoid_classes_with_only_static_members)
Javaなどと違って、Dartにはトップレベルの関数、変数、定数があるため、何かを定義するためだけにクラスは必要ありません。
名前の重複などを防ぐ目的で、必要なものが名前空間である場合はライブラリの方が適しています。
DateTime mostRecent(List<DateTime> dates) {
return dates.reduce((a, b) => a.isAfter(b) ? a : b);
}
const _favoriteMammal = 'weasel';
class DateUtils {
static DateTime mostRecent(List<DateTime> dates) {
return dates.reduce((a, b) => a.isAfter(b) ? a : b);
}
}
class _Favorites {
static const mammal = 'weasel';
}
ただし、厳密に守る必要はなく、定数と列挙型のタイプではそれらをクラスにグループ化するのが自然かもしれません。
class Color {
static const red = '#f00';
static const green = '#0f0';
static const blue = '#00f';
static const black = '#000';
static const white = '#fff';
}
サブクラス化を目的としないクラスは拡張を避ける
生成的コンストラクタ(Generative Constructors)からFactoryコンストラクタに変更された場合、元々の生成的コンストラクタを呼び出すサブコンストラクタは正しく機能しなくなる可能性があります。
これを防ぐため、IterableBase
などの分かりやすい名前を付けるか、ドキュメンテーションコメントで記述しておきます。逆に、サブクラス化をサポートする場合はそれを明記するようにします。
この辺は、Dart特有な思想かもしれません。
Interface
を目的としないクラスをimplements
するのを避ける
Dartにはinterface
というキーワードはありませんが、クラスを定義すると同名のinterfaceが生成され、implements
することができるようになります。(Implicit Interface:暗黙的インタフェース)
Dartのクラス継承、Implicit Interface、Mix-inについて
extends
との違いは、あくまでinterfaceであるため、元の実装は引き継がれない(superで呼ぶこともできない)点です。
そのため、元のクラスで実装が追加された場合は、当然implements
したクラスでも実装を追加する必要があります。それを防ぐため、interface
を目的としないクラスを無闇にimplements
を避ける必要もありますし、クラスの実装側はimplements
されることを想定している場合はドキュメンテーションコメントで記載しておきましょう。
mixin
を定義する際はmixin
キーワードをつける
mixin
自体は昔からDartにあった機能ですが、Dart2.1からはmixin
キーワードが追加され、明示的に示すことができるようになりました。
逆に、mixin
キーワードがなくてもmixin
は使用できますが、元のクラスがmixin
を想定していないケースもあり、危険なため極力避ける様にしましょう。
Constructors
コンストラクタをconst
に出来る場合はconst
を付与する
フィールドがすべてfinalで、コンストラクタでそれらを初期化するだけの場合、コンストラクタにconst
を付与することができます。
ただし、コンストラクタが変更され、後からconst
でなくなってしまった場合は、そのクラスを使っている箇所も変更する必要が出てくるため、注意が必要です。
Members
フィールドとトップレベル変数は可能な限りfinal
にする(prefer_final_fields)
publicなフィールドはミュータブルでfinalを付けるのが難しいこともあるかもしれませんが、privateなフィールドはfinalかどうかの判断は比較的容易に出来ると思います。
プロパティにアクセスするにはget
を使用します
外部からプロパティへのアクセス方法を、メソッドとgetterのどちらにするかは難しい問題ですが、Dartではgetterが推奨されています。
他の言語では、getterはフィールドにアクセスするだけだったり、軽量な計算のみを行う「高速なメソッド」、メソッドの場合は計算を必要とする「遅いメソッド」という棲み分けがされています。
しかし、Dartではドットアクセスした場合、どれも計算を実行できるメンバー呼び出しになり、以下の条件を満たせばgetterとして扱うことを推奨します。
- getterは引数を取らず、値を返すこと
- getterは結果のみを気にするようにして、「どのように」結果を返却するかを気にしないこと
- 「どのように」が重要な場合は動詞を付けてメソッドにする
- getterは計算が必要でも構わない
- 処理が高速であることは、getterの必要条件ではない
- ただし、あまりにも計算量が多い場合はその動作を説明する動詞を名前としたメソッドにする
- ユーザに見える副作用を及ぼしてはいけない
- 値をキャッシュしたりログ出力するなど、ユーザに見えない部分に関しては、問題ない
- 基本的に、何度callしても結果は変わらず同じ結果を返却する
-
DateTime.now
などは呼び出す度に異なる値になるため、getterにはできない - ただし、
list.length
のように明らかにオブジェクトの状態が変更されている場合は除く - ここでいう「同じ結果」とは、同一のオブジェクトである必要はない。それを強制すると、getterは脆弱なキャッシュを持つことになる上、getterを使用するすべてのポインタが同じになってしまうためです。
-
- 結果は、元のオブジェクトの状態の一部のみを返却します
- すべての状態を返却したい場合は、
toXXX
またはasXXX
を使います
- すべての状態を返却したい場合は、
プロパティを概念的に変更する操作の場合にはsetterを使用する(use_setters_to_change_properties)
setterなのかメソッドなのかの判断は、getterの場合と似ています。以下の例を見ていただくのが一番直感的にわかると思います。
// bad
rectangle.setWidth(3);
button.setVisible(false);
// good
rectangle.width = 3;
button.visible = false;
詳細に記述すると
- 単一の引数を取り、結果を返却しない
- 操作によりオブジェクトの状態が変更される
- 操作はべき等(同じ操作をしても結果は変わらない)である
- 内部的にキャッシュやロギングが発生したとしても、それは問題ない
- 使う側から見て変更が見えなければOK
対応するgetterなしでsetterのみを定義しない(avoid_setters_without_getters)
ユーザはsetter/getterをセットとして目に見えるプロパティとして捉えます。仮に、書き込みだけできて読み込みできないプロパティがあるとすると、混乱を招いてしまいます。また、getterがない場合、=
を用いて値の変更はできますが、+=
を使うことができず、不便になります。
// bad
class Bad {
int l, r;
set length(int newLength) {
r = l + newLength;
}
}
// good
class Good {
int l, r;
int get length => r - l;
set length(int newLength) {
r = l + newLength;
}
}
通常、必要以上の情報を公開すべきではありません。仮にgetterを用意できない場合などはメソッドを使用します。
戻り値がbool/int/double/numの場合はnullを返さない(avoid_returning_null)
bool/int/double/numのように小文字で始まるプリミティブ型はnonnullであることを前提でコーディングすることが多いため、基本的にnullを返さないようにします。
nullableであることが適している場合は、ドキュメントなどでそれを説明するようにします。
fluent interfaceを実現するためにthis
を返却するのは避ける(avoid_returning_this)
fluent interface、メソッドチェーンを実現するためにthis
を返却するテクニックよりも、DartのMethod cascades
を使いましょう。
// good(MethodCascades)
var buffer = StringBuffer()
..write('one')
..write('two')
..write('three');
// bad(fluent interface)
var buffer = StringBuffer()
.write('one')
.write('two')
.write('three');
ただし、以下の場合はthis
を返却してもOK
- オペレーター
- 現在のクラスとは異なる戻り値の型を持つメソッド
- 親クラス/mixin/interfaceで定義されたメソッド
Types
-
type annotated
: 明示的な型宣言 -
inferred
: 型推論- 失敗することもある
- 静的解析で失敗するとエラーになる
- 型を特定できない場合は
dynamic
になる
-
dynamic
: 動的
publicフィールドとTOPレベル変数の場合、型が明白でない場合に型を宣言する(type_annotate_public_apis)
// bad
install(id, destination) => ...
// good
Future<bool> install(PackageId id, String destination) => ...
「明白」の定義はありませんが、以下の場合などが該当します。
- 文字列リテラル
- コンストラクター呼び出し
- 型が明白な定数への参照
- 数値と文字列の簡単な式
- 一般的によく知られたようなメソッド
-
int.parse()
など
-
悩むような場合は、基本的に型宣言を書くことを推奨します。
また、型が明白でも、型が他のライブラリで定義された型などの場合は、型を明示した方が安全です。(そのライブラリの仕様が変更されて戻り値が変わった時に気付けない可能性があるため)
型が不明な場合は、privateフィールドとトップレベル変数には型を付ける(prefer_typing_uninitialized_variables)
// bad
class BadClass {
static var bar; // LINT
var foo; // LINT
void method() {
var bar; // LINT
bar = 5;
print(bar);
}
}
// good
class GoodClass {
static var bar = 7;
var foo = 42;
int baz; // OK
void method() {
int baz;
var bar = 5;
...
}
}
initializer等によって型が明白になる場合などを除き、privateフィールドでも型宣言を定義した方が可読性があがります。
初期化されたローカル変数には型宣言を記述しなくても良い(omit_local_variable_types)
以下のように特にスコープの小さく、型が明白なローカル変数の場合、型宣言を書かない方が分かりやすくなることがあります。
以下の例では、型を省略することで初期値や変数名など、より本質的な情報に読み手が集中することができます。
List<List<Ingredient>> possibleDesserts(Set<Ingredient> pantry) {
var desserts = <List<Ingredient>>[];
// [desserts]は初期化されているので、型が明白
// List<List<Ingredient>> desserts = <List<Ingredient>>[];
for (var recipe in cookbook) {
// [recipe]の型はList<Ingredient>であることが明白
if (pantry.containsAll(recipe)) {
desserts.add(recipe);
}
}
return desserts;
}
逆に、以下のように初期化されていない場合、型宣言がないとdynamicになるため、型宣言を付けます。
List<AstNode> parameters;
if (node is Constructor) {
parameters = node.signature;
} else if (node is Method) {
parameters = node.parameters;
}
関数式の推定パラメータには型宣言を避ける
// good
var names = people.map((person) => person.name);
// bad
var names = people.map((Person person) => person.name);
無名関数は、大抵すぐにメソッドに渡されます。
例えば、上記のIteratable.mapのように、そのままメソッドに渡されることがほとんど。このとき、Dartはmapの期待する型に推論することができます。
仮に、無名関数をフィールドとして保持しておき、あとからメソッドに渡す場合などは名前付きパラメータを使うと分かりやすくなります。
冗長な型宣言は避ける
型宣言と初期化で冗長な型宣言を避ける様にします。
// good
Set<String> things = Set();
// bad
Set<String> things = Set<String>();
また、以下の例では型情報が不足してしまうため、初期化時に型を定義する様にしています。
// good
var things = Set<String>();
// bad
var things = Set();
型推論が意図通り動作しない場合は、正しい型を宣言する
例えば、下記の例。
num highScore(List<num> scores) {
// ここでは[var]ではなく[num]で型定義が必要
num highest = 0;
for (var score in scores) {
if (score > highest) highest = score;
}
return highest;
}
highest
はゼロで初期化しているため、var highest = 0
と書きたくなりますが、この場合、highest
はint
型に推論されます。
この時、[scores]はnum型の配列のため、配列内に1.2
のようなdouble
型が含まれていた場合、highest
への代入はできずにエラーとなってしまいます。
そのため、明示的に[num]で型宣言をしてあげる必要があります。
dynamic
型は明示的に宣言する
Dartは型推論があるため、型宣言を省略することが可能です。仮に型推論ができなかった場合は動的にdynamic型として扱われます。そのため、当然dynamic型も記述を省略することは可能になっています。
しかし、コードの保守・運用面を考慮した場合、意図的にdynamic型を必要としている場合は、明示的にdynamicの宣言を記述するべきです。(型宣言が省略されていた場合、後からコードを読んだ人は、これが意図してdynamicになっているのか、本来別の型だったのが型推論できずに失敗してdynamic型になっているかの判断ができません)
※ちなみに、Dart2以前はこれと全く逆のことを推奨していたそうです。
関数型の定義は、戻り値・パラメータの型も宣言する
関数型Function
は戻り値・引数の宣言なしでdynamicのように使用することが可能です。が、極力戻り値・引数の型は宣言すべきです。
// good
bool isValid(String value, bool Function(String) test) => ...
// bad
bool isValid(String value, Function test) => ...
ただし、以下の様に戻り値・引数の型が動的になる場合は例外として扱われます。
void handleError(void Function() operation, Function errorHandler) {
try {
operation();
} catch (err, stack) {
// ここでerrorHandlerの型によって挙動を変えているため、宣言時に型を特定できない
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の戻り値voidは省略して良い
Dartではsetterの戻り値がvoidであることは自明なので、省略しましょう。
// good(先頭に戻り値のvoidは不要)
set foo(Foo value) { ... }
レガシーなtypedefの定義フォーマットは使用しない(prefer_generic_function_type_aliases)
Dartにはtypedefの記述方法が2パターンあります。
// bad(レガシー)
typedef int Comparison<T>(T a, T b);
// good
typedef Comparison<T> = int Function(T a, T b);
// good(パラメータ名は記述しなくてもOK)
typedef Comparison<T> = int Function(T, T);
レガシーなパターンでは、2つの課題があります
(1)
ジェネリクス関数型に名前を割り当てる方法がありません。レアなケースですが、以下の例をご覧ください。
Comparison1
、Comparison2
ともにジェネリクスの型は宣言していません。が、Comparison2
ではFunction<T>
と定義しているため、型推論時にint Function<T>(T, T)
と表示されているのが分かります。一方、Comparison1
はT
が抜け落ちて引数がdynamic型となってしまっています。
(2)
関数の引数に一つだけパラメータが記述された場合、それは型ではなく、パラメータ名として識別されます。例えば以下の例です。
TestNumber1
はレガシーな記述方法ですが、これだとbool Function(dynamic)
となっているのが分かります。TestNumber1
で定義したnum
はnum型ではなく、numというdynamic型のパラメータ名として扱われてしまいます。
レガシーな構文も、下位互換性のためサポートはされていますが、deprecated扱いとなっています。
一度しか使われない場合は、typedefよりもinlineで実装する(avoid_private_typedef_functions)
Dart1では関数型を使いたい場合、typedefで定義する必要がありましたが、Dart2では関数型構文がサポートされました。
関数型が長い場合や頻繁に登場する場合はtypedefが有用ですが、一度かぎりの場合などはinlineで書く方が読みやすくなります。
引数に関数型を導入することを検討する(use_function_type_syntax_for_parameters)
// Dart2以前
Iterable<T> where(bool predicate(T element)) {}
// better
Iterable<T> where(bool Function(T) predicate) {}
後者の方が少しだけ長くはなりますが、コード内で一貫性がでるので、後者の方を推奨しているようです。
どの型でも許容されている場合は dynamic
ではなく Object
を使う
まず、Object
と dynamic
の違いですが、こちらの記事にも記述されています。
Dartの不定型について
「本当に何型であっても良い」ことを示す際には
Object
を、「型Aと型Bのうちどちらか、のような、期待する型が決まってはいるがDartの型システムで表現できない型表現」をdynamic
でするべき、らしいです。
// [toString]はすべての型に許容されているため、[Object]を使用
void log(Object object) {
print(object.toString());
}
/// [arg]で期待されているのは[bool]か[String]のみのため、[dynamic]を使う
bool convertToBool(dynamic arg) {
if (arg is bool) return arg;
if (arg is String) return arg == 'true';
throw ArgumentError('Cannot convert $arg to a bool.');
}
非同期関数の戻り値で、値を生成しない場合は Future<void>
を使う
値を生成しない関数の場合は、戻り値に void
を使う様に、非同期関数で完了を待つ必要があるが、値を生成しない場合は Future<void>
を戻り値に指定します。
Future
や Future<Null>
を見かけることもあるかもしれませんが、これは過去にvoid
が許可されていなかった頃の記述方法です。
また、非同期処理を待つ必要がない場合などは、void
でOKです。
FutureOr<T>
は戻り値に使用しない
FutureOr<T>
が戻り値の場合、戻り値はTまたはFutureとなります。
そのため、使う側は必ず型をTなのかFutureなのかをチェックした上で処理を書かなくてはいけません。
// good
Future<int> triple(FutureOr<int> value) async {
// [await]を使うことで常にFuture型として扱える様になります
return (await value) * 3;
}
// bad
FutureOr<int> triple(FutureOr<int> value) {
// FutureOrで戻すと、使う側も同様のコードが必要になります
if (value is int) return value * 3;
return (value as Future<int>).then((v) => v * 3);
}
少し難しい言葉を使うと、反変性(引数)にはFutureOr
を使ってもよく、共変性(戻り値)にはFutureOr
を避ける、とも書いてあります。
ちなみに、引数の中のcallback関数ではこれが反転します。つまり、callback関数も戻り値は半変性となるため、FutureOr
を使ってもOK、となります。
// good
Stream<S> asyncMap<T, S>(
Iterable<T> iterable,
// 引数の中のcallback関数の戻り値はFutureOrでOK
FutureOr<S> Function(T) callback) async* {
for (var element in iterable) {
yield await callback(element);
}
}
Parameters
booleanの順序付き任意パラメータは避ける(avoid_positional_boolean_parameters)
Dartのオプショナル引数には名前付きパラメータと順序付きパラメータの2パターンがあります。
booleanの場合、意味が曖昧になるため順序付き(optional positional parameter)を避け、名前付きパラメータにします。
// bad(true/falseを見ても、何の意味か読み取れない)
new Task(true);
new Task(false);
new ListBox(false, true, true);
new Button(false);
// good(名前付きコンストラクターを使った場合)
Task.oneShot();
Task.repeating();
// good(名前付きパラメータを使った場合)
ListBox(scroll: true, showScrollbars: true);
Button(ButtonState.enabled);
前方のパラメータを省略したいケースがある場合、順序付き任意パラメータを避ける
順序付き任意パラメータの場合、順序が重要となるため、前方から順番にすべて指定する必要がります。しかし、前方のパラメータのうち、省略したいケースが想定される場合は、名前付きパラメータの方が適しています。
// 月、日、時、分と省略することなく順番に指定することが想定される
DateTime(int year,
[int month = 1,
int day = 1,
int hour = 0,
int minute = 0,
int second = 0,
int millisecond = 0,
int microsecond = 0]);
// 前方の[days]や[hours]はデフォルト値0のまま、[seconds]だけを指定したいケースなどが想定される
Duration(
{int days = 0,
int hours = 0,
int minutes = 0,
int seconds = 0,
int milliseconds = 0,
int microseconds = 0});
「指定しない」を受入可能な必須パラメータを避ける
パラメータを「指定しない」ということを示す場合は、nullを引数に渡すケースが他の言語でもよく見受けられます。Dartではその場合、必須パラメータではなく、任意パラメータにすることで回避できます。
// bad(第二引数のnullの意味が伝わりづらい)
var rest = string.substring(start, null);
// good(任意パラメータなので、指定されなかった場合、関数内でデフォルト値のnull扱いされる)
var rest = string.substring(start);
rangeを指定する場合のstartは包括的、endは排他的にする
範囲を引数で指定する場合、コアライブラリと同様にしましょう。
// 開始位置のindex=1は範囲に含まれる
// 終了位置のindex=3は範囲に含まれない
[0, 1, 2, 3].sublist(1, 3) // [1, 2]
'abcd'.substring(1, 3) // 'bc'
Equality
==
をoverrideする場合はhashCode
もoverrideする(hash_and_equals)
Javaや他の言語でも同じだと思います。
dart:ui
パッケージにhashValues
やhashList
という関数もあります。パッケージがui、というのが気になりますが…
==
のoverrideの中では、nullチェックは行わない(avoid_null_checks_in_equality_operators)
このチェックは言語側で自動的に行われます。
つまり、==
演算は、右辺がnon-nullでない場合にのみ実行されるため、==
の引数にnullが入ってくることはあり得ず、nullチェックは不要となります。
// bad
class Person {
final String name;
// ···
// nullチェックはDart側で行うため不要
bool operator ==(other) => other != null && ...
}