Subterranean Flower

Dartコーディングスタイルガイド

Author
古都こと
ふよんとえんよぷよぐやま!!!

この記事について

Dart公式にあるDart Style Guideの日本語まとめ記事になる。翻訳記事ではない。英語が読める場合は、より正確で情報量の多い原文をあたってほしい。

単純な関数で置き換えられるときは、単一のメンバしか持たない抽象クラスを定義することを避ける

Javaとは違い、Dartにはファーストクラスとしての関数がある。コールバックなどを定義するときは、単に関数を使うべきだろう。

typedef bool Predicate(item);
abstract class Predicate {
  bool test(item);
}

メンバ

インスタンスの作成にはスタティックメソッドではなくコンストラクタをできれば使うようにする

コンストラクタは、主な目的がクラスのインスタンスを返すということが明確だ。

インスタンスを生成するのに、スタティックメソッドを使うべきではない。オブジェクト作成の方法を明らかにしたい場合は名前付きコンストラクタを、目的に応じたサブクラスを作成する場合にはファクトリコンストラクタを、それぞれ使用すべきである。

class Point {
  num x, y;
  Point(this.x, this.y);
  Point.polar(num theta, num radius)
      : x = radius * math.cos(theta),
        y = radius * math.sin(theta);
}
class Point {
  num x, y;
  Point(this.x, this.y);
  static Point polar(num theta, num radius) {
    return new Point(radius * math.cos(theta),
        radius * math.sin(theta));
  }
}

コンストラクタのボディが空の場合は{}ではなく;を使用する

Dartではコンストラクタのボディは省略できる。

class Point {
  int x, y;
  Point(this.x, this.y);
}
class Point {
  int x, y;
  Point(this.x, this.y) {}
}

super()は初期化リストの最後に書く

初期化リストは記述順に実行される。super()を初期化リストの真ん中に書けば、残りの初期化リストが評価される前にスーパークラスの初期化子が評価されることになるだろう。

ただしコンストラクタのボディについてはその限りではない。スーパークラスのコンストラクタボディは、初期化リストのsuper()の位置にかかわらず常に一番最後に実行される。

実際のところ、super()の位置によって問題が起こることは滅多にない。しかし、super()を一番最後に書くことを習慣づけることで、一貫性を確保したり、スーパークラスのコンストラクタボディの実行タイミングを明確にすることができる。

