🤨

【Dart入門(5)】型|組み込み型(Collections)

に公開

Dartの組み込み型について

dartにはデフォルトで用意されている型が存在します。
以下はそのほかの言語でも用意されている型です。

よく使用する型
  • Numbers (int, double)
    • 数値に関する型で整数型と小数型
  • Strings (String)
    • 文字列に関する型
  • Booleans (bool)
    • 論理値に関する型。trueかfalseか
  • Records ((value1, value2))
    • 固定長で異なる型が共存できる型。タプルに該当
  • Functions (Function)
    • 関数型。関数もオブジェクトなので型があります。
  • Lists (List, also known as arrays)
    • 配列型
  • Sets (Set)
    • 集合型
  • Maps (Map)
    • 辞書型
  • Runes (Runes; often replaced by the characters API)
  • Symbols (Symbol)
  • The value null (Null)
    • 何もないを意味する型

これ以外に、dart特有の型も存在します。

dart特有の型
  • Object: Null型を除いた全ての型の親となる型。
    • Null以外はObject型を継承している。
  • Enum: 列挙型。
  • Future and Stream: 非同期処理で使用される型。
    • Future: 非同期型でawaitしてその結果を取得できる。
    • Stream: web socket 通信のように常にどこかと通信して結果を受け取る型。
  • Iterable: for ループで使用される型。
  • Never: 必ず成功しないことを意味する型。必ずエラーを吐く関数などが返す型。
  • dynamic: 型が決定しない型。大抵はObject型やObject?を使用した方が良い。
  • void: 値が使用されないことを意味する型。何も返却しない関数が返す。

これらの型は今すぐ理解する必要はありませんが、頭の片隅にこのような型があると認識しておくことが大切です。


組み込み型を詳しくみていく(Collections)

先ほど紹介した型のよく使用する型の使い方を詳しくみていきましょう。
collectionsにはlist(配列型)とset(集合型)とmap(辞書型)が存在する。

List(配列について)

Listは同じ型やinterfaceを持つオブジェクトを並べた配列オブジェクト。list型のオブジェクトの作り方は、各要素を,で区切り両端を[]で囲んで作る。

List<String> sampleStringList = ["1", "aaa", "nnn"];

List<int> sampleIntList = [1,2,4,5,6,7,-1]

List<Object> sampleList = [2, "D"];

List型でよく使用するメソッドについて説明します。

  • 基本操作
    • add, addAll, insert, remove, removeAt, clear
  • 取得と検索
    • first, last, [{数字}], indexOf, contains
  • 並び替えと変換
    • sort, reverse, map
  • 条件抽出
    • where, firstWhere, any, every
  • 集計とそのほか
    • length, isEmpty, reduce, fold, sublist

これら全て覚える必要はないですが、頭の片隅には置いておきましょう

要素の追加と削除

var list = [1,2,4,5];
list.add(10);
// [1,2,4,5,10]
list.addAll([1,2,3]);
// [1,2,4,5,10,1,2,3]
list.insert(1,-2);
// [1,-2,2,4,5,10,1,2,3]
list.remove(10);
// [1,-2,2,4,5,1,2,3]
list.removeAt(2);
// [1,-2,4,5,1,2,3]
list.clear();
// []

取得と検索

var list = ["a", "c", "b"];
print(list.first);
//a
print(list.last);
//b
print(list[2]);
//b
print(list.indexOf("c"));
//1
print(list.contains("b"));
//true

並び替えと変換

var list = [3, 1, 4, 1, 5];

list.sort();             // [1, 1, 3, 4, 5] : 昇順ソート
list.reversed.toList();  // [5, 1, 4, 1, 3] : 逆順
var mapped = list.map((x) => x * 2).toList(); // [2, 2, 6, 8, 10]

条件抽出

var list = [1, 2, 3, 4, 5, 6];

var evens = list.where((x) => x.isEven).toList(); // [2, 4, 6]
var firstEven = list.firstWhere((x) => x.isEven); // 2
var anyEven = list.any((x) => x.isEven);          // true
var allEven = list.every((x) => x.isEven);        // false

その他

var list = [1, 2, 3, 4];
print(list.length);     // 4 : 要素数
print(list.isEmpty);    // false : 空かどうか
print(list.reduce((a, b) => a + b)); // 10 : 累積計算
print(list.fold(100, (a, b) => a + b)); // 110 : 初期値付き累積計算

Set

