最近仕事でDart + Flutterを扱う必要が出てきたため事前にEffective Dartの内容を読んで自分なりに解釈してまとめておきます(とりあえず言語のコードスタイルなどは序盤に読んでおいた方がよいでしょうということを踏まえ)。
※Effective Dart自体は基本的にCreative Commons Attribution 4.0 Internationalライセンス、コードサンプルは3-Clause BSDライセンスとなっています(ライセンス上問題無い範囲で記事中でも使わせていただきます)。
※DartとFlutterに詳しく無いため誤解している点などは優しめにコメントなどでマサカリ投げていただけますと幸いです。また、普段はPythonを書いていることが多いためPythonとの比較が多めです。
導入部分
導入部分には一貫性と簡潔さについて触れられていました。
If there are multiple ways to say something, you should generally pick the most concise one.
簡潔さに関しては上記のように複数の方法があるのであればもっとも簡潔なものをと書かれています。この辺はThe Zen of Pythonに通じるものが少しありますね。
There should be one-- and preferably only one --obvious way to do it.
何かいいやり方があるはずだ。誰が見ても明らかな、たったひとつのやり方が。
スタイルガイド
クラス名はアッパーキャメルケース
クラス名は最初の文字も含め単語区切りで大文字を使うアッパーキャメルケースを使用します。
class MyClass {
...
}
拡張機能名はアッパーキャメルケース
extensionのキーワードによる拡張機能の記述はクラスと同様にアッパーキャメルケースを使用します。
extension MyList<T> on List<T> {
...
}
パッケージ名、フォルダ名、ファイル名などはローワースネークケースを使用する
パッケージ名、フォルダ名、ファイル名などは小文字で単語間をアンダースコアで繋ぐ形のローワースネークケースを使用します。
例 : my_package/lib/my_system.dart
importのasによるプレフィックス名はローワースネークケースを使用する
Dartではimport時にasのキーワードを指定することでパッケージなどの利用でプレフィックス(パッケージ内での定義の前に配置するエイリアス名)を扱うことができます。
その名前もローワースネークケースとなります。
import 'package:/my_package.dart' as my_package;
変数や定数、enumなどはローワーキャメルケースを使う
変数などは先頭の単語を小文字にする形での単語の先頭を大文字にするローワーキャメルケースを使用します。
int myCount = 0;
特徴的だなと思ったのですが、他の言語では定数やenumの値はアッパースネークケースを使うことが多い(例 : MY_CONSTANT_VALUE
)一方でDartでは変数と同じローワーキャメルケースを使う形になっていることです。
const myConstantValue = 10;
3文字以上の略語なども通常のキャメルケースを使う
HTTPなどの略語の場合も通常通りのキャメルケースのルールを使いHttpやhttpといった名前で扱います。
これはHTTPSFTPといったような名前を使う場合にHTTP SFTPなのかHTTPS FTPなのかといった単語の区切りが分かりづらいからという理由のようです。
例外的に2文字の略語の場合には両方とも大文字にするそうです(例 : IO)。
コールバックなどの引数で使用しない引数名にはアンダースコア単体を使用する
コールバック関数などで引数のフォーマットが決まっている一方で対象の引数を使わない場合には対象の引数名はアンダースコア単体にして使用しない(していない)ことを明示することが好まれます。
void myCallback(_, _) {
...
}
この辺りはPythonのループとかでも使用しない変数などにアンダースコア単体を使ったりもするので似たような感じでしょうか。
プライベートではない要素のプレフィックスへのアンダースコアの利用はNG
Dartではプライベートの要素を表現したい場合privateなどのキーワードを使うのではなくPythonのように要素名のプレフィックスにアンダースコアを使います。
int _myPrivateValue = 10;
そのためプライベートを表現する以外の目的でアンダースコアのプレフィックスを使わないように注意します。
ハンガリアン記法などのプレフィックスは使わない
現在では基本的に不要なため型の識別用などのためにハンガリアン記法などで変数名等にプレフィックスを付けないようにします。
ハンガリアン記法に関しては必要に応じて以下の記事などをご確認ください。
ライブラリに直接名前を指定しない
何のこと?と思ったのですが、どうやらimportとかで外部ライブラリを読み込む際にlibrary my_library
といった形でライブラリ名の上書きが効くらしいです。
私が読んだ入門書にも書いておらず基本的に使わないでしょう・・・という感じですが、そういった記述はEffective Dart的にもNGとされているようです。
importの順番と空白行
importはルールに従ってセクションごとに分けて1行明けにしたりします。基本的にこの辺はフォーマッタに任せておけばOKなので軽くだけ触れておきます。
- Dartビルトインのものは先にimportを記述します。
Pythonでもビルトインのものを先頭にimportを書いて1行明けにしますが似たような感じでDartでもビルトインのdart関係のものを記述します。
import 'dart:async';
import 'dart:html';
import 'package:foo/bar.dart';
- 相対パス指定になっているimportは後に記述します。
他の言語では基本的にimportは全体パスのみで指定してきたので相対パスは使わなさそう・・・ではありますが、使う場合には相対パスによるimportを後に書くようにします。
import 'package:foo/bar_1.dart';
import 'package:foo/bar_2.dart';
import 'bar_3.dart'
- exportの記述はimportの後に記述します。
Pythonで言うところの__init__.py
とかを使ってパッケージパスを短縮するのと恐らく同じような記述としてexportの記述がDartにはあるのですが、そのexportはimportの記述の後に記載します。importとexportを混ぜたりはせずにセクションを分けます。
import 'package:foo/bar_1.dart';
import 'package:foo/bar_2.dart';
export 'package:foo/bar_3.dart';
- importの各セクション内でimportの各行はアルファベット順に並び変えます。
この辺りのルールはPythonと同様です。面倒なのでフォーマッタ任せで良いと思います。
1行の長さは80文字よりも長くならないようにする
1行の長さは80文字以内になるようにします。Pythonで言うところのPEP8とほぼ同じ長さでblackよりも少し短めの文字数といったところでしょうか。
フォーマッタが基本的には可能な場合には調整してくれると思いますが、フォーマッタが反映されても80文字以内にならないことがあります。そういった場合には手動でのコードの記述の調整を検討します(変数名やインデントの深さを見直すなど)。
※例外的に文字列中に指定するURLやファイルパスなどは1行で記述した方が読みやすいケースとなり、それらの場合には80文字を超えるケースは起こりえます(その辺はそのままで良いと思います)。
{}の括弧を省略しない
if-elseなどの記述では{}の括弧を省略せずに記述します。
if (isHoliday) {
...
} else {
...
}
ただし例外的にifのみ(elseなどを使わない形)で且つifステートメント内で処理がreturn関係の1行のみといったケースには省略して書くこともOKとされています。
if (isHoliday) return ...
ドキュメンテーション
Python界隈でもdocstring然りかなりドキュメント関係がしっかりしていた印象ですが、Dartでもドキュメント関係が過剰になっているよりもどちらかというと不足しているケースが多いのでしっかり書きましょう・・・という方針のようです。
次節以降ではコメントなども含めたドキュメンテーション関係について触れていきます。
通常の文章のようにコメントを書く
英語のコメントであれば最初を大文字にしたりコメントの最後を.や?、!などの記号で終了する形で書きます。
日本語のコメントであれば。や!などの句点などの記号でコメントを終了しましょうといったところでしょうか。
ドキュメンテーションのためのコメントにはブロックコメントは使わない
Dartでは//
によるコメントと/**/
によるブロックコメントの両方がサポートされています。
しかしブロックコメントの方は基本的にコードの特定範囲の一括でのコメントアウトとして使うためドキュメンテーションとしてのコメントでは//
のインラインコメントの方を使用していきます。
ドキュメンテーションコメント
jsのJSDoc、Pythonのdocstring、RustのドキュメンテーションコメントのようにDartでもドキュメンテーションコメントがサポートされています。また、Rustのようにdart自体がドキュメンテーションコメントに応じたドキュメント出力をサポートしています。
ドキュメンテーションコメントは///で書く
ドキュメンテーションコメントは///のようにスラッシュを3つ重ねる形でのインラインコメントで表現します。通常のインラインコメントのように2つではないので注意します。
また、JSDocなどのように/**/によるブロックコメントは使用しません(※古くは使われていた時期もあった?ようですが現在は非推奨となっているようです)。
単純にインラインコメントで書いたときの方が行数が少なくて済むのとマークダウンでリストを表現する際にアスタリスク記号が被らずに読みやすいといったメリットが加味されているようです。
パプリックなAPIに大してはドキュメンテーションコメントを基本的に書く
トップレベルの定義などにすべてドキュメンテーションコメントを書く必要はありませんがパブリックなAPI(他のユーザーが触るインターフェイス)に関しては少なくともドキュメンテーションコメントを書くことが推奨されます。
ライブラリ自体に対するドキュメンテーションコメントを残す
まだDartのライブラリを書いたりといったことはしたことがないため配慮するのが先になるかもしれませんがライブラリ自体のドキュメンテーションコメントを書くことが推奨されています(Pythonで言うとモジュールなどに対するdocstringが近いかもしれません)。
- ライブラリの概要を表す短文
- ライブラリで使用されている特殊な用語への説明
- 動作する形のAPI利用例
- 重要な関数やクラスへのリンク
- ライブラリに関連する重要な外部資料へのリンク
などの記載があると良いかもしれません。
privateのAPIに関しても(可能であれば)ドキュメンテーションコメントを書くと好ましい
必須というわけではありませんが外部ユーザーが触る以外にも中でコード編集などを扱う開発者のためにもprivateのAPIに対してもドキュメンテーションコメントがあるとコードを読む際の助けとなります。
ドキュメンテーションコメントは1文でスタートする
ドキュメンテーションコメントはその内容を表す概要の1文からスタートさせます。最初から何文も長々と書かずになるべく内容をしっかりと表している短い概要で表現します。
ドキュメンテーションコメントの最初の概要とその後の詳細の間には1行空ける
APIの詳細説明が必要になるケースが多いと思いますが、そういった場合にはAPIの概要説明の1文との間には1行空けて概要部分と分離して見やすくします。
コードを見れば一瞬で内容が把握できて自明なものはドキュメンテーションコメントからは省く
ぱっと見で即座になんのものなのか把握が誰でもできて、且つ誤解を与えたりしない(ドキュメンテーションコメントを書いてもコードの内容を2重にそのまま記述するような類の)ものに関してはドキュメンテーションコメントからは記述を省きます。
英語のドキュメンテーションコメントは3人称で書く
日本語で書く場合にはあまり気にしなくともOKですが、英語でドキュメンテーションコメントを書く場合には3人称を使います。
例えば動詞でコメントを開始する場合には3人称のためのsを動詞に付けます。
例 : Returns true if every condition satisfies ...
真偽値を除いた変数や属性に対するドキュメンテーションコメントは名詞句からスタートするのが好ましい
こちらも日本語だと話が変わってきますが英語でコメントを書く場合には名詞句でスタートさせます(The ... など)。
/// The current day of the week, where `0` is Sunday.
int weekday;
ライブラリやクラスなどのドキュメンテーションコメントは名詞句からスタートさせるのが好ましい
通常の変数等と同様にライブラリやクラスの概要などのドキュメンテーションコメントの概要は可能であれば名詞句からスタートさせます。
真偽値の変数や属性に対するドキュメンテーションコメントはWhetherからスタートするのが好ましい
こちらも英語での話です。○○かどうかの真偽値といったようなドキュメンテーションコメントを書きたい場合にはWhetherからスタートするコメントで記載します。
/// Whether the modal is currently displayed to the user.
bool isVisible;
getterとsetterの両方を持つインターフェイスでは片方にのみドキュメンテーションコメントを書く
Dartではgetterとsetterは1つのインターフェイスのように扱われるためドキュメンテーションコメントも片方にのみ記載します。もしも両方に書かれていた場合にはsetter側は無視されます(この辺はPythonと似たような感じですね)。
基本的にはgetter側にドキュメンテーションコメントを書いておけば問題ないと思います。
ドキュメンテーションコメントに対してコード例を追加する
必須ではありませんがドキュメンテーションコメント上にコード例があると便利なことがあります。
DartではRustのようにマークダウンの形式(```によるコードブロック表現)でコード例として記述できるようです。
/// Returns the lesser of two numbers.
///
/// ```dart
/// min(5, 3) == 3
/// ```
num min(num a, num b) => ...
ただしPythonやRustにあるようなdoctestのようにドキュメンテーションコメント上のコードのテストとしての実行(常にCI/CDなどでコード例が動作することをチェックし続ける)は現在はサポートされていないようです。
issueは出ていたので将来もしかしたらサポートされるかもしれません。
スコープ内の定義(キーワード)に大しては[]の括弧で囲む
資料を読んでいてこの機能はPythonやRustにも欲しい・・・と思ったのですが、スコープ内の特定の定義などを[]の括弧で囲むとdart docでドキュメント変換した際に検索して該当する定義(ドキュメンテーションコメント)が存在すればリンクを張ってくれるようです。リンク先の対象が無い場合は恐らく単純に強調表示されます。
/// Throws a [StateError] if ...
/// similar to [anotherMethod()], but ...
割とこの手のものは自作のPythonライブラリでは自前で似たような感じでリンクが張られるようにしていたのでDartはビルトインでサポートされているのは良いなと思いました。
ドットで繋いでコンストラクタを参照させる・・・といった記述もできるようです。
/// To create a point, call [Point.new] or use [Point.polar] to ...
引数や返却値、例外情報などを個別に記載せずに文章内で説明を行う
これもDartで特徴的に思ったのですが、JSDocで@param
などで引数ごとに説明を書いたりPythonでNumPyスタイルなどで個別に引数情報などの説明をドキュメンテーションコメント内では書かないそうです(今まで触ってきた言語では大体個別に書いているケースが基本だったので印象的です)。
代わりにコメントの文章内で引数や返却値などの説明を書き、[]の括弧で囲って強調表示する・・・という形式なようです。
/// 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.
Flag addFlag(String name, String abbr) => ...
ドキュメンテーションコメントはメタデータアノテーションよりも上に記述する
@
の記号を使う形で関数などに反映できるメタデータアノテーションという機能(挙動は異なりますがPythonのデコレーターに近い雰囲気の書き方)がDartにはありますが、そのメタデータアノテーションはドキュメンテーションコメントよりも下に書きます。
関数にメタデータアノテーションをする場合であれば上から順番に
- ドキュメンテーションコメント
- メタデータアノテーション
- 関数定義
といった順番で記述します。
/// A button that can be flipped on and off.
@Component(selector: 'toggle')
class ToggleComponent {}
過度にマークダウンで装飾しない
前節までである程度使ってきましたが、ドキュメンテーションコメント内ではRustなどと同じようにマークダウンが使えます。dart docでのドキュメント出力にも反映されます。
ただしドキュメントを装飾するのが目的ではなくドキュメントで他の人に内容を伝えるのが目的なので内容重視でマークダウンでの装飾ばかりになって中身が無い・・・といったことにならないように注意します。
マークダウンで表現できないものをHTMLタグで装飾しない
マークダウンで表現しきれない装飾などをHTMLタグで装飾するといった対応はできれば避けるべきです。
マークダウンでも表現しきれないものに対してHTMLタグなどを使うのはコメントの記述が複雑になりすぎたり装飾過剰になっている可能性があります。
スペース4つによるコードブロックの表現は使わない
そもそもEffective Dartの資料を見ていて知ったのですが、マークダウンでは`の記号を3つ連続させた形でのコードブロック表現の他にもスペース4つを加える形でもコードブロックが表現できるそうです。
ただしDartのドキュメンテーションコメントでは基本的に`の記号を3つ使う形でコードブロックを表現します。
略語を避ける
i.e.
やe.g.
などの略語は人によっては伝わらないことがあります。そういった略語は使わずに普通の単語で説明を入れましょう。
ただし、後々の命名関係の節でも触れますが略称の方が一般的な場合にはそちらを使用します(あくまでも伝わりやすさが大切)。
ライブラリのルール
ライブラリの機能に関したルールについて触れていきます。個人的にはpart/part ofとかはあまり使わなそう・・・という気がしている(読みやすさや誤解しやすさなどの面で利用を避けた方が無難なのだろうか?という気がしている)ため軽く触れる程度にしておきます(この辺詳しく無いというのもあります)。
part ofなどではライブラリ名ではなくURIの文字列による指定を行う
ファイルを分割しつつ1つのファイルのように振舞ってもらうためのpart/part ofの機能ですが、part ofなどで対象を指定する際にはライブラリ名ではなくURIの指定が推奨されています。ライブラリ名での指定も動くものの曖昧さをもたらす可能性があり現在では利用は非推奨となっているようです。
part of my_library;
part of '../../my_library.dart';
他のライブラリパッケージのsrc以下の内容は利用しない
他のライブラリパッケージのsrc以下の内容はユーザー側が触らないという前提(慣習)に基づいて手が加えていくため、ライブラリ側のパッチバージョン程度のアップデートでも悪影響が出るケースが発生しうるため基本的に利用しないようにとされています。
ライブラリのimportでlibなどの指定を避ける
恐らくライブラリ開発者向けのルールだと思われ、その辺りまではまだ必要になっていないので理解が追いついていない感がありますが、ライブラリ内部でimportの記述をする際にはlib
などを含めないようにする・・・といったルールがあるようです(../を使ってlibの指定を省略する書き方も非推奨になっています)。
import '../lib/api.dart';
資料を読んだり調べたりしていた感じ、理由としては他のライブラリなどと競合したりエラー内容に影響が出たりといった具合のようです。
この辺りはLintでもチェックできるようなので将来必要になってきたら(Lintなどに引っかかる段階になってきたら)都度深堀りして調べようと思います。
相対パスによるimportは推奨される
Pythonとかだとファイルパスを変更した際の影響を加味してimportなどは絶対パスを使用すること・・・とされていますが、Dartではどうやら相対パスによるimportは推奨されるようです。
null関係
変数はnullで初期化しない
Dartではnullを取れる型指定の変数の場合には自動でnullで初期化されるため変数などの初期値を明示的にnullで初期化しておく必要はありません。
Item? bestItem = null;
nullを取れる型の引数もnullのデフォルト値の指定は不要
変数のnullでの初期化が不要なのと同様にnullを取れる型の引数に関してはnullで初期化されるためデフォルト値としてのnullの明示的な指定は不要です。
TypeGuard的にnullを取れる型の値をnull以外の型の値に変換しておくのは好ましい
TypeScriptやPythonなどではTypeGuardでその辺は制御したりしますが、例えばローカル変数や引数に渡されたnullをとり得る値に対して関数などを挟んでnull以外の返却させるなどしてnull以外の型に値にし、それらをクラス属性などに使用したりnullableな値を受け付けない関数で利用したり・・・といった制御は好ましいです。
初期化されたかどうかを冗長に判定しないといけない場合にはlate変数の使用を避けてnullを受け付ける型にする
宣言時に初期化を行わないlate変数というものをDartではサポートしています。
ただし(Pythonのクラス属性などであればhasattrなどでシンプルに判定できるものの)このlate変数が初期化されたかどうかを判断するためのシンプルな機能はDartには存在しないため、そういった判定が必要な場合にはlate変数を使うよりもnullを受け付ける型にしてnullかどうか(初期化が済んでいるかどうか)を判定する方がシンプルになります。
文字列関係
クォーテーションで囲まれた文字列同士の連結は+の記号を省略する
DartではPythonのようにクォーテーションで囲った文字列の間に+の記号が無くとも文字列を連結してくれるため、そういったケースでは+の記号を使わずに連結を行います(Pythonでは稀にうっかりによる悪影響が出たこともあった気がしますが、基本的には改行などを含めたいケースなどに記述が煩雑にならず便利です)。
raiseAlarm('ERROR: Parts of the spaceship are on fire. Other '
'parts are overrun by martians. Unclear which are which.');
文字列内へ変数の値を入れたい際には$記号の表現を使う
クォーテーションで囲まれた文字列中に変数の値などを組み込みたい場合にはDartでは$記号を使うことで対応することができます。
Pythonで言うところのf-strings、Rustでも新し目のバージョンでは{}の括弧を使って括弧中に変数名を記述することで変数の値が文字列中に展開できたりしますが、それらと同じようなことを$記号を使うことで対応することができます。
この書き方はクォーテーションを閉じて+記号で変数の値を連結したり等するよりもぐっと記述がシンプルになります。
'Hello, $name! You are ${year - birth} years old.';
文字列中への変数展開で{}の括弧が不要な場合には括弧を省略する
$
の記号を使って文字列中に変数の値を展開する際に、添え字なども一緒に変数と使っている場合には{}の括弧の記述が必要になります。
一方で、単純な変数のみを展開したい場合には不要な{}の記述は行わないようにします。
var greeting = 'Hi, ${name}! I love your ${decade}s costume
真偽値関係
真偽値のtrueとfalseはif文の右辺では使わない
真偽値をif文で評価したい場合には右辺にtrueとfalseを置くのではなくその真偽値の変数など単体をif文の中に記述します。
if (boolValue) { ... }
否定条件で評価したい場合には!の記号を真偽値の変数の前に配置します。
if (!boolValue) { ... }
nullの値を取り得る真偽値に関しては??の記号によるnull-aware演算子もしくは真偽値 != null && 真偽値
といったようにnullかどうかの判定を挟む形を使用します。
if (nullableBool ?? false) { ... }
if (nullableBool != null && nullableBool) { ... }
個人的には特にDartやFlutterに慣れていない身だとnull-aware演算子よりも後者のnull判定を分ける記述の方が記述が長いものの違和感が少なく好みではあります。
コレクション関係
コレクションの初期化にはコンストラクタの代わりにコレクションリテラルを使う
コレクション(リストやマップ(辞書)、セット(集合))を初期化する際には記述をシンプルにするために各コレクションのコンストラクタではなく可能であれば[]や{}の括弧を使ったコレクションリテラルを使用します(普段から他の言語でもコレクションリテラルを使うことが大半だったのでむしろコンストラクタを使うことはほとんど無かった感じではあります・・・)。
例えばリスト、セット、マップなどは以下のようにコレクションリテラルを使って初期化できます。
List<int> myList = [1, 2, 3];
Set<String> mySet = {'apple', 'banana', 'cherry'};
Map<String, int> myMap = {'apple': 1, 'banana': 2, 'cherry': 3};
コレクションの内容が空かどうかの判定はlength属性ではなくisEmptyとisNotEmptyを使用する
コレクションの中身が空かどうかの判定はlength属性ではなくisEmpty属性とisNotEmpty属性を使用します。
空では無い場合の判定では!の記号とisEmptyを使用するのではなくisNotEmptyの方を利用します。
if (lunchBox.isEmpty) return 'so hungry...';
if (words.isNotEmpty) return words.join(' ');
リストやセットなどで通常のループ目的でforEachメソッドは使用しない
リストやセットなど(マップは除く)で内部の要素を順番に参照したい場合にはforEachではなく通常のfor文を使用します。
jsとかだと通常のfor文がインデックスを参照する形となるためforEachも有益ですが、Dartではリストなどに対するfor-in文がPythonなどと同じようにリスト内の各要素を変数として参照できるようになっているためforEachをこのようなケースで使用する必要はありません。
for (final person in people) {
...
}
ただしリストなどの中の各要素に対してすでに存在する関数などを反映したい場合などにはforEachの利用は問題ありません(これはこれで記述がシンプルにできます)。
people.forEach(print);
リストのfromとtoListの使い分け
Iterableの要素からリストを作成する場合、リストには主にfromメソッドとtoListメソッドの2つの方法があります。
この2つの使い分けですが、単純にコピーや型をリストにしたいというだけであればtoListの方が向いています。こちらの場合元のIterableの値の型の情報が保持されます。例えば元のリストがList<int>
の型であれば結果もそのままList<int>
となります。
一方で途中でリストの値を一部取り除いたりしてListの中身の型の内容が変更になった場合にはtoListの方を使用し新たな変数で新しい型を指定し直したい・・・といった場合にはfromメソッドが向いています。
たとえばリストの内容が操作によって整数型のみになっているものの、型がdynamicになっているためtoListがキャストなどを使わないといけない場合などにはfromの方が向いています。
リストなどの中から特定の型の値のみに抽出する場合にはwehereTypeメソッドを使う
例えば整数と文字列を格納したリストから数字の値のみを抽出したリストを作りたい場合などは、型をしったりと扱うためにはwhereに加えてキャストなどを使う方法もありますが記述が冗長になりがちです。
そういった場合にはwhereTypeの方を使用すると無駄がなくシンプルに記述することができます。
var objects = [1, 'a', 2, 'b', 3];
var ints = objects.whereType<int>();
可能であればリストなどへのキャストは避ける
リストに対してのcastメソッドによる型変換は無駄が多いそうです。出来ればcastメソッドによるリストなどの変換処理は避ける形でコードを記述します。
※ループで参照できるようになった個別の値に対してキャストをするといった制御は問題ありません。
void printEvens(List<Object> objects) {
for (final n in objects) {
if ((n as int).isEven) print(n);
}
}
castメソッドを挟まずににfromメソッドで型の指定を変換しても問題ないケース(中身がすべて整数で統一されていることが分かっているようなケースなど)でのfromメソッドの利用も問題ありません。
int median(List<Object> objects) {
var ints = List<int>.from(objects);
ints.sort();
return ints[ints.length ~/ 2];
}
関数関係
ローカル関数の定義は変数ではなく通常の関数定義を使う
関数内でネストされる形で定義されたローカル関数では通常の関数としての定義とローカル変数としての定義のそれぞれが利用できますが、基本的には通常の関数定義の方を使用していきます。
void main() {
void localFunction() {
...
}
}
void main() {
var localFunction = () {
...
};
}
関数やメソッドなどを引数として受け付ける箇所で同じ引数構造での呼び出しができる場合、そのまま関数単体などを指定して記述をシンプルにする
見出しの内容がなんとも分かりやすく説明がしづらいところではありますのでコードで例を上げつつで説明します。
関数やメソッドを引数で受け付ける箇所に構造的に既存の関数などを指定できる場合には無名関数などを挟まずにそのまま引数にその定義済み関数やメソッドなどを指定します。
よくある例ではforEachで引数にprint関数を指定して各要素の内容を出力するようなケースです。以下のように引数に直接print関数を指定してシンプルに記述します。
charCodes.forEach(print);
一方で以下のように無名関数を挟む形での指定は記述が増えて煩雑になるので割けます。
charCodes.forEach((code) {
print(code);
});
引数のデフォルト値には:ではなく=の記号を使う
Dartでは関数などの引数のデフォルト値には:の記号と=の記号を使う2つの方法がサポートされていますが:の記号の方は歴史的経緯からサポートが残っている形なため基本的には=の記号の方を使用します。
void insert(Object item, {int at = 0}) { ... }
個人的にもPythonとかだとデフォルト値は=の記号だったのと:の記号の方だと型定義などの方を連想してしまうので=の方が自然に思います。
引数と返却値の型指定は行う
メソッドとかもそうですがDartでは基本的に引数と返却値の型は明示するようにします(他の言語のように内容から返却値の型が推論されたりデフォルト値に準じて推論されたりはしません)。
この辺はRustとかでも関数などは型の明示が強制されているので安全面的にも良いとは思います(想定していない型の推論結果で事故になるなどを避けれるため)。
※無名関数に関しては基本的に型の明示等は行いません。基本的には無名関数はコールバックなどに設定されることが多いため、そちらのコールバックから型の推論などが実行されます。
無名関数で型推論がうまくいかず何らか問題になっている場合には通常の名前付きの関数などで代替する形が推奨されます。
※別の節で触れますが初期化フォーマルの場合は例外的に引数には型アノテーションを行いません。
関数型の型の指定は引数と返却値の型情報も含めて記述する
Pythonで言うところのCallable型のようにDartでもFunction型という型で関数を指定する引数部分等に詳細な型の指定を行うことができます。
ただし可能なケースではFunction型だけを型として指定するのではなく引数と返却値も含めたFunction型で型の指定を行うようにします。
bool isValid(String value, bool Function(String) test) => ...
setterの返却値には型の記述を行わない
setterの返却値はDartでは必ずvoidとなるため記述の無駄を減らすために型の記述は省略します。
typedefの古い書き方は使用しない
関数の型定義を扱うためのtypedefには歴史的経緯から以下の2つの書き方が存在します。
typedef int Comparison<T>(T a, T b);
typedef Comparison<T> = int Function(T, T);
古い方の書き方は一応まだ動きますが現在は非推奨となっていたり、且つ一部のコーナーケースなどに対応しきれないといった問題があるようです。特に理由が無ければ基本的に後者の新しい方の書き方を使用します。
また、後者の書き方で引数名なども設定したい場合には以下のように書けます。
typedef Comparison<T> = int Function(T a, T b);
typedefよりもFunction型での直接の型の指定を優先する
関数の変数などに対して型の指定をしたい場合にはtypedefとFunction型の2つの方法があります(引数の場合は特殊な書き方がもう一つあります)が、どちらでも構わない場合にはFunction型の方を優先します。
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は型を変数のように事前に定義しておいて後から対象の関数やメソッドなどに指定するという形に対してFunction型の方は基本的に直接関数などの行でインラインで定義と指定を同時に行う形となります。Pythonとかで言うところの型エイリアスのような挙動をします。
Dart1のころにはtypedefの方しか無かったのでtypedefが主な利用となっていましたが、Dart2からはFunction型による新しい書き方が利用可能になっている点と直接型が指定されていた方が定義に飛んだりせずに済むため可読性も増すため基本的にはFunction型の方を優先します。
ただし大量に同じ型を反映する必要がある・・・といった場合にはtypedefの方が有益なケースがあるかもしれません。
引数の関数への型の指定は古い書き方よりもFunction型の方を優先する
引数に関数を受け付ける場合にはDart1などの以前のバージョンではtypedefの他にも以下のような特殊な型の指定が出来ました
Iterable<T> where(bool predicate(T element)) => ...
Dart2以降ではFunction型で引数の関数に対しても型の指定ができるようになっているためFunction型での指定の方を優先します。
Iterable<T> where(bool Function(T) predicate) => ...
わずかにFunction型を使った書き方の方が文字数が多くなりますが他の箇所も基本的にFunction型を使うケースが多くなるでしょうから統一感的にも多少の文字数の多さは許容します。
真偽値の引数は位置引数として指定しない
setterのインターフェイスを除いて、真偽値の引数はtrueもしくはfalseで直接指定することが多くなります。
他の型であれば変数や定数などで指定することが多くなるため名前からある程度推測がしやすくなりますが、真偽値のみの場合はぱっと見でコードの内容の把握が難しくなりがちです。
new Task(false);
new ListBox(false, true, true);
その代わりに有効化・無効化で分かりやすい名前の付いた名前を使用したり、名前付き引数(キーワード引数)を使用したり、もしくはenumなどで名前が見える形にします。
Task.oneShot();
Task.repeating();
ListBox(scroll: true, showScrollbars: true);
Button(ButtonState.enabled);
ユーザーにとって途中のデフォルト値が設定されている位置引数を省略したくなるような引数内容になっている場合には名前付き引数を使用する
デフォルト値が設定されている位置引数で、ユーザー側にとって途中の引数の指定は省略したい(デフォルト値のまま利用したい)一方で後ろの方に調整したい位置引数が後ろの方にあると途中の引数を指定しないといけなくなり記述が煩雑になります。
そういった場合には名前付き引数(キーワード引数)を使用してユーザー側が必要な引数のみ指定できるようにします。
引数の省略目的での空文字やnullなどは指定しなくともOKな引数構造にする
デフォルト値の設定や名前付き引数などを利用して、後半の引数でnullや空文字などで引数省略目的の指定を行わずに済むようにします。また、そういった場合で省略できる場合には後半の引数でnullなどは指定を省略するようにします。
var rest = string.substring(start, null);
var rest = string.substring(start);
開始と終了位置を受け付ける引数などでは開始値~終了値 - 1の範囲のインデックスになるようにする
リストのスライス処理などで開始位置と終了位置を受け付ける引数を持つ関数などでは、開始~終了値 - 1の範囲のインデックスの値になるようにします。
たとえば引数の開始値に1、終了値に4を指定したら1~3のインデックス範囲の値を取るようにします。
このインデックス範囲の挙動はDartコアライブラリなどでも同じ実装になっているためユーザーを混乱させないためにも合わせる形にします。
[0, 1, 2, 3].sublist(1, 3) // [1, 2]
'abcd'.substring(1, 3) // 'bc'
変数関係
大半のローカル変数には型の指定を行わずにvarによる型推論などを使う
Effective Dartを読んでいて一番「えっ」と思ったのがこの項目なのですが、Dartでは変数などを宣言する際に型を明示して定義する方法とvarなどを使って型推論に任せる方法があります。
個人的には型を明示する方が記述が長くなりがちではありますが事故が少なくなる印象は持っており他の言語では好みだったのですがEffective Dartではvarによる型推論の方が推奨される形のようです(記述はこちらの方が短くて済むのとEffective Dartの序盤で記述の簡潔さなどを重視している旨の記載があったのでその辺を加味されている感じでしょうか・・・)。
とはいえ郷に入っては郷に従えということで、折角なのでローカル変数ではvarによる型推論をヘビーに使っていく形でプロジェクトを進めていこうと思います。
ただし次の節で触れますが、定義時に初期化しないケースでは正確に型推論ができないのでvarではなく型の指定を行うようにします。
また、ローカル変数で型推論結果が持って欲しい型にならない場合があります。たとえば初期化後に別の型の値を代入するといったケースが該当しますが、そういった場合にはローカル変数でもvarではなく型を明示するようにします。
宣言時に初期化されない変数に関しては型の明示を行う
Dartでは変数定義などで宣言時に型を明示する方法とvarを使って型推論を行って定義する方法の2つがあります。
宣言と同時に初期化を行う場合には型推論が行いやすいのですが宣言時に初期化を行わない場合には型推論が微妙なことがあるため、そういった場合にはvarを使わずに型を明示するようにします。
List<AstNode> parameters;
if (node is Constructor) {
parameters = node.signature;
} else if (node is Method) {
parameters = node.parameters;
}
ぱっと見で明らかな場合を除いてトップレベルの要素に関しては明示的な型の指定を行う
変数以外もそうですがトップレベルの要素(グローバルなもの)に対してはぱっと見で明らかなケースを除いてvarによる型推論ではなく型を明示するようにします。
ぱっと見で明らかかどうか・・・は例えば以下のようなケースが考えられます。
- シンプルな数値や文字列が設定されている場合
- 何らかのクラスのコンストラクタの呼び出しになっている場合(型がそのクラスだと分かるため)
- 他の型が明示されている定数が設定されている場合
-
int.parse()
やFuture.wait()
などのユーザーが返却値の型を良く知っていると思われる場合
コードで一例を上げると例えば以下のようなものが該当します。
const screenWidth = 640; // ぱっと見で型が整数だと分かります。
ぱっと見で明らかかどうかの判断に迷った場合には型を明示する方を優先します(そちらの方が堅牢なコードになりますし他のユーザーが利用する際にも型に起因するトラブルが減ります)。
この節のルールはプライベートとパブリックな要素を問わず反映されます。プライベートなものでもトップレベルのものであれば型が明示されていた方がコードを編集する他のプログラマーがコードを読む際の助けとなります。
ローカル変数は以下の2つのルールのどちらかを使う
プロジェクトの方針に応じてどちらかに統一すればどちらでも良さそうですが、ローカル変数は以下のどちらかのルールに従うことが推奨されています。
- 再代入されないものはfinal、再代入されるものはvarで扱う
- すべてvarで扱う
計算可能なものの保持は避ける
非常に計算負荷がかかるものなどを除いて計算を行って算出が可能な値などはクラスの属性などに保持しないようにします。
例えば円の直径を保持しているクラスのインスタンスで円の面積や円周の長さを求めるのは大した計算量ではなく、必要になった際に都度計算するくらいでもほぼシステムに悪影響は出ません。面積などの値を事前に更新してクラスの属性などに保持することもできますが、直径の値が更新された場合に他の関連する値の更新を忘れてしまう・・・といったミスにも繋がりがちなので基本的には計算可能な値に関しては属性などに保持しないようにします。
キャッシュとかとも同様でパフォーマンス的にまったく問題ない箇所でもキャッシュを多用することで複雑になってキャッシュに起因する不具合が発生するといった話に少し近い感じでしょうか。
もちろん再計算にかなりの負担がかかるケースもあり値を保持した方が良いケースもあると思いますが、そうではないケースも多くあると思われるため計算できる値を保持する判断にする際には事前にしっかり考えたり何故保持しているのかのコメントなどを残すことが推奨されます。
ジェネリックな型を持つものを扱う際には型を明示する
リストなどのコレクション関係もそうですが、それらのジェネリックの値などで型の明示ができる際には明示しておきます。
Dartではジェネリックも含め型推論は賢いですが、ジェネリック関係などに関しては型を明示しないと十分ではない(推論結果が正しくなってくれないケースが結構発生しうる)ためです。
var playerScores = <String, int>{};
final events = StreamController<Event>();
ただし次の節で触れますが型の明示部分が重複する場合や推論が容易な場合には型の明示を省略します。
ジェネリックな型を持つ場合で型の明示が別途されている場合や推論が容易な条件であれば型の指定を省略する
前節とは逆の条件となる面もありますが、ジェネリックの型で既に型が明示されている場合は片方は省略します。
例えば以下のようにジェネリックの型の指定が2か所されている箇所があるとします。
class Downloader {
final Completer<String> response = Completer<String>();
}
上記のようなコードは片方の型の指定が無駄なので以下のようにジェネリックの型の指定部分を1か所にして記述を簡潔にします。
class Downloader {
final Completer<String> response = Completer();
}
また、整数や文字列、整数のみを格納したリストなどが直接指定されており型の推論が容易な場合も例外的にジェネリックの型の指定を省略します。
例えば以下のようなコードでは<List<int>
や<int>
といったジェネリックのための型の指定は無くても容易に推論ができる条件になっています。
var items = Future<List<int>>.value(<int>[1, 2, 3]);
上記のコードは以下のように省略した形で書くようにします。
var items = Future.value([1, 2, 3]);
ただ、次の節で触れますがどこまで明示的にジェネリックの型の判断が容易でもdynamicが割り振られるケースもあるようで判断が難しい時も個人的には少しあるかも・・・という気がしています。
ジェネリックの型の指定が不足していてdynamicになるようなケースではジェネリックの型の指定を行う
前節の内容も踏まえエディタ上で型推論の結果を見ないと判断が難しそうなケースもありそうですが、一見するとジェネリックの型の推論が容易そうに見えるケースでもdynamicなどの型になるケースがあるそうです。
例えば以下のような変数定義ではnumbers
とcompleter
の変数両方でdynamicが反映される・・・とEffective Dartには記載されています。
List numbers = [1, 2, 3];
var completer = Completer<Map>();
これらのジェネリックの型がdynamicになってしまうケースでは可能であれば型を明示するようにします。
List<num> numbers = [1, 2, 3];
var completer = Completer<Map<String, int>>();
dynamicの型を使う必要がある箇所は明示的にdynamicの型の指定を行う
型推論では推論ができないケースや、雑多な型で返却されるケースなどで型の指定が難しく型の指定をしないとdynamicの型になる箇所では明示的にdynamicの型指定を行っておきます。
dynamic mergeJson(dynamic original, dynamic changes) => ...
こうしておくことでユーザーはdynamicと型が指定されている箇所は静的型付けの恩恵を受けづらく型による安全性が担保されていないインターフェイスということが分かりやすくなります。明示的にdynamicの型指定がされていない場合は実装者が型の記述を忘れている状態なのか等が判断が難しくなります。
ただし、既にdynamicで型が指定されているものを利用する場合などにはdynamicの型の記述は省略しても構いません。
例えば以下のコードではreadJson関数で既にdynamicの型の記述がされているため、その関数を使った返却値部分ではdynamicの型の記述は省略可です。
Map<String, dynamic> readJson() => ...
void printUsers() {
var json = readJson();
var users = json['users'];
print(users);
}
使用しない変数に対する型の指定は行わない
コールバック関係などで使用しない変数や引数などに関してアンダースコア(_
)を変数名などに使うことがあります。
このアンダースコアの変数などに関しては型の指定などは省略します。
定数関係
コレクションなどで重複してconstの記述を行わない
リストなどのコレクションを定数として扱いたい場合コレクション側に1か所constの記述をすれば十分で、内部の値にまでconstの記述を重複して行わないようにします。
const primaryColors = const [
const Color('red', const [255, 0, 0]),
const Color('green', const [0, 255, 0]),
const Color('blue', const [0, 0, 255]),
];
const primaryColors = [
Color('red', [255, 0, 0]),
Color('green', [0, 255, 0]),
Color('blue', [0, 0, 255]),
];
メンバー要素(クラスのフィールドとメソッド)関係
不要なgetter/setterは設けない
単純にフィールド要素の値の取得・更新を行うだけの実装の場合にはgetter/setterのインターフェイスは設けずに直接フィールドを扱うようにします。
Javaなどだと話が変わってきますがDartでは単純な値の取得と更新を行うだけであればgetterとsetterのインターフェイスを設けるのは記述が煩雑になるだけでメリットがありません。
フィールドの値の取得のみが必要な場合には可能であればgetterではなくfinalを使う
例えばコンストラクタで値を1回だけ設定して、その後は更新を行わない(インターフェイスとしてもsetter側が無いもの)インターフェイスを設けたい場合にはgetterのみを用意するのではなく対象のフィールドにfinalを指定する方が記述がシンプルになります。
シンプルなgetter/setterなどのメンバーのインターフェイスでは=>の記号による定義が利用できる
ルール的に使わないといけないというものではありませんが、1行で書けるようなシンプルなgetterなどの処理であればDartでは=>の記号を使うことでもメンバーのインターフェイスを定義することができます。
この書き方を使うことで記述を簡潔に済ませることができます。
double get area => (right - left) * (bottom - top);
一方で条件分岐やfor文など複数記述があった方が好ましいようなインターフェイスでは通常の関数・メソッド的な記述で対応する形にします。
必要なとき以外はthisを使わない
クラスのメンバー要素にアクセスする際に他の言語ではthisやselfなどの記述が必要になることがありますがDartではthisなどの記述が必要になるケースは少な目です。
一方でthisの記述自体はサポートされています。一部のケースでthisを書く必要が出てきますが、それ以外ではthisの記述は省略するようにします。
例外のケースとしては以下のようなものがあります。
- ローカル変数とメンバー要素の名前が被っている場合 :
class Box {
Object? value;
void update(Object? value) {
this.value = value;
}
}
- 名前付きのコンストラクタが複数定義されており、且つ内部で別の名前付きコンストラクタなどが呼び出されているようなケース :
Pythonで言うところのコンストラクタ以外でインスタンスを作成するためのクラスメソッドに近いもので名前付きコンストラクタと呼ばれるものがDartにはありますが、それを使って名前付きコンストラクタの内部で別の名前付きコンストラクタを呼び出しているようなケースではthisが必要になるケースがあります。
例えば以下のコードはEffective Dartの資料から引用させていただいていますが、alsoBlackという名前付きコンストラクタの内部でblackという別の名前付きコンストラクタを呼び出しています。
この場合thisを設けないと生成されるインスタンスがalsoBlackとblackとで別のものになってしまったり・・・で正常な記述にならなくなってしまいます(恐らくコンパイルも通らない?)。
class ShadeOfGray {
final int brightness;
ShadeOfGray(int val) : brightness = val;
ShadeOfGray.black() : this(0);
ShadeOfGray.alsoBlack() : this.black();
}
※Dart初心者のため資料を読んでいる際に初見の際少しの間理解が追い付かず考えていたのでこの辺自信がないため理解が間違っていたらご容赦ください・・・。
その他、単純にコンストラクタの値をフィールドに設定したい・・・といった場合もthisを使って記述を簡潔にすることができます。この辺はコンストラクタ関係の節で触れます。
コンストラクタのパラメーターに依存しないメンバーのフィールドは宣言時に初期化しておく
コンストラクタに渡される各パラメーターに依存しないフィールドに関しては宣言時点で初期化しておくとコンストラクタ内の記述が減って簡潔になります。
※Pythonだとミュータブルなメンバー要素を初期化していると割と事故になったり・・・としがちですがDartだとその辺考えなくともOKそうな印象があります(資料のサンプルコードを見ている限りでは)。
なるべくトップレベルのメンバーのフィールドはfinalにする
なるべくクラスのトップレベルのメンバーのフィールドはfinalを指定してイミュータブルにしておくことでコードの挙動を推測しやすくなります。
Rustなども変数がデフォルトでイミュータブルだったりしますが、同じような感じでデフォルトではフィールドはfinalでイミュータブル、必要になった際にそのフィールドだけfinalを外すくらいが好ましいかもしれません。
コンストラクタの時点で初期化できないフィールドに関してもlate finalの利用で解決できればそちらを使うべきです。
setterを使うべきケースの条件
以下のような条件を満たす場合の値の更新はメソッドではなくsetterのインターフェイスを利用するようにします。
- 引数を1つのみ受け取る場合
- 処理が状態を更新する場合
- 処理が冪等(複数回呼び出しても状態が同じになる)である場合
- ※ロギングなどは条件に含まず、その辺りの出力等が異なっていても問題ありません。
- getter側のインターフェイスも定義できる場合
- ※次の節で触れます。
getterを設けられない場合にはsetterではなくメソッドを使う
setterによる値の更新は行えるもののgetterのインターフェイスは何らかの要因によって設けることが出来ない場合はユーザーの混乱の元になるためsetterではなくメソッドでインターフェイスを設けるようにします。
これは、例えば=は使えても+=は使えない・・・といったユーザーからすると分かりづらい挙動になってしまうためです。
可能であればオーバーロードに近いことをするためにランタイム中の型によるメソッドの分岐などは避ける
Dartには同じ名前のメソッドを複数定義し、且つそれぞれの引数の型によって使用されるメソッドを分岐させるオーバーロード的な機能はありません。
引数の型によって内部で処理を分岐させる・・・といったことも出来ないことはありませんが、可能であれば別名のメソッドを型ごとに用意するようにします。
初期化処理が組みこまれていないパブリックのlate finalの利用は避ける
コンストラクタ(名前付きコンストラクタなども含む)もしくはインスタンス生成時に必ず呼ばれる初期化処理で初期化されるようになっていない場合にはパブリックのlate finalのフィールドを設けないようにします。
ユーザー側に初期化を任せる形になっているとうっかり忘れた際にランタイムエラーとなるケースが残ったり、もしくは潜在的なランタイムエラー条件が存在する状態でデプロイされてしまう可能性があります。
メソッドチェーンのためにthisを返却しないようにする
他の言語であるようなメソッドでthis(self)などを返却することでメソッドチェーン的に書けるようにする・・・という実装はDartではthisを返却しなくともメソッドカスケード(Method cascades)という機能を使うことで実現できます。
メソッドカスケードは以下のようにドットを2つ記述する形で利用できます。
var buffer = StringBuffer()
..write('one')
..write('two')
..write('three');
コンストラクタ関係
初期化フォーマルが使えるときには活用する
initializing formal (初期化フォーマルで訳として良いのだろうか・・・)という機能があり、引数名とフィールド名が一致しており且つ引数内容がフィールドに反映するだけ・・・といった場合にDartではthisを使って記述をシンプルに書くことができます。
例えばxとyをコンストラクタでフィールドに設定するコードを書きたい際には以下のように書くことができます。
class Point {
double x, y;
Point(this.x, this.y);
}
ぱっと見コンストラクタに引数の記述しか無い・・・という感じですがこれでxとyのフィールドに引数の値が設定される挙動をするようです。
使えないケースも多いですが使える時には使うと記述がシンプルになります。
初期化フォーマルを使う場合には引数に型の指定を行わない
基本的に関数やメソッドの引数や返却値の型アノテーションは行うというルールについて別の節で触れましたが、初期化フォーマルに関しては引数に型の指定は行わずに省略します(フィールド側で型の明示がされており記述が重複してしまうため)。
lateはコンストラクタの初期化で対応できる場合には使用しない
コンストラクタ時点では初期化されないメンバーのフィールドなどはlateを使って対応することができます。
しかしlateを使うと通常コンパイルエラーで検知できていたものが検知できなくなり、ランタイム中に初期化されていないフィールドなどにアクセスした場合にエラーになるケースが発生しうるようになります。
そのため安全などのためにコンストラクタ時点で初期化できるのであればlateは使わないようにします。
コンストラクタの内容が空の場合には{}の代わりに;を使う
Dartでは初期化フォーマル使用時などでコンストラクタの内容を引数以外空にできることがあります。
そのような場合にはDartではコンストラクタの最後は{}の括弧ではなく;のセミコロンの記号で記述を終了することができ、こちらの方がコードが僅かに簡潔になります。
newは使わない
歴史的経緯からコンストラクタで初期化する際にnewの指定をしても動作しますが、現在は非推奨となっているため使用しないようにします。
コンストラクタは可能な場合にはconstで定義する
他の言語と比べて特徴的だ・・・と思ったのですが、Dartではコンストラクタをconstとして定義することができます。
条件としてはメンバー要素が全てfinalで定義されているという条件が付きます。
例えば以下のようなコードになります。
class Animal {
final String name;
final int age;
const Animal(this.name, this.age);
}
コンストラクタにconstが付いているとそのコンストラクタを使ったインスタンスもconstで定義できるようになります(コンストラクタにconstが付いていないとコンパイルエラーになります)。
const animal1 = Animal('ミケ', 5);
また、コンストラクタに指定された各引数の値が一致している場合、インスタンス同士の比較でtrueを返すようになります。
void main() {
const animal1 = Animal('ミケ', 5);
const animal2 = Animal('ミケ', 5);
const animal3 = Animal('タマ', 5);
print(animal1 == animal2); // true
print(animal1 == animal3); // false
}
true
false
例外処理関係
例外処理を書く場合にはonで型の絞り込みを行うようにする
Dartではtry-catchによる例外処理を書く際にonで型の指定を行いつつ、各エラーの型ごとに例外処理を分岐させることができます。
プログラマー側がどんなエラーが発生しうるのか、それぞれのエラーでどんな例外処理が必要なのかは把握しておくべきなので基本的にonでの例外処理のエラーの型は個別に指定するべきです(onを省略するとすべてのエラーに対する例外処理となってしまいます)。
フレームワークやライブラリなどを使用する際にエラー内容を絞り込むのが難しい場合もあるとは思いますが、その場合でも基底のエラークラスであるExceptionをonで指定するようにします。
Exceptionを指定した場合はプログラム以外のエラーのみを対象とするようになります(例 : DB関係のエラーなど)。プログラム上の問題(ミスなど)は例外処理で制御せずにプログラム側を修正することで対応します。
どうしてもonを使わない形で例外処理を書く必要がある場合にはそのエラーを握りつぶすようなことはせずログを取るなりユーザーに表示するなりといった対応を行うようにします。
プログラムのエラーに関してはErrorクラス(もしくはそのサブクラス)を使用する
プログラムのエラー(なんらかのプログラムの記述ミスがある場合)に関してはErrorクラスもしくはそのサブクラスを指定するようにします。
プログラム以外のエラーに関してはExceptionもしくはそのサブクラスを使用します。
プログラム以外のエラーに対してErrorクラスもしくはそのサブクラスを指定すると誤解の元になるため使用しないようにします。
Errorもしくはそのサブクラスに対する例外処理は実装しない
前節で少し触れましたが、Errorもしくはそのサブクラスはプログラム上のなんらかのミスに起因するエラーとなるため、例外処理で対応するのではなくプログラムのミスを修正するようにします。
例外処理の中で再度例外を投げるときにはrethrowを使う
try-catchで例外処理を扱っているものの、catch内でも処理できない例外で再度例外を投げて止めたい場合にはthrowではなくrethrowの方を使います。
rethrowの方は元の例外時のエラー情報も保持されているのでエラー情報が欠落するといったことが起きません。
try {
somethingRisky();
} catch (e) {
if (!canHandle(e)) rethrow;
handle(e);
}
非同期処理関係
※Dartの非同期処理に不慣れで知見が少ないため非同期処理はスキップしがちでいきます(知らない機能が触れられている箇所などは特に)。
非同期処理にはFutureの代わりにasync/awaitの方を優先して使う
Dartでは非同期処理ではFutureとaync/awaitの2つの方法が主に存在しますが、複雑な制御でfuture側じゃないと対応ができないといったようなケースを除いて可読性が高く制御のしやすいasync/await側を利用します。
もしくはFuture側の方が記述が簡潔になる場合などにはFuture側の方の利用を検討します。
ayncが不要な場合には利用を避ける
なんでもかんでも非同期関係にasyncを使うのではなく、asyncの記述が不要なケースもあるので不必要にasyncを使わないようにします。
nullになりうるFutureやStream、コレクションなどの返却は避ける
FutureやStream関係、もしくはコレクション関係で返却するデータが無い場合にはnullを返すかもしくは空のコレクション(空のリストなど)を返却する方法があります。
そういった場合には空のコレクションなどを返却する形にし、isEmptyなどの用意されているインターフェイスで空かどうかを判定しシンプルに返却値を扱うようにします。もしくはStreamであれば値自体を返却しないようにします。
ただしnullと空のコレクションで挙動が異なるようなケースには該当しません。
返却値を返さない場合には返却値にFuture<void>
の型を使用する
以前のDartのバージョンでは非同期処理の返却値の型にFuture<void>
の型指定が出来なかったため返却値を返さない場合にはFuture
やFuture<Null>
などの型が指定されていました。
現在のDartのバージョンではFuture<void>
という記述が使えるようになっているためこちらを使用します。他の通常の関数で返却値を返さない場合にvoid
が使われているのと記述を統一できるのと、エラーチェックなどの面で好ましいそうです。
また、非同期処理で返却値を返さず且つ呼び出し元が非同期処理の完了を待ったり失敗時の制御などが不要な投げっぱなしで問題無い場合には単純にvoid
の型を返却値に指定するようにします。
返却値の型でのFutureOr<T>
の利用は避ける
返却値の制御が煩雑になるので非同期処理の返却値ではFutureOr<T>
の代わりにFuture<T>
を使うようにします。
命名関係
ある程度変数関係の節などでも触れてきましたが命名関係のものを追加で以下の節で触れていきます。
名前には一貫性を持たせる
複数人で作業していると発生しがちですが、プロジェクトで使う単語名などは統一します。表記ブレ・意味は同じなものの異なる名前などはなるべく使わないようにします。
一貫性を持たせることで利用ユーザーやプロジェクトの新しいメンバーなどの学習コストを下げることができます。
省略形は避ける
名前で省略形は避けます。例えばbuildRects
とかよりもbuildRectangles
などの単語を省略しない形を優先します(あまりに名前が長くなる場合は考える必要があるとは思いますが・・・)。また、単語区切りの大文字も正しく設定します。
ただし省略形や略語の方が一般的な場合はそちらを使用します。たとえばHyperTextTransferProtocol
よりもHttp
とかの単語の方が一般的なのでそちらを優先します。
一番内容を具体的に示している名詞を名前の最後に持ってくる
形容詞や重要度の低い名詞などを名前の先の方に配置して、一番内容を具体的に示している重要な名詞を名前の最後に配置するようにします。
例えば「ページの数」という変数名であれば数を一番重要と置いてnumPages
よりもpageCount
といったように名前を付けます。
「CSSのフォントフェイスのルール」という変数名であればルールを一番重要としてRuleFontFaceCss
などではなくCssFontFaceRule
という名前を付けます。
これは他の言語とかだと人によって様々ですがDartではルールが決まっているようで、この辺まで公式が決めているのは印象的です。
自然な英文を意識する
名前に迷った際には英語的に自然な方を優先します(まるで英文のようなプログラムを書くという面を意識します)。
また、メソッドなどに関してもクラス名(インスタンス名)などに合っている名前を優先します。
例えばsubscription
というインスタンス名があればメソッド名はtoggle
などよりかはcancel
などの方が意味的に自然です(subscription.cancel()
など)。
ただし英語的に正しいからといって冠詞(theなど)までは名前に不要です(この辺りまで付け始めると記述が煩雑になってきます)。
真偽値以外の変数名やフィールドなどは名前を動詞からスタートしない方が好ましい
基本的に真偽値を除いて動詞から始まる名前や関数やメソッド名などに使うようにして、変数名やフィールド名は動詞でスタートしないようにします(メソッドなどと混同しやすくなるため)。
真偽値の属性や変数の名前は形容詞よりも動詞の方が好ましい
例えばcloseable
という真偽値の変数名よりもcanClose
といったような動詞の名前の方が分かりやすい名前になります。
真偽値の名前のプレフィックスとしてはisやhas、can、mustなどのものが分かりやすいです。
印象的なのはempty
とかは他の言語などだと割と使われている印象ですが、それよりもisEmpty
などの方がDartだと推奨されている点です。
上記のようなプレフィックスのもの以外にも能動動詞且つ述語としか意味が取れないものも向いています(例 : existsなど)。
一方でclosingConnection
などだと「接続が閉じているかどうか」という真偽値的な意味合いと「閉じている接続」という名詞的な意味合いの名詞としても取れて曖昧なので好ましくありません。
名前付きの真偽値の引数(キーワード引数)では動詞を省略しても構わない
真偽値のキーワード引数などは意味が伝わりやすいためisなどの動詞のプレフィックスなどを付けなくても良いとのことです。
Isolate.spawn(entryPoint, message, paused: false);
真偽値では正と負の単語両方がある場合には正の単語の方を優先する
たとえばvisibleとinvisibleという単語のどちらかを選ぶ必要があるといった場合には正のvisibleの方を選んでisVisibleといった真偽値の方を選択します。
こうすることでif文で否定を利用する際などに2重否定で紛らわしくなったりを避けることができます。
ただしいくつかの要素では正と負が曖昧だったり、もしくは否定系の方がはるかに使われているといったケースもあります(例えばファイルを保存済みかどうかの真偽値ではsavedよりもunsavedの方が好ましいといったケースもあるかもしれません)。そういった場合には無理せず適していると思われる方を選択します。
状態を変更する関数やメソッドの名前に関しては命令形の動詞でスタートするのが好ましい
状態が変わる系の関数やメソッドなどは命令形の動詞でスタートさせます。
たとえばadd
やremoveFirst
といった名前が該当します。
getterに性質が近い値を返却するメソッドでは名詞を名前の最初に持ってくる形が好ましい
例えばelementAt
みたいなgetterのインターフェイスに性質が近いもののどのインデックスが対象なのかなどの引数を取る必要があるメソッドに関しては動詞スタートではなく名詞スタートで名前を付けるのが好ましいです。
ただし、状態を変更せずに値を返却するインターフェイスであってもstring.split()
などのように動詞スタートの方が自然なケースもあります。
値を返却する関数などの処理の場合に処理内容に注意を払って欲しい場合には命令形の動詞を使用する
状態の変更などは行われず、且つ値を返却するインターフェイスであっても処理内容に注意を払って欲しい処理の場合にはgetterだったり名詞を含める形ではなく命令系の動詞を使用します。
たとえばdownloadData
といったように処理時間がかかるという点に注目してほしいような処理であれば命令形の動詞の名前を付けます。
単純に値を返すだけのメソッドではgetで始まるメソッドではなくgetterのインターフェイスを使用する
単純に値を返却するような処理であればメソッドではなくgetterのインターフェイスで実装し、getでスタートするメソッドの使用は避けます。
前節で触れたように処理内容に注意を払って欲しい処理の場合にはget以外の内容を示す動詞を設定します(downloadやcalculateなど)。
状態を変更した形のコピーを作成するメソッドにはtoのプレフィックスを付けるのが好ましい
たとえばtoString
やtoSet
のように型などの状態を変更しつつコピーを返却するメソッドのインターフェイスではtoのプレフィックスをメソッド名に設定します。
Dartのコアライブラリなどではこのようなtoを付ける名前が慣例として採用されているため統一感を出すことができます。
状態を変更した形のオブジェクト、且つ元のオブジェクトへの参照が残るメソッドにはasのプレフィックスを付けるのが好ましい
型の変更などがされた新しいオブジェクトが返却されるものの、元のオブジェクトへの参照が残った状態になるメソッドに関しては慣習としてasのプレフィックスをメソッド名に設定します。
前節とのtoのプレフィックスとの違いはディープコピーとシャローコピーの差に近いかもしれません(ディープコピー的な処理であればtoの方を使う形といった具合に)。
var map = table.asMap();
関数やメソッド名で引数内容を説明しないようにする
例えばelementのみを持つリストのようなインスタンスで要素を追加するメソッドが必要になった際にはlist.addElement(element)
といった書き方をせずに単純にlist.add(element)
といったようなインターフェイスにします(引数で追加される要素は分かるのと対象の絞り込みは型で制御すれば良いため)。
しかし異なる型に対する処理で似たような名前のインターフェイスを設ける必要がある場合には引数と被った説明を関数名やメソッド名に含めるようにします。
map.containsKey(key);
map.containsValue(value);
ジェネリックなどで単一の型名を使うときには既存のよく使われる文字を選択する
ジェネリックの型などで単一の型名(Tなど)を使う場合には用途に併せて既存のよく使われる文字を選択します。
例えば以下のようなものがあります。
- コレクション内での要素としての
E
:
class IterableBase<E> {}
class List<E> {}
class HashSet<E> {}
class RedBlackTree<E> {}
- キーと値を表す
K
とV
:
class Map<K, V> {}
class Multimap<K, V> {}
class MapEntry<K, V> {}
- あまり使う機会は無さそうですが返却値としての
R
:
abstract class ExpressionVisitor<R> {
R visitBinary(BinaryExpression node);
R visitLiteral(LiteralExpression node);
R visitUnary(UnaryExpression node);
}
- 単一の型を持ち、且つ周囲の定義で内容が把握できる場合のジェネリックの型としての
T
、S
、U
:
ジェネリックの型としてはT
が使われるのを良く見かけますが、入れ子にしたい場合などにはS
やU
の文字も使用するようです。
class Future<T> {
Future<S> then<S>(FutureOr<S> onValue(T value)) => ...
}
実際のコーディングのときには上記の例で大体がカバーできますが、それ以外を使う必要が出た場合には任意の単一の文字を使うかもしくは単語の文字列を使って説明的で分かりやすい名前を使うのも許容されます(個人的にはそのような場合には省略せずに単語で表現する形が好みです)。
class Graph<N, E> {
final List<N> nodes = [];
final List<E> edges = [];
}
class Graph<Node, Edge> {
final List<Node> nodes = [];
final List<Edge> edges = [];
}
クラス・ミックスイン・ライブラリ関係
プライベートな定義にはアンダースコアのプレフィックスを付ける
慣習というよりも言語自体に組み込まれている挙動となりますが、プライベートなフィールド名やメソッド名などには先頭にアンダースコアを付けます。
可能な限りフィールドやメソッドはプライベートにする
可能な限りフィールドやメソッド名の先頭にアンダースコアを付けてプライベート相当にし、必要なもののみパブリックで扱います。
パブリックにしたものは他のユーザーが使用するためインターフェイスの内容は変えづらくなるため不要なものまでパブリックになっているとアップデートなどがしにくくなってしまいます。
また、パブリックのインターフェイスが少ない方が利用者の学習コストが下がるというメリットもあります。
プライベートに設定しておいたものは静的解析で使用していないものなどを検知して無駄なコードを削除しやすいといった保守面でのメリットもあります。
1つのライブラリ(ファイル)内に複数のクラス定義などは許容される
Javaとかだと1ファイルに1クラスといった制約が付きますが、Dartではその辺りの制約はなくPythonなどのように1ファイルの中に複数のクラスを定義することができ、このような書き方は許容されます。
これによって例えば関連するクラスを1つのファイルにまとめてグループ化しておいたり、関連クラス側からのみプライベートな定義にアクセスするといった制御が可能になります。
※全てのクラスを1ファイルに・・・というわけではなく、ある程度便利な時には1ファイルにある程度まとめてグループ化するといったものになります。
1つの抽象メソッドのみを持つクラスの定義は避ける
以下のような1つの抽象メソッドのみを持つクラスの定義は記述が煩雑なので避けます。
abstract class Predicate<E> {
bool test(E element);
}
代わりにシンプルに関数を使うことで記述が簡潔になります。
typedef Predicate<E> = bool Function(E element);
静的なメンバーのためだけにクラスを定義しない
他の言語では全ての定義をクラス内に行わないといけないケースがあり、その場合静的な定義(staticな変数など)もクラス内に宣言しないといけないことがあります。そのため静的なメンバーの定義のためだけにクラスを挟む・・・といったコードになることがあります。
Dartではそういった制約は無いのと、そのような静的メンバーを定義したい場合にはライブラリ(ファイル)の方に定義する方が適切です。
ただし、enumのクラス的に静的なメンバーを特定のクラスにまとめておくといった使い方は許容されます。
サブクラス化される想定になっていないクラスは継承しない
対象のクラスがサブクラスを持つ想定になっていないクラスを継承した場合、内部のコードの更新などで壊れやすく、且つ影響範囲が大きくなりがちです。
継承される前提のクラスかどうかはクラスのドキュメンテーションコメントに書いておくか、もしくはIterableBaseのように継承を想定している名前を付けておくのが好ましいです。
もしそのような継承する想定になっていないクラスの場合には将来の悪影響を加味してなるべく継承を避けるべきです。
なんでもかんでもインターフェイスを挟まない(インターフェイスを挟む箇所を必要な箇所に限定する)
インターフェイスは便利で積極的に使っていくべき機能ではありますが、一方でインターフェイスと複数のクラス間はかなり密結合な形になります。
そのため多くのものをインターフェイスを挟む形にしておくと場合によっては将来インターフェイスもしくはクラス側の変更が効きづらくなることがあります。
あくまでメソッドの構造を統一したい箇所のみにインターフェイスを使用し、インターフェイスを挟むメリットの無い箇所に関しては使用を避けた方が将来のインターフェイスやクラスの変更がしやすくなります。
インターフェイスとして利用できるクラスであればドキュメンテーションコメントにその旨を記載しておく
もしインターフェイスとして利用できるクラスであればドキュメンテーションコメントにその旨を書いておきます。
もしくはEffective Dartには記載がありませんが継承可なクラスに対してBaseとサフィックスを付けるのと同様にInterfaceといったサフィックスを付けておくのも良いかもしれません。
ミックスインを実装する場合にはmixin構文を使う
古いバージョンのDartではmixin構文が無かったためそれを使わずにミックスインに近い制御が実装されていましたが、現在はミックスイン用のmixin構文が存在するためミックスイン関係を実装する場合にはmixin構文を使用します。
この構文を使うことで対象の定義をミックスイン専用として制限出来たりDart言語側もミックスインとしての制約を与えることができます。
ミックスインとして想定されていないクラスをミックスインとして使わない
明示的にミックスインとして想定されていないクラスはミックスインとして使用しないようにします。
ミックスイン用として想定されていないクラスをミックスインとして使うと、将来そのクラスに更新が入ってミックスイン用として使えなくなるといったリスクがあります(コンストラクタが追加されるなど)。
具体的にはドキュメンテーションコメントにミックスイン用として記載があるか、もしくは名前がIterableMixinといったようにMixin用としてサフィックスが付いていない場合にはミックスインとして想定されていないクラスとして判断します。
逆にミックスインの実装者はミックスインを想定していることをドキュメンテーションコメントもしくは名前のサフィックスなどで明示します。
型関係
静的な型チェックの恩恵を無効化したい場合にのみdynamic型を使う
dynamic型の利用は全ての型を受け付ける柔軟性を得られる一方でコンパイル時の型のチェックの恩恵を得られなくなりランタイム上でエラーなどのトラブルになるリスクが増加します。そのため安全のために本当にdynamic型が必要な箇所でのみ使用するようにします。
また、似たような型としてdynamicの他にもObject型という型もあります。どちらも全てのオブジェクトを受け付けますが、Objectの方は?を付けるか付けないかでnullを許容するかどうかなどの細かい差を付けることができます(null以外の全てのオブジェクトとしたい場合にはObject型の方をdynamic型の代わりに使うのが適しています)。
比較関係
== の演算子をoverrideする場合hashCodeメソッドもoverrideする
※そもそも後の節でミュータブルなクラスでは==演算子のoverrideが非推奨になっている点と、イミュータブルなクラスとしてconstと共に定義したらoverrideしなくとも==演算子で比較できるようになるのでその辺のルールも守る場合この節の内容は不要かもしれません。
Dartでは独自実装のクラスも含めたインスタンス同士の比較などのために==演算子をoverrideして独自の条件で比較できるようになっています(各フィールドの値が一致するかどうかなどの条件で)。
ただし、デフォルトではDartではhashCodeと呼ばれるメソッドでオブジェクトのハッシュ値を返すようになっており、その値が一致しているかどうか(同じオブジェクトかどうか)で==演算子の判定がされています(Pythonで言うところのid関数での返却値での比較に近い感じになります)。
そのため、独自のクラスなどで==演算子をoverrideした場合にはhashCodeメソッド側もoverrideしないと比較時の挙動が正常に一致判定になってくれないため、==演算子側をoverrideしたら忘れずにhashCodeの方もoverrideするようにします。
==演算子での比較は数学的なルールを守るようにする
==演算子による比較処理は(overrideなどして独自に組む場合などには)以下の数学的な条件を満たすようにします。
- a == a は必ずtrueとなるようにします。
- a == b は b == a と同じ結果が返るようにします。
- a == b 且つ b == c の両方を満たす場合、a == c も満たすようにします。
ミュータブルなクラスでは==演算子のoverrideを避ける
ハッシュ値などの都合で予期せぬ動作をすることがあるためミュータブルなクラスでは==演算子のoverrideは非推奨となっているようです。
また、イミュータブルなクラスであれば「コンストラクタは可能な場合にはconstで定義する」の節で触れたようにoverrideしなくともフィールドの内容を踏まえた比較が有効になるのでこのルールを守る場合は実質的に「==演算子のoverrideは行わない」という判断になりそうです。
ただ、個人的にはPythonとかで__eq__
などのdunder methodを上書きして便利に使うことも結構あったので何とも言えないところではあります。
nullableの値は==演算子で比較には使用しない
予期せぬ挙動をしてしまう可能性があるので?記号の付いたnullableの値は==演算子での比較には使用しないようにします。
==演算子は右辺側がnullではない場合にのみ処理が呼ばれる?ことに起因するそうです。
終わりに(余談)
とりあえず一通りEffective Dartを読んでみたら割とボリュームがあって普段の作業中に失念するものもぼちぼち出るとは思いますが折角記事にしておいたので定期的に見直して復習していこうと思います・・・w
また、この手の資料は割と新しく触る言語では序盤に勉強しますがDartでも特徴的なルールや考え方などを学べて大変勉強になりました・・・!
Dart次回にそこまでまだ精通しておらず理解が浅い点も多々ありましたが、知らない機能や単語なども色々出てきてその辺も大変勉強になりました!
参考文献・参考サイトまとめ