341
231

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

FlutterAdvent Calendar 2019

Day 5

Effective Dartまとめ

Last updated at Posted at 2019-12-04

Flutter Advent Calendar の5日目です。
Effective Dartを意訳してまとめました。
他の言語でも共通で言えるようなことも多く記載されています。その辺りは一部割愛しましたが、Dartを書くにあたって知っておいた方がよいと私が感じた部分をまとめました。(それでもかなり長いですが…)

また、ここでは割愛しますが、Flutterとしてのスタイルガイドはこちらに記載されているので、併せて読むと良いと思います。
Style guide for Flutter repo

Style Guide

命名規則

対応style guide
importPrefixの例
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 > DartWrapping and BracesタブのHard wrap atの数値を変更すれば反映されます。
スクリーンショット 2019-10-22 14.38.43.png

すべての制御分にカッコをつける(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を指定する

partpart ofは対の構文になっていて、簡単に言うとファイル分割の手段として利用できます。
レガシーな理由から、part ofには親のファイル名だけを指定できますが、分かりにくくなるため、直接ファイルへのpathを記述するようにしましょう。

part-ofの書き方
// [part of my_library]と省略しない
part of "../../my_library.dart";

Booleans

オプショナルな場合は??を使いましょう

Booleanの使い方
// 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)

ListとMapの生成
var points = [];    // List()は使わない
var addresses = {}; // Map()は使わない

// 型を宣言する場合
var points = <Point>[];
var addresses = <String, Address>{};

CollectionがemptyかどうかはisEmpty/isNotEmptyを使う

Collectionのサイズチェック
// 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が慣用的とのこと。

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が便利です。

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()は避ける

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)

これはみた方が早いです。

tear-off
// 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をデフォルト値として書かなくても良いです。

オプショナルな引数のデフォルト値には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を後から変更できるようになっていて、その場合areacircumferenceの再計算が必要になるため、厳密なコードはより複雑になります。

Members

不必要にgetter/setterを作らない(unnecessary_getters_setters)

Javaのように、privateなメンバーのsetter/getterは作らない。

不必要なgetter/setter
// 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)同じ名前のローカル変数がある場合

thisが必要な場合(1)
class Box {
  var value;
  void update(value) {
    // この場合は区別するために[this]が必要
    this.value = value;
  }
}

ただし、コンストラクタの中ではthisが不要です。

コンストラクタパラメータの場合はshadowしないのでthis不要
class Box extends BaseBox {
  var value;

  Box(value)
      : value = value,  // ここに[this]は不要。左辺が[this.value]であるのは自明だから。
        super(value);
}

(2)Named Constructorにアクセスする場合

thisが必要な場合(2)
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)

コンストラクタの引数を直接フィールドに代入できます。

initializingFormalの例
class Point {
  num x, y;
  Point(this.x, this.y);
}

ちなみに、initializing formalに型は不要です。

initializingFormalに型宣言は不要
// bad
class Point {
  int x, y;
  Point(int this.x, int this.y);
}

emptyなコンストラクタのbodyは{}ではなく;を使う(empty_constructor_bodies)

emptyなコンストラクタbody
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指定にしましょう。

重複する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-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した部分までしか追跡できません。

rethrowを使おう
try {
  somethingRisky();
} catch (e) {
  if (!canHandle(e)) {
    // [throw e]と書くと、今までのスタックトレースが追跡できなくなります
    rethrow; 
  }
  handle(e);
}

非同期処理

async/awaitを使って、直接Futureを操作しないようにしましょう

async/awaitを使った方が可読性やデバッグのしやすさが向上します。

[good]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;
  }
}
[bad]Futureを使う
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が有用なケース

  1. コード内でawaitを使っている場合
  2. 戻り値がFutureで、Errorを非同期で返却する場合
  3. 戻り値がFutureで、Futureで暗黙的にラップしたい場合
便利なasync
// 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を使ってしまう傾向があるが、より簡潔な書き方を学びましょうとのことです。