Set型は順番なしの要素の集合。リストの場合は[]で要素を括っていましたが、集合の場合は{}で括ります。型はSet<T>で指定します。

Set<String> wordSet = {"word1", "word2", "word3"}; 

Setでよく使用するメソッド

  • 基本操作: add, remove, clear

  • 確認: contains, isEmpty, length, first, last

  • 集合演算: union, intersection, difference

  • 変換: map, where, toList, toSet

基本操作

Set<int> set = {1, 2, 3};

set.add(4);         // {1, 2, 3, 4} : 要素を追加
set.add(2);         // {1, 2, 3, 4} : 重複なので無視される
set.remove(3);      // {1, 2, 4}   : 指定した要素を削除
set.clear();        // {}          : 全削除

要素確認

Set<String> set = {"a", "b", "c"};

print(set.contains("a")); // true : 要素を含むか
print(set.isEmpty);       // false : 空かどうか
print(set.length);        // 3     : 要素数
print(set.first);         // a     : 最初の要素
print(set.last);          // c     : 最後の要素

集合演算

var a = {1, 2, 3};
var b = {3, 4, 5};

print(a.union(b));        // {1, 2, 3, 4, 5} : 和集合
print(a.intersection(b)); // {3}             : 積集合
print(a.difference(b));   // {1, 2}          : 差集合 (a - b)

変換

var set = {1, 2, 3};

set.forEach((x) => print(x));  // 1, 2, 3 を順番に出力

var doubled = set.map((x) => x * 2).toSet(); // {2, 4, 6}
var filtered = set.where((x) => x.isEven).toSet(); // {2}


Map(辞書について)

Map は「Key → Value」をペアで保持するコレクションです。リテラルは {キー: 値, ...} の形です。
注意:空の {}Map を表します。空の Set を作るには <T>{} を使います(後述)。

final userAges = <String, int>{
  'Alice': 20,
  'Bob': 25,
};

// add
userAges['Carol'] = 30;
// addAll                    
userAges.addAll({'Dave': 40, 'Eve': 35});
//update  
userAges.update('Bob', (v) => v + 1);
// なければ追加(あるなら何もしない)             
userAges.putIfAbsent('Frank', () => 18);          

// 取得・確認
// 20(なければ null)
print(userAges['Alice']);
// Keyを含んでいるかどうか              
print(userAges.containsKey('Dave'));   // true
// Valueを含んでいるかどうか
print(userAges.containsValue(30));     // true

// 走査
// これはよく使用するので覚えておきましょう
for (final entry in userAges.entries) {
  print('${entry.key} = ${entry.value}');
}

// 変換
// mapも高頻出
final doubled = userAges.map((k, v) => MapEntry(k, v * 2));  // 値を2倍
final onlyTwentyPlus = Map.fromEntries(
  userAges.entries.where((e) => e.value >= 20),
);

// 削除
// Keyに該当するデータを削除
userAges.remove('Alice');
userAges.removeWhere((k, v) => v < 20);
userAges.clear();
よく使うプロパティ/メソッド(Map)
  • 追加/更新/削除
    []= / addAll / update / updateAll / putIfAbsent / remove / removeWhere / clear
  • 取得/調査
    [] / containsKey / containsValue / keys / values / entries
  • 変換
    map / forEach / cast / Map.from / Map.fromEntries
  • 情報
    length / isEmpty / isNotEmpty
ちょいメモ:順序はどうなる?

Dart の Map(既定の実装 LinkedHashMap)は挿入順を保持します。keys / values / entries で辿る順序も挿入順です。


Set と Map の {} 衝突に注意(空セットの作り方)

// Map<dynamic, dynamic>(空の {} は Map)
var m1 = {};
// 空の Set<int>       
var s1 = <int>{};
// これはエラー({} は Map)→ こう書く: Set<String> s2 = <String>{};      
Set<String> s2 = {};

Iterable という考え方(List, Set, Map)

ListSet はどちらも Iterable<T> を実装しています。(この実装の概念については後ほど。今はListとSetはIterable<T>というクラスでもあると認識していただく程度でok)
where / map / expand / take / skip などは 遅延評価(必要になるまで実行されない)です。

final list = [1, 2, 3, 4, 5];
// ここではまだ実行されない(遅延)
final iter = list.where((x) => x.isOdd).map((x) => x * 10);

// 実際に使うときに実行される
print(iter.toList()); // [10, 30, 50]
Map でも Iterable

map.keysmap.values はそれぞれ Iterable<K> / Iterable<V> です。
map.entriesIterable<MapEntry<K, V>>

