HAKUTAI Tech Notes

IT関連、数学のことなどを主として思いつきで色々書き溜めていきます

Dart学習時のメモ色々

1月なので何か新しいことでもやってみようかと思い、Flutterでのアプリ開発に足を踏み入れてみることにした。ただしその前に、Flutter開発に必要なDart言語が全くの初見だったので基礎の学習から始めた。Dartの自己学習中に引っかかった部分やDart特有の機能などについて調べた際のメモが結構貯まったので、せっかくなので項目別に整理してまとめることにした。


環境

  • Dart SDK 2.18.6

使用した参考書

Dartを始めて触るにあたり下記の書籍を参考にしながら学習を進めた。


Dart入門 - Dartの要点をつかむためのクイックツアー
各種の構文や演算子の使い方、クラス、非同期処理などDart言語の基本的な文法・仕様を一通り確認できる。Dartの公式ドキュメントを簡潔にしたような感じで、要点が分かりやすくまとめられている。最初はこの本のように必要最低限の基本事項を確認できるものに目を通して、部分的に詳細を知りたい場合は公式ドキュメントを読み込むというやり方が良さそう。


脱初心者のための問題集 Dart編
Dartの基本文法(特に制御構文やデータ型別のメソッドの使い方など)に関する練習問題が55問収録されている。上記の本で基本を確認した後で一気にこの問題集に取り組むとだいぶDartに慣れることができると思う。ただし、クラスや非同期処理に関する問題はほぼない(クラスの問題が1問あるぐらい)ので、必要に応じて別途自分でフォローする必要がある。


学習メモ

Null Safetyについて

DartはNull Safety(null安全)な言語で、nullにアクセスする可能性のある処理が含まれているとコンパイルの時点で失敗するので、プログラム実行中にnull絡みで落ちるといったことを防いでいる。Null Safetyが備わっていない言語から入ってくると、何てことない処理を書いたつもりでもNull Safetyに引っかかって「あれ?」とハマってしまうこともある。
Dartでは変数の型を宣言するとnull非許容型(non-nullable)となるのがデフォルトであり、この変数に対してnullが代入される可能性がある場合はコンパイルエラーとなる。

void main() {
  int number = 0;
  List<int> list = [12, 50, 9];

  number = null; // エラー
  list.add(null); // エラー
}

変数がnullになる可能性がありnull許容型(nullable)として宣言したい場合は型宣言に?を付ける。

void main() {
  int? number = 0;
  List<int?> list = [12, 50, 9];

  number = null; // nullを代入可
  list.add(null); // nullを追加可
  print(number); // -> null
  print(list); // -> [12, 50, 9, null]
}

null許容型の式、変数だがnullにならないことが確実に分かっている場合、nullアサーション演算子!を付けるとnull非許容型に変換できる。
例えば、下記のreturnStringAlways()は必ず「Hello」が返ってくると分かるが、null許容型なのでそのままではNull Safetyに引っかかり後続のindexOf()は使えない。この場合、!を付けてnull非許容型に変換することでindexOf()が使えるようになる。

void main() {
  print(returnStringAlways()!.indexOf('e')); // -> 1
}

// null許容型だが必ず文字列を返す関数
String? returnStringAlways() => 'Hello';

null許容型の式、変数でnullになる可能性がある場合、条件付きプロパティアクセス演算子?.を使用すれば後続に処理を繋げることができる。?.の左側がnullでなければ右側の処理が実行され、左側がnullならば右側の処理は実行されないのでそのままnullとして評価される。

void main() {
  // 現在時刻の秒が偶数なら文字列、奇数ならnullを代入する
  String? str = DateTime.now().second % 2 == 0 ? 'ABCDEFG' : null;
  print(str?.length); // -> 7 or null
  print(returnListOrNull(10)?..add(4)); // -> [1, 2, 3, 4]
  print(returnListOrNull(7)?..add(4)); // -> null
}

// 偶数が渡されたらintのリスト、奇数が渡されたらnullを返す
List<int>? returnListOrNull(int n) => n % 2 == 0 ? [1, 2, 3] : null;