View(Style style, List children)
    : _children = children,
      super(style) {
View(Style style, List children)
    : super(style),
      _children = children {

プロパティへのアクセスにはgetterを使用する

もし次の条件に当てはまる場合はメソッドのかわりにgetterを使用すべきだ。

  • 引数を取らない
  • 値を返す
  • 副作用がない
  • 高速に実行される
num width = rect.width;
bool allGone = collection.isEmpty;
bool canSee = button.visible;
num amount = accounts.sum;           // 計算に時間が掛かる可能性がある
DateTime timeStamp = DateTime.now;   // 呼び出しごとに異なる値を返す
window.refresh;                      // 値を返さない

プロパティの変更にはsetterを使用する

もし次の条件に当てはまる場合はメソッドのかわりにsetterを使用すべきだ。

  • 単一の引数を取る
  • オブジェクトの状態を変更する
  • 対になるgetterがある
  • 冪等性を持つ
  • 高速に実行される
rect.width = 3;
button.visible = false;

ただ「安全」のためだけにgetter/setterでラップすることを避ける

JavaとC#ではすべてのフィールドをgettser/setterでラップするのが一般的だ。これは、Javaではgetter/setterとフィールドアクセスが全く異なるものであり、C#ではプロパティとフィールドアクセスにバイナリ互換が無いためだ。

しかしDartでは違う。Dartではgetter/setterとフィールドの間に違いはない。必要であれば、あとからラップして処理を追加することができる。

class Box {
  var contents;
}
class Box {
  var _contents;
  get contents => _contents;
  set contents(value) {
    _contents = value;
  }
}

privateフィールドとgetterの組み合わせより、public finalフィールドをできれば使うようにする

finalを使用したほうがシンプルである。

class Box {
  final contents = [];
}
class Box {
  var _contents;
  get contents => _contents;
}

単一の式の結果を返すメンバの定義に=>を使用することを検討する

ラムダ式は、単に計算して値を返すだけのメンバを定義するのに適している。

get width => right - left;
bool ready(num time) => minTime == null || minTime <= time;
containsValue(String value) => getValues().contains(value);

複雑な式を一行に詰め込んで=>を利用することもできるが、その場合は通常のボディのほうが適しているだろう。

メソッドチェインのためだけにthisを返すメソッドを避ける

メソッドチェインにはメソッドカースケードを使用するといい。

var buffer = new StringBuffer()
  ..write("one")
  ..write("two")
  ..write("three");
var buffer = new StringBuffer();
buffer
  .write("one")
  .write("two")
  .write("three");

意味が明らかな場合を除きbooleanの引数を避ける

引数にbooleanを含むと、その意味が明らかでない限り、可読性が著しく下がる。

new Task(true);
new Task(false);
new ListBox(false, true, true);

かわりに、名前付き引数や名前付きコンストラクタを使用して、意味を明らかにすべきだ。

new Task.oneShot();
new Task.repeating();
new ListBox(scroll: true, showScrollbars: true);

型アノテーション

公開APIでは型アノテーションをできるだけ書くようにする

型アノテーションはそれ自身が重要なドキュメントとなる。パラメータと戻り値に型アノテーションを追加することで、そのAPIが何を期待し、いかに動作するのかということが明確になる。

ただし、任意の型を許容する場合や、Dartの型システムでは表現できない範囲の値を取る場合は、アノテーションがなくても構わないだろう。

またライブラリ内部のprivateなコードには必ずしもアノテーションを書く必要はない。

install(id, destPath) {
  // ...
}

この場合、idやdestPathがどんな型を受け取るのかわからないし、同期的か非同期的かもわからない。

Future<bool> install(PackageId id, String destPath) {
  // ...
}

アノテーションさえあれば、これらのことが明らかになる。

ローカル変数には型アノテーション無しのvarをできれば使用する

モダンなコードでは、メソッドのボディはできるだけ短く書く。そしてローカル変数は、その初期化式から型が容易に推測可能だ。よって明示的な型アノテーションは邪魔になる場合が多い。型アノテーションを省略しても、まともなエディタなら型を推論してオートコンプリートなどを利用することができる。

Map<int, List<Person>> groupByZip(Iterable<Person> people) {
  var peopleByZip = new Map<int, List<Person>>();
  for (var person in people) {
    peopleByZip.putIfAbsent(person.zip, () => <Person>[]);
    peopleByZip[person.zip].add(person);
  }
  return peopleByZip;
}
Map<int, List<Person>> groupByZip(Iterable<Person> people) {
  Map<int, List<Person>> peopleByZip = new Map<int, List<Person>>();
  for (Person person in people) {
    peopleByZip.putIfAbsent(person.zip, () => <Person>[]);
    peopleByZip[person.zip].add(person);
  }
  return peopleByZip;
}

パフォーマンス重視のコードではnumのかわりにintやdoubleをできれば使用する

単相的な呼び出しは多相的な呼び出しに比べて最適化されやすい。

numは「intかdoubleのどちらか」という意味になる。曖昧な表現はDartランタイムによる最適化を困難にする。

コンストラクタでの初期化には型アノテーションを使用しない

thisキーワードを用いてフィールドの初期化をする場合、その型がフィールドと同じであることは明らかである。

class Point {
  int x, y;
  Point(this.x, this.y);
}
class Point {
  int x, y;
  Point(int this.x, int this.y);
}

関数式では型アノテーションを避ける

関数式の価値はその簡潔さにある。関数を理解するのに型アノテーションが必要なほど複雑なら、関数文を用いるか、メソッドを使用するのがいい。逆に関数式にできるほど短いのであれば、型アノテーションは必要ない。

var names = people.map((person) => person.name);
var names = people.map((Person person) {
  return person.name;
});

必要ない場合、dynamicのアノテーションは避ける

Dartではほとんどの場合、型アノテーションを省略することができる。その場合、自動的にdynamic型として扱われる。

lookUpOrDefault(String name, Map map, defaultValue) {
  var value = map[name];
  if (value != null) return value;
  return defaultValue;
}
dynamic lookUpOrDefault(String name, Map map, dynamic defaultValue) {
  var value = map[name];
  if (value != null) return value;
  return defaultValue;
}

任意のオブジェクトを示すときはdynamicのかわりにObjectを使用する

Objectもdynamicも任意のオブジェクトを受け付ける点では同じだが、異なる部分がある。

Objectによるアノテーションは「任意のオブジェクトを受け付け、かつObjectクラスのメソッドのみを要求する」という意味になる。

dynamicによるアノテーションは、どんな型を受け付けるか、記述できる型アノテーションが存在しないことを意味する。

// 任意のオブジェクトを受け付ける。
void log(Object object) {
  print(object.toString());
}

// Stringかbool型のみ受け付ける。これは型アノテーションでは表現できない。
bool convertToBool(arg) {
  if (arg is bool) return arg;
  if (arg is String) return arg == 'true';
  throw new ArgumentError('Cannot convert $arg to a bool.');
}

名前

型名はUpperCamelCaseを使用する

classとtypedefではUpperCamelCaseを使用する。

class SliderMenu {
  // ...
}

class HttpRequest {
  // ...
}

typedef num Adder(num x, num y);

定数にはlowerCamelCaseをできれば使用する

const pi = 3.14;
const defaultTimeout = 1000;
final urlScheme = new RegExp('^([a-z]+):');

class Dice {
  static final numberGenerator = new Random();
}
const pi = 3.14;
const kDefaultTimeout = 1000;
final URL_SCHEME = new RegExp('^([a-z]+):');

class Dice {
  static final NUMBER_GENERATOR = new Random();
}

他の識別子にはlowerCamelCaseを使用する

var item;

HttpRequest httpRequest;

align(clearItems) {
  // ...
}

メタデータアノテーションに使用されるクラスにはUpperCamelCaseを使用する

アノテーションが引数を取らないのであればlowerCamelCaseも使用できる。

@Foo(anArg)
class A { ... }

@Foo()
class B { ... }

@foo
class C { ... }

ライブラリとソースの名前にはlowercase_with_underscoresを使用する

Good:

  • slider_menu.dart
  • file_system.dart
  • library peg_parser;

Bad:

  • SliderMenu.dart
  • filesystem.dart
  • library peg-parser;

ライブラリのプレフィックス指定にはlowercase_with_underscoresを使用する

import 'dart:math' as math;
import 'dart:json' as json;
import 'package:js/js.dart' as js;
import 'package:javascript_utils/javascript_utils.dart' as js_utils;
import 'dart:math' as Math;
import 'dart:json' as JSON;
import 'package:js/js.dart' as JS;
import 'package:javascript_utils/javascript_utils.dart' as jsUtils;

ライブラリ名にはパッケージ名とドット区切りのパス名を付ける

// In lib/my_package.dart
library my_package;

// In lib/other.dart
library my_package.other;

// In lib/foo/bar.dart
library my_package.foo.bar;

// In example/foo/bar.dart
library my_package.example.foo.bar;

// In lib/src/private.dart
library my_package.src.private;

3文字以上のアクロニムや略語は先頭だけ大文字にする

例えばHTTPSFTPConnectionという名前があるとき、HTTPS FTPなのか、HTTP SFTPなのか区別するすべはない。これを避けるため、3文字以上の略語は先頭だけ大文字にする。

HttpConnection
uiHandler
IOStream
HttpRequest
Id
id
Pt
DB
HTTPConnection
UiHandler
IoStream
HTTPRequest
ID
PT
Db

コメント

docコメントを使ってコメントをつける

Dartは///と/**の2種類のdocコメントをサポートしているが、///を使用することを推奨する。

/// オプション文字列をパースする。それぞれのオプションについて:
///
/// * もし`null`なら、 無視する。
/// * もしstringなら、[validate]を実行します。
/// * もし他の型なら、有効では*ありません*。
void parse(List options) {
  // ...
}

なお、docコメントではmarkdownが使用できる。

あらゆる場所で行コメントを使用する

greet(name) {
  // 正しい名前が表示されるはずだ。
  print('Hi, $name!');
}
greet(name) {
  /* 正しい名前が表示されるはずだ。 */
  print('Hi, $name!');
}

ブロックコメント(/* … */)はコードを一時的にコメントアウトする場合に使用する。

文章のように先頭を大文字にして句読点を打つ(英語)

必ずしも完全な文章である必要はない。例えば“Returns the number of items.”は許容できるコメントだ。

// Remove the last item from the collection.
// remove the last item from the collection

スコープ内にある識別子はdocコメント内で角括弧で囲む

変数やメソッド、型などを角括弧で囲んでおけばdocgenは相互リンクを貼ってくれる。

import 'dart:math';

/// 両方の[Dice]を振って高い方の値を返す。
num greatestRoll(Dice a, Dice b) => max(a.roll(), b.roll());

docコメントは文章で書く

他の言語ではタグやセクションを用いてパラメータや戻り値について記述する。

/// 与えられた名前と略語でフラグを定義する。
///
/// @param name フラグの名前。
/// @param abbr フラグに対する略語。
/// @returns 新しいフラグ
/// @throws IllegalArgumentException 与えられた名前か略語が
///     既に存在していた場合。
Flag addFlag(String name, String abbr) {
  // ...
}

Dartの慣習では、これらはメソッドの説明の中に書き、パラメータは角括弧でハイライトする。

/// フラグを定義する。
///
/// 既に[name]という名前のオプションが存在するか、
/// オプションを使う[abbr]が存在する場合[IllegalArgumentException]をthrowする。
/// 新しいフラグを返す。
Flag addFlag(String name, String abbr) {
  // ...
}

docコメントはメタデータの前に書く

/// _Deprecated: Use [newMethod] instead._
@deprecated
oldMethod();
@deprecated
/// _Deprecated: Use [newMethod] instead._
oldMethod();

スペース

TABを使わない

スペースを使えば、TABと違いどの環境でも同じように表示される。

一行あたり80文字を超えることを避ける

長い行は可読性を下げる。

80文字より長くなるようなら、よりコンパクトにすることを考えるべきだろう。本当にそのクラスはAbstractWidgetFactoryManagerBuilderという名前でないといけないのか?

複数行に渡る式では演算子を前の行に書く

if (isDeepFried ||
    (hasPieCrust && !vegan) ||
    containsBacon) {
  print('Bob likes it.');
}

ラムダ式においても同様だ。

bobLikes() =>
    isDeepFried || (hasPieCrust && !vegan) || containsBacon;
bobLikes()
    => isDeepFried || (hasPieCrust && !vegan) || containsBacon;

複数行に渡る式ではピリオドを次の行に書く

someVeryLongVariable.withAVeryLongProperty
    .aMethodOnThatObject();

ブロックのボディは2つのスペースでインデントする

if (condition) {
  print('hi');
}

継続する行は少なくとも4つ以上のスペースでインデントする

someLongObject.aReallyLongMethodName(longArg, anotherLongArg,
    wrappedToNextLine);

より多くのスペースを使っても構わない。

someLongObject.aReallyLongMethodName(longArg, anotherLongArg,
                                     wrappedToNextLine);

ラムダ式においても同様だ。

bobLikes() =>
    isDeepFried || (hasPieCrust && !vegan) || containsBacon;
bobLikes() =>
  isDeepFried || (hasPieCrust && !vegan) || containsBacon;

関数式に続く行をインデントしない

上記のルールの例外となる。

new Future.delayed(const Duration(seconds: 1), () {
  print('I am a callback');
});
new Future.delayed(const Duration(seconds: 1), () {
      print('I am a callback');
    });

開きカーリーブレイス({)を同じ行に置く

class Foo {
  method() {
    if (true) {
      // ...
    } else {
      // ...
    }
  }
}

すべてのフローコントロールにカーリーブレイスを記述する

ぶらさがりelse問題を避けるためである。

if (true) {
  print('sanity');
} else {
  print('opposite day!');
}
if (true) print('sanity');
else
  print('opposite day!');

例外として、else節の無い一行のif文ならば省略しても構わない。

if (arg == null) return defaultValue;

switch文のcaseは2つ、caseのボディは4つのスペースでインデントする

switch (fruit) {
  case 'apple':
    print('delish');
    break;

  case 'durian':
    print('stinky');
    break;
}

メソッド名、演算子、setterやパラメータリストの宣言の間にスペースを入れない

bool convertToBool(arg) { ... }
bool operator ==(other) { ... }
set contents(value) { ... }
bool convertToBool (arg) { ... }
bool operator == (other) { ... }
set contents (value) { ... }

operatorキーワードの後にはスペースを入れる

bool operator ==(other) => ...;
bool operator==(other) => ...;

二項・三項演算子、コンマの後にはスペースを入れ、単項演算子の後には入れない

a = 1 + 2 / (3 * -b);
c = !condition == a > b;
d = condition ? b : object.method(a, b, c);
if (obj is! SomeType) print('not SomeType');
a=1+2/(3* - b);
c= ! condition==a>b;
d= condition?b:object.method(a,b,c);
if (obj is !SomeType) print('not SomeType');

ループ内でのinの前後、;の後にはスペースを入れる

for (var i = 0; i < 100; i++) {
  // ...
}

for (final item in collection) {
  // ...
}

フローコントロールキーワードの後にはスペースを入れる

while (foo) {
  // ...
}

try {
  // ...
} catch (e) {
  // ...
}

(、[、{の後、)、]、}の前にはスペースを入れない

ジェネリクスの<>においても同様。

var numbers = <int>[1, 2, (3 + 4)];

関数とメソッドの{ の前にスペースを入れる

getEmptyFn(a) {
  return () {};
}
getEmptyFn(a){
  return (){};
}

コンストラクタの初期化リストは各フィールドを別の行にする

MyClass()
    : firstField = "some value",
      secondField = "another",
      thirdField = "last" {
  // ...
}

セミコロンは次の行に置き、スペース4つを入れる。

名前付きパラメータと名前付き引数のコロンの後にはスペースを入れる

class ListBox {
  bool showScrollbars;

  ListBox({this.showScrollbars: false});
}

main() {
  new ListBox(showScrollbars: true);
}
new ListBox(showScrollbars:true);
new ListBox(showScrollbars : true);

オプショナルパラメータの=の前後にはスペースを入れる

class HttpServer {
  static Future<HttpServer> listen([int port = 80]) {
    // ...
  }
}
import 'dart:async';

class HttpServer {
  static Future<HttpServer> listen([int port=80]) {
    // ...
  }
}

メソッドカスケードは2つ以上のスペースでインデントする

list = new List()
  ..addAll([1, 2, 3])
  ..addAll([4, 5, 6]);
list = new List()
    ..addAll([1, 2, 3])
    ..addAll([4, 5, 6]);

ListとMapのリテラルは2つ以上のスペースでインデントし、閉じカッコは次の行に書く

var simpleList = [1, 2, 3];
var simpleMap = {'a': 1, 'b': 2, 'c': 3};

var fooList = [
  'a',
  'b',
  'c'
];

var barMap = {
  'a': 'b',
  'c': 'd'
};

var listInsideMap = {
  'a': ['b', 'c', 'd'],
  'e': [
    'f',
    'g',
    'h'
  ]
};

var mapInsideMap = {
  'a': {'b': 'c', 'd': 'e'},
  'f': {
    'f': 'g',
    'h': 'i'
  }
};

var mapInsideList = [
  {
    'a': 'b',
    'c': 'd'
  },
  {
    'a': 'b',
    'c': 'd'
  },
];
var fooList = [
    1,
    2,
    3
];

var fooList = [
  1,
  2,
  3];

var fooList = [1, 2,
  3, 4];