その他具体例

全部 遅延評価(必要になるまで実行されない) を体感できるサンプルです。


例1:map / where は「使う時」にだけ動く

final list = [1, 2, 3, 4, 5];

final iter = list
    .where((x) {
      print('where: $x');       // フィルタ判定が呼ばれたら表示
      return x.isOdd;           // 奇数だけ残す
    })
    .map((x) {
      print('map: $x');         // 変換が呼ばれたら表示
      return x * 10;
    });

// ここではまだ何も表示されない(遅延)
// 実際に取り出すときに初めて実行される
print(iter.toList());
// 出力例:
// where: 1
// map: 1
// where: 2
// where: 3
// map: 3
// where: 4
// where: 5
// map: 5
// [10, 30, 50]

例2:take(n) で「必要な分だけ」計算される(最小仕事)

final list = [1, 2, 3, 4, 5];

final iter = list
    .where((x) {
      print('where: $x');
      return x.isOdd;           // 奇数だけ
    })
    .map((x) {
      print('map: $x');
      return x * 10;
    })
    .take(2);                   // 2個だけ欲しい

print(iter.toList());
// 実行ログに注目:必要な分(奇数2個)に到達した時点で止まる
// where: 1
// map: 1
// where: 2
// where: 3
// map: 3
// [10, 30]

例3:any / firstWhere は短絡評価(見つかったら即終了)

final list = [1, 2, 3, 4, 5];

final hasOverThree = list.any((x) {
  print('check any: $x');
  return x > 3;
});
print(hasOverThree); // true
// ログ:
// check any: 1
// check any: 2
// check any: 3
// check any: 4   ← ここで true、以降は見ない

final firstEvenOverTwo = list.firstWhere((x) {
  print('check firstWhere: $x');
  return x.isEven && x > 2;
});
print(firstEvenOverTwo); // 4
// ログ:1,2,3,4 で停止

例4:reversed も遅延評価(必要な分だけ逆から見る)

final list = [1, 2, 3, 4, 5];

// 末尾から3つだけ欲しい → 全部を逆順リストにコピーしない
final lastThree = list.reversed.take(3).toList();
print(lastThree); // [5, 4, 3]

例5:expand でフラット化(ネストを平らに)

final words = ['ab', 'cd'];

// 各文字へ分解して1本の列に(['a','b','c','d'])
final chars = words.expand((w) => w.split(''));
print(chars.toList()); // [a, b, c, d]

// 1→[1,-1], 2→[2,-2] のように要素を増やして平らに
final doubledSigned = [1, 2, 3].expand((x) => [x, -x]);
print(doubledSigned.toList()); // [1, -1, 2, -2, 3, -3]

例6:巨大(や無限風)な列でも take で安全に一部だけ

// 0,1,2,3,... を生成(理論上は無限に近い列だと思ってOK)
// これは 00...01を30ビットだけ左にシフトさせる意味。1 * 2^{31}
final naturals = Iterable<int>.generate(1 << 30, (i) => i);

// 最初の5つだけ欲しい → 余計な分は計算されない
print(naturals.take(5).toList()); // [0, 1, 2, 3, 4]

例7:Iterable は配列インデックスがない(必要なら elementAt

final iter = [1, 2, 3].where((x) => x.isOdd); // Iterable<int>

// iter[0]; // これは不可(List じゃない)
print(iter.elementAt(0)); // 1(要素を取り出す関数を使う)

例8:同じ遅延列を「何度も」使うならキャッシュ化(toList() / toSet()

final lazy = [1, 2, 3, 4, 5].where((x) {
  print('filter: $x');
  return x.isOdd;
});

// 2回使うと2回とも評価される(毎回ログが出る)
print(lazy.toList()); // 1回目の評価
print(lazy.toList()); // 2回目の評価

// 1回だけ評価して使い回したいならキャッシュ
final cached = lazy.toList();   // ここで1回だけ実行
print(cached);                  // 2回目以降は評価なし

まとめ(超要点)

  • where / map / expand / take / skip / reversed / any / firstWhere などは 遅延評価
  • 必要な分だけ 計算され、見つかったら止まる(短絡評価)ものもある。
  • 同じ処理結果を何度も使うなら、一度 toList() / toSet()実体化して使い回すのが効率的。

この辺を理解しておくと、「最小の計算で欲しい結果だけ取る」書き方ができて、パフォーマンスやメモリにも優しい実装が可能となります。


コレクション if / for(コレクション内制御フロー)

リテラル内で iffor を使って要素を条件追加・生成できます。

final isPremium = true;
final menu = [
  'home',
  'search',
  if (isPremium) 'premium', // 条件付き
];

final squares = [
  for (var i = 0; i < 5; i++) i * i,  // 0,1,4,9,16
];

final mixed = {
  for (final c in ['a', 'b', 'c']) c: c.codeUnitAt(0), // Map を生成
};

スプレッド演算子(... / ...?

複数のコレクションを展開して結合できます。...?null を無視して展開します。

final base = [1, 2, 3];
final more = [...base, 4, 5];         // [1,2,3,4,5]

List<int>? maybe;                      // いまは null
final safe = [0, ...?maybe, 99];       // [0, 99]

final a = {'x': 1};
final b = {'y': 2};
final merged = {...a, ...b};           // {'x':1, 'y':2}

final s1 = {1, 2};
final s2 = {2, 3};
final s3 = {...s1, ...s2};             // {1,2,3}

読み取り専用/定数コレクションを作る

ミスで変更されたくないときに便利です。

final roList = List.unmodifiable([1, 2, 3]);   // 実行時に不変
final roSet  = Set.unmodifiable({1, 2, 3});
final roMap  = Map.unmodifiable({'a': 1});

// コンパイル時定数(完全に不変)
const nums = [1, 2, 3];
const words = {'a', 'b', 'c'};
const dict = {'x': 1, 'y': 2};

List/Set/Map の便利コンストラクタ

final zeros = List.filled(5, 0);          // [0,0,0,0,0]
final idx    = List.generate(5, (i) => i); // [0,1,2,3,4]

final copied = List.from([1, 2, 3]);
final sFrom  = Set.from([1, 2, 2, 3]);     // {1,2,3}
final mFrom  = Map.from({'a': 1});

重複・等価性(Set/Map のキー)

Mapではキーに、Setでは要素に、重複を持つことができない

  • Set重複を持てません。重複判定には ==hashCode が使われます。
  • Mapキー も同様に ==hashCode に基づきます。
    → 自作クラスをキーや Set 要素にするなら operator ==hashCode を実装しましょう。
class Point {
  final int x, y;
  const Point(this.x, this.y);

  
  bool operator ==(Object other) =>
      other is Point && x == other.x && y == other.y;

  
  int get hashCode => Object.hash(x, y);
}

よくある落とし穴(覚えておくとハマりにくい)

  • {} は Map。空の Set は <T>{} で作る。
  • list.reversedIterableList が欲しければ toList()
  • sort()元の List を破壊的に変更します(新しい並びの List が欲しいなら ...list でコピーしてから sort() するか、List.of(list)..sort())。
  • map() / where()遅延評価。今すぐ結果が欲しければ toList() / toSet()
  • Map.map()MapEntry を返す関数を取り、新しい Map を返します(Iterable.map とはシグネチャが違うので注意)。

便利スニペット集

// 1) 頻度カウント(文字数など)
String s = 'banana';
final freq = <String, int>{};
for (final ch in s.split('')) {
  freq.update(ch, (v) => v + 1, ifAbsent: () => 1);
}

// 2) グルーピング
final users = [
  {'name': 'A', 'team': 'red'},
  {'name': 'B', 'team': 'blue'},
  {'name': 'C', 'team': 'red'},
];
final byTeam = <String, List<Map<String, String>>>{};
for (final u in users) {
  byTeam.putIfAbsent(u['team']!, () => []).add(u);
}

// 3) List を Map に(インデックス → 値)
final list = ['a', 'b', 'c'];
final indexMap = {
  for (var i = 0; i < list.length; i++) i: list[i],
};

// 4) Map の値だけ加工
final priced = {'apple': 100, 'banana': 80};
final taxed = priced.map((k, v) => MapEntry(k, (v * 1.1).round()));

まとめ

個人的にはこれらは全て暗記する必要なないと思います。ただ、Collectionのオブジェクトに対して何かしらの操作を施する際に、こんなのがあったな〜程度でいいと思います。具体的な使用方法などはその都度調べていくのが良いです。

関数型はジェネリックという概念と深く関わってくるため後ほど説明します。

  • List / Set / Map は Dart のコレクション三本柱。
  • collection if/forスプレッド(... / ...?)const / unmodifiable を押さえると表現力が一気に上がります。
  • 空の {}Map、空の Set は <T>{}——まずはここを間違えないこと。

Discussion