null許容型の式、変数がnullになる場合に代替値を与えたい時はnull合体演算子??を使用する。??の左側がnullの場合のみ右側の式が評価される。
また、null合体代入演算子??=を使えば、null許容型変数がnullの場合にその変数自身に値を代入することができる。三項演算子を使っても同じことができるが、??=のほうが簡潔に書ける。

void main() {
  // 現在時刻の秒が偶数なら500、奇数ならnullを代入する
  int? number = DateTime.now().second % 2 == 0 ? 500 : null;
  print(number ?? 0); // -> 500 or 0
  number ??= 0;
  // number = number != null ? number : 0; 三項演算子を使った場合と同じ結果
  print(number); // -> 500 or 0
}


StringBufferでの文字列の連結

StringBufferクラスの説明には、「文字列を効率的に連結するためのクラス」とある。

api.dart.dev

何が効率的なのかと言うと、最初にバッファー(StringBufferオブジェクト)を1つ生成してそれに文字列を追加していくことで、何度も新しいインスタンスを作ることなく文字列を結合できる点である。
Stringクラスの+を使っても同じように文字列の結合はできるが、こちらは毎回新しいインスタンスが作られる。例えば、下記のようにidentical()を利用して元のstrと文字列結合後のstr += 'xxxx'を比較するとfalseが返ってくるので、両者は異なるインスタンスであることが分かる。

void main() {
  var buffer = StringBuffer();
  buffer.write('xxxx'); // 同一インスタンスとして文字列の結合が可能

  var str = '';
  print(identical(str, str += 'xxxx')); // -> false
}

StringBuffeを使う例。最初にバッファーを生成しておき、StringBuffeクラスの各種メソッドを利用してバッファーにオブジェクトを追加していく。toString()を使うとバッファーに含まれる要素が結合されて単一の文字列として出力される。

void main() {
  var buffer = StringBuffer('first');
  buffer.write('second'); // write():bufferにオブジェクトの文字列表現を追加する
  print(buffer.length);

  buffer.writeln(); // writeln():bufferに改行を追加する
  buffer.writeAll([111, 'aaa', true, '\n']); // writeAll():bufferにIterableオブジェクトの要素を順に全て書き込む
  buffer.writeAll([123, 456, 789], '-'); // 第2引数の区切り文字でIterableオブジェクトの要素を繋いだ文字列を追加する
  buffer.writeln();
  [0x52, 0x45, 0x44].forEach((code) => buffer.writeCharCode(code)); // charCode():ASCIIコード表のコードに対応する文字を書き込む
  print(buffer.toString()); // toString():bufferの中身を単一の文字列に変換する

  buffer.clear(); // clear():bufferの中身をクリアする
  print('クリア:${buffer.toString()}');
}
11
firstsecond
111aaatrue
123-456-789
RED
クリア:

同じことをStringBufferを使わずに行うと下記のようになる。

void main() {
  var str = 'first';
  str += 'second';
  print(str.length);

  str += '\n';
  str += [111, 'aaa', true, '\n'].join();
  str += [123, 456, 789].join('-');
  str += '\n';
  str += String.fromCharCodes([0x52, 0x45, 0x44]);
  print(str);

  str = '';
  print('クリア:${str}');
}


データ型の確認と比較

オブジェクトのruntimeTypeプロパティにアクセスすると、そのオブジェクトのデータ型を確認できる。javascriptで言うところのtypeofと同じようなことができる。

void main() {
  String str = 'Saitama';
  List<String> list1 = ['Tochigi', 'Tokyo', 'Kanagawa'];
  const list2 = [123, 'Gunma', true, 'Ibaraki', 456];

  print(str.runtimeType); // -> String
  print(list1.runtimeType); // -> List<String>
  print(list2.runtimeType); // -> List<Object>
}

オブジェクトが特定のデータ型かどうかを確認する時はis(否定はis!)を利用すればいい。

void main() {
  print('Chiba' is int); // -> false
  print('Chiba' is! int); // -> true
}


whereType()とwhere()

IterableクラスのwhereType()は、<>で指定した型を持つ要素のみ抽出したIterableを返す。例えば、whereType<int>()なら整数値の要素のみ抽出してくれる。 返り値はWhereTypeIterable<int>型なのでtoList()で変換したものをList<int>に格納可能。