[bad]Completerの誤った使い方
Future<bool> fileContainsBear(String path) {
  var completer = Completer<bool>();

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

  return completer.future;
}
[good]Completerの使用は避ける
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して処理を行うことがあります。

[good]FutureOrの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;
  }
}

このとき、Tintなどプリミティブ型であれば問題はないのですが、Objectの場合は以下のコードでは問題が発生します。

[bad]FutureOrのcast
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値の場合、動詞が明確であることも多く、記述量も減って分かりやすくなります。

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で関数を定義した方が簡潔に済みます。

[bad]ひとつのメンバーだけのabstractクラスは定義しない
abstract class Predicate<E> {
  bool test(E element);
}
[good]代替案
typedef Predicate<E> = bool Function(E element);

静的メンバーのみのクラスは定義しない(avoid_classes_with_only_static_members)

Javaなどと違って、Dartにはトップレベルの関数、変数、定数があるため、何かを定義するためだけにクラスは必要ありません。
名前の重複などを防ぐ目的で、必要なものが名前空間である場合はライブラリの方が適しています。

[good]トップレベルの関数を使う
DateTime mostRecent(List<DateTime> dates) {
  return dates.reduce((a, b) => a.isAfter(b) ? a : b);
}

const _favoriteMammal = 'weasel';
[bad]静的メンバーのみのクラスは定義しない
class DateUtils {
  static DateTime mostRecent(List<DateTime> dates) {
    return dates.reduce((a, b) => a.isAfter(b) ? a : b);
  }
}

class _Favorites {
  static const mammal = 'weasel';
}

ただし、厳密に守る必要はなく、定数と列挙型のタイプではそれらをクラスにグループ化するのが自然かもしれません。

[good]定数と列挙型はクラス化してもよい
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の場合と似ています。以下の例を見ていただくのが一番直感的にわかると思います。

setter
// bad
rectangle.setWidth(3);
button.setVisible(false);

// good
rectangle.width = 3;
button.visible = false;

詳細に記述すると

  • 単一の引数を取り、結果を返却しない
  • 操作によりオブジェクトの状態が変更される
  • 操作はべき等(同じ操作をしても結果は変わらない)である
    • 内部的にキャッシュやロギングが発生したとしても、それは問題ない
    • 使う側から見て変更が見えなければOK

対応するgetterなしでsetterのみを定義しない(avoid_setters_without_getters)

ユーザはsetter/getterをセットとして目に見えるプロパティとして捉えます。仮に、書き込みだけできて読み込みできないプロパティがあるとすると、混乱を招いてしまいます。また、getterがない場合、=を用いて値の変更はできますが、+=を使うことができず、不便になります。

setterは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を使いましょう。

MethodCascades
// 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)

publicフィールドには型を宣言する
// bad
install(id, destination) => ...

// good
Future<bool> install(PackageId id, String destination) => ...

「明白」の定義はありませんが、以下の場合などが該当します。

  • 文字列リテラル
  • コンストラクター呼び出し
  • 型が明白な定数への参照
  • 数値と文字列の簡単な式
  • 一般的によく知られたようなメソッド
    • int.parse()など

悩むような場合は、基本的に型宣言を書くことを推奨します。
また、型が明白でも、型が他のライブラリで定義された型などの場合は、型を明示した方が安全です。(そのライブラリの仕様が変更されて戻り値が変わった時に気付けない可能性があるため)

型が不明な場合は、privateフィールドとトップレベル変数には型を付ける(prefer_typing_uninitialized_variables)

privateなフィールドの型宣言
// 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と書きたくなりますが、この場合、highestint型に推論されます。
この時、[scores]はnum型の配列のため、配列内に1.2のようなdouble型が含まれていた場合、highestへの代入はできずにエラーとなってしまいます。
そのため、明示的に[num]で型宣言をしてあげる必要があります。

dynamic型は明示的に宣言する