void main() {
  const list = [12, 33, true, 5, 19, 50, 'abcd', 'efgh'];
  List<int> intList = list.whereType<int>().toList();
  print(intList); // -> [12, 33, 5, 19, 50]
  print(list.whereType<int>().runtimeType); // -> WhereTypeIterable<int>
}

where()でも同様のことは可能だが、様々な型の要素を含むList<Object>が対象の場合は結果がWhereIterable<Object>型となるので、toList()で変換してもList<int>には格納できない。なお、cast()でキャストすれば格納は可能だが、コレクションの型を変更するためにcast()を利用するのは非推奨とされているので、やはりこの場合はwhereType()を使った方がいい。

void main() {
  const list = [12, 33, true, 5, 19, 50, 'abcd', 'efgh'];
  print(list.where((e) => e is int).runtimeType); // -> WhereIterable<Object>
  List<int> intList1 = list.where((e) => e is int); // これはエラー
  List<int> intList2 = list.where((e) => e is int).toList().cast<int>(); // castすれば代入可
  print(intList2); // -> [12, 33, 5, 19, 50]
  print(intList2.runtimeType); // -> CastList<Object, int>
}


複数条件でのMapリストのソート

要素がMapオブジェクトのリストを、複数の比較条件でソートする場合の書き方。
sort()の比較関数の中で使っているcompareTo()は比較対象が同値の場合は0を返すので、1つ目の比較結果が0の時だけ2つ目の比較に移るようにif文を使って制御する。同じ要領でさらに多くの比較条件をつなげることもできる。
なお、複数条件で比較する場合は比較関数の中が2行以上になるので、アロー演算子=>で記述することはできない。

void main() {
  List<Map<String, dynamic>> teams = [
    { 'name': 'Spain', 'points': 4, 'difference': 6 },
    { 'name': 'Costarica', 'points': 3, 'difference': -8},
    { 'name': 'Germany', 'points': 4, 'difference': 1 },
    { 'name': 'Japan', 'points': 6, 'difference': 1 },
  ];

  // 各チームを1.points、2.differenceの順に比較して並び替える
  teams.sort((a, b) {
    int result = b['points'].compareTo(a['points']); // 1つ目のkey 'points' での比較(降順)
    if (result != 0) return result;
    return b['difference'].compareTo(a['difference']); // 2つ目のkey 'difference' での比較(降順)
  });

  print(teams); // -> [{name: Japan, points: 6, difference: 1}, {name: Spain, points: 4, difference: 6}, {name: Germany, points: 4, difference: 1}, {name: Costarica, points: 3, difference: -8}]
}


Mapのソート

Mapオブジェクトの要素には順番の概念がないので、keyやvalueに応じてソートしたい場合は少し工夫する必要がある。


◆ SplayTreeMap.from()を使ってソートする

SplayTreeMapクラスはkeyに順番が存在するマップである。SplayTreeMapクラスのメンバの1つであるfrom()は、比較関数に応じてソート対象のMapの中身を並び替えるものである。SplayTreeMapを使うためにはcollectionライブラリをインポートする必要がある。

from()の第2引数の比較関数には比較対象の2つのkeyが渡される。 keyをそのままcompareTo()の比較対象にすればkeyでのソートになり、keyで参照したvalueを比較対象にすればvalueでのソートになる。ただし、valueでソートする場合、値が同値になると片方の要素は消えてしまうので注意が必要。

import 'dart:collection';

void main() {
  Map<String, int> population = {
    'Gunma': 193,
    'Tochigi': 193,
    'Ibaraki': 299,
    'Saitama': 704,
    'Chiba': 603,
    'Tokyo': 1237,
    'Kanagawa': 869
  };

  // keyでソート
  SplayTreeMap<String, int> sortedByKey = SplayTreeMap.from(population, (k1, k2) => k1.compareTo(k2));
  // valueでソート
  SplayTreeMap<String, int> sortedByValue = SplayTreeMap.from(population, ((k1, k2) => population[k1]!.compareTo(population[k2]!)));
  print(sortedByKey);
  print(sortedByValue);
}
sortedByKeyの出力
{Chiba: 603, Gunma: 193, Ibaraki: 299, Kanagawa: 869, Saitama: 704, Tochigi: 193, Tokyo: 1237}

sortedByValueの出力(Tochigiが消えてしまう)
{Gunma: 193, Ibaraki: 299, Chiba: 603, Saitama: 704, Kanagawa: 869, Tokyo: 1237}

valueで比較した時に同値、つまりcompareTo()の結果が0となって要素が消えてしまう場合は下記のようにすれば良い。要はcompareTo()が0になる時は意図的に0以外の値を返すようにしてやる。 下の例では、valueでの比較結果が0になる場合、次にkeyでソートして0が返らないようにしている。

  // valueでソート(同値になる場合を考慮)
  SplayTreeMap<String, int> sortedByValue = SplayTreeMap.from(population, ((k1, k2) {
    int result = population[k1]!.compareTo(population[k2]!);
    if (result != 0) return result;
    return k1.compareTo(k2);
  }));
  print(sortedByValue);
{Gunma: 193, Tochigi: 193, Ibaraki: 299, Chiba: 603, Saitama: 704, Kanagawa: 869, Tokyo: 1237}


◆ Map.entriesとMap.fromEntries()を組み合わせてソートする

Mapentriesプロパティを参照すると、MapEntryオブジェクトとして各要素のkeyとvalueのペアを取得できる。これをtoList()MapEntryのリストに変換する。

print(population.entries.toList());
[MapEntry(Gunma: 193), MapEntry(Tochigi: 193), MapEntry(Ibaraki: 299), MapEntry(Saitama: 704), MapEntry(Chiba: 603), MapEntry(Tokyo: 1237), MapEntry(Kanagawa: 869)]

ソート処理の部分はリストに対するソートと同じで、比較対象をkeyにすればkey、valueにすればvalueでソートできる。 後は、ソートしたリストをMap.fromEntries()に渡してMapオブジェクトに変換してやれば良い。 この方法であれば、valueが同値であっても要素が消えてしまうことはない。

Map<String, int> sortedByKey = Map.fromEntries(
  population.entries.toList()..sort((a, b) => a.key.compareTo(b.key)));
Map<String, int> sortedByValue = Map.fromEntries(
  population.entries.toList()..sort((a, b) => a.value.compareTo(b.value)));


外部ライブラリの使用

Dartの標準ライブラリ(接頭辞dart:を付けてインポートするライブラリ)ではなく外部ライブラリを使用したい場合、なにもせず対象のライブラリをインポートしようとしても当然ながら「そんなものはない」と言われる。

Error: Not found: 'package:intl/intl.dart'


外部ライブラリを利用するためにはPubパッケージ・マネージャでライブラリを管理する必要があり、そのためにはpubspec.yamlを作成して必要な設定を記述する必要がある。実行対象のDartファイルとpubspec.yamlを含むディレクトリを1つのDartパッケージとして扱う。Flutterのプロジェクトとして新規作成した場合は自動でpubspec.yamlが作成されるが、今回はただDartのコードを単体で実行するだけなので自分でファイルを作成する必要がある。

pubspec.yamlに最低限記述しなければならないフィールドはnameenvironmentの2つである。
nameはパッケージの名称を指定するもので、Dartのコードを実行するだけなら適当な名称でいい。使える文字は小文字の半角英数字かアンダースコアのみで、先頭が数字のものやDartの予約語は指定不可である。
environmentはパッケージが動作するDart SDKのバージョンを指定するもので、Dart 2 以降は指定が必須である。例えば>=2.10.0 <3.0.0と指定した場合、2.10.0以降3.0.0より前のバージョンのDart SDKで動作するパッケージになる。

name: xxxx
environment:
  sdk: ">=2.10.0 <3.0.0"

pubspec.yamlを作成したら、下記のコマンドで目的のライブラリを取得する。(intlを取得する例)

$ dart pub add intl

正常に取得されればpubspec.yamldependenciesフィールドが追加され、対象のライブラリのバージョン情報が追記される。これでプログラムの内部で対象のライブラリをインポートして使うことができるようになる。

name: xxxx
environment:
  sdk: ">=2.10.0 <3.0.0"
dependencies:
  intl: ^0.18.0