Dartは型推論があるため、型宣言を省略することが可能です。仮に型推論ができなかった場合は動的にdynamic型として扱われます。そのため、当然dynamic型も記述を省略することは可能になっています。
しかし、コードの保守・運用面を考慮した場合、意図的にdynamic型を必要としている場合は、明示的にdynamicの宣言を記述するべきです。(型宣言が省略されていた場合、後からコードを読んだ人は、これが意図してdynamicになっているのか、本来別の型だったのが型推論できずに失敗してdynamic型になっているかの判断ができません)
※ちなみに、Dart2以前はこれと全く逆のことを推奨していたそうです。

関数型の定義は、戻り値・パラメータの型も宣言する

関数型Functionは戻り値・引数の宣言なしでdynamicのように使用することが可能です。が、極力戻り値・引数の型は宣言すべきです。

Function型に戻り値・引数を宣言する
// good
bool isValid(String value, bool Function(String) test) => ...
// bad
bool isValid(String value, Function test) => ...

ただし、以下の様に戻り値・引数の型が動的になる場合は例外として扱われます。

Function型の例外事例
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であることは自明なので、省略しましょう。

setterの戻り値は省略
// good(先頭に戻り値のvoidは不要)
set foo(Foo value) { ... }

レガシーなtypedefの定義フォーマットは使用しない(prefer_generic_function_type_aliases)

Dartにはtypedefの記述方法が2パターンあります。

typedef
// 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)
ジェネリクス関数型に名前を割り当てる方法がありません。レアなケースですが、以下の例をご覧ください。
スクリーンショット 2019-11-17 18.33.30.png
Comparison1Comparison2ともにジェネリクスの型は宣言していません。が、Comparison2ではFunction<T>と定義しているため、型推論時にint Function<T>(T, T)と表示されているのが分かります。一方、Comparison1Tが抜け落ちて引数がdynamic型となってしまっています。

(2)
関数の引数に一つだけパラメータが記述された場合、それは型ではなく、パラメータ名として識別されます。例えば以下の例です。
スクリーンショット 2019-11-17 18.30.16.png

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 を使う

まず、Objectdynamic の違いですが、こちらの記事にも記述されています。
Dartの不定型について

「本当に何型であっても良い」ことを示す際には Object を、「型Aと型Bのうちどちらか、のような、期待する型が決まってはいるがDartの型システムで表現できない型表現」を dynamic でするべき、らしいです。

Objectと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> を戻り値に指定します。

FutureFuture<Null> を見かけることもあるかもしれませんが、これは過去にvoidが許可されていなかった頃の記述方法です。

また、非同期処理を待つ必要がない場合などは、voidでOKです。

FutureOr<T>は戻り値に使用しない

FutureOr<T> が戻り値の場合、戻り値はTまたはFutureとなります。
そのため、使う側は必ず型をTなのかFutureなのかをチェックした上で処理を書かなくてはいけません。

戻り値にFutureOrは使用しない
// 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、となります。

callbackの戻り値には[FutureOr]が使用可能
// 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)を避け、名前付きパラメータにします。

booleanのoptionalParameterは名前付きで定義する
// 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は排他的にする

範囲を引数で指定する場合、コアライブラリと同様にしましょう。

rangeの指定
// 開始位置の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パッケージにhashValueshashListという関数もあります。パッケージがui、というのが気になりますが…
スクリーンショット 2019-11-22 1.06.08.png

==のoverrideの中では、nullチェックは行わない(avoid_null_checks_in_equality_operators)

このチェックは言語側で自動的に行われます。
つまり、==演算は、右辺がnon-nullでない場合にのみ実行されるため、==の引数にnullが入ってくることはあり得ず、nullチェックは不要となります。

[==]ではnullチェック不要
// bad
class Person {
  final String name;
  // ···

  // nullチェックはDart側で行うため不要
  bool operator ==(other) => other != null && ...
}
341
231
9

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
341
231

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?