[Flutter] Dartで学ぶイテレータの基礎と、基本的な使い方
Dartのイテレータに関して基礎的な部分と使い方をまとめてみた。
対象読者は、「Dartを少し使えるけどイテレータは知らなかったりよく分からなかったりする」というくらいの人。ここで言う「少し使える」は「チョットデキル」の意味ではない。
使用したDartのバージョンは2.12.2
。
イテレータって、なに?
Wikipediaのイテレータのページには、以下のように書かれている。
イテレータ(英語: iterator)とは、プログラミング言語において配列やそれに類似する集合的データ構造(コレクションあるいはコンテナ)の各要素に対する繰り返し処理の抽象化である。実際のプログラミング言語では、オブジェクトまたは文法などとして現れる。JISでは反復子(はんぷくし)と翻訳されている[1][2]。
辞書的な書き方で、正確な記述ではあるけど理解しにくいので、噛み砕いてみるとこんな感じ。
- 複数のデータをまとめる構造を「コレクション」と呼ぶ。
- Dartで言えばList、Map、Setといったもの。
- コレクションの中身のことを「要素」という。
- 例えばListに入れた1個1個の値が「要素」。
-
コレクション内の要素に対する繰り返し(反復)処理を簡単に書けるようにしたのが「イテレータ」。
- つまりListの中身1個ずつに対して何らかの処理をする、という感じのコードを簡単に書けるようにしている。
- Mapもコレクションではあるけど、DartではMapは反復できない扱いとなっている。(Map自体はイテレータを持たない)
Dartのコードを書いていると、実はあまり意識しないうちにイテレータを使っていたりする。
例えば、Listの要素を繰り返してprint
したいとき、Dartでは以下のような書き方が代表的だと言われている。
final list = [0, 1, 2];
// for
for (var i = 0; i < list.length; i++) {
print(list[i]);
}
// forEach その1。普通の関数タイプ。
list.forEach((v) {
print(v);
});
// forEach その2。ファットアローを使った1行関数タイプ。
list.forEach((v) => print(v));
// for-in
for (var v in list) {
print(v);
}
この内、実はforEach
はイテレータで、List
の親クラスであるIterable
のメソッドになっている。
詳しくは後述するけど、このようにループ的な場面で使うのがイテレータ、と思って差し支えはないと思う。
イテレータってどこが便利なの?
コードを書いていると、コレクションの要素を順々取り出して処理する、というのはとてもありふれた操作。これは上記のように、for-in
を使ったり、あるいは書くのが面倒な昔ながらのfor
を使って簡単に対応できる。
でもそこで単なる繰り返しではなく、一定の範囲のものだけを取得したり、何かの条件を満たすものだけを抽出したり、全部の値を累計したり、といったこともやりたくなる。
こんなとき、もちろんループで中身を1個1個見ていくコードを書いてやってもいいんだけど、書いたコードにミスが混入する恐れがあるし、何よりそんなよくある処理を毎度書くのは面倒くさい。
ということで、コレクションに対するよくある処理をすっきり書けるように抽象化する、というのがイテレータ。
ここでいう「抽象化」は、具体的な処理を隠蔽(使う人からは見えないように)した、という意味。つまりあらかじめ用意されているメソッドを使えば、複雑なコードを書かなくてもその機能を利用できる。
この抽象化されたメソッドは何種類もあるけど、それぞれは単純な機能しか持たない。でも複数のメソッドを組み合わせることで、複雑な機能を分かりやすく楽に実現できるのでとても便利。
以降、どんなものかを見ていく。
Iterableクラス
Dartのイテレータの基礎になっているのがIterable
クラス。
Iterable
クラスは抽象クラスで、イテレータとしての基本的なメソッドが定義されている。
これを継承したサブクラスとして具体的なコレクションのクラス(List
やSet
等、dart:collection
にある多くのクラス)が定義されて、それらの具体的なクラスでは、それぞれのデータ構造に適したイテレータ関連のメソッドが追加されている。
ということで、Iterable
クラスを見てみれば、様々なコレクションに共通するイテレータの基本的な部分を理解できる。
イテレータの具体的な使い方
これからIterable
クラスで定義されているイテレータの具体的な使い方を見ていくけど、Iterable
は抽象クラスでインスタンスを作れないので、実際に動かすためにサブクラスの1つであるList
を使ったコードで見ていく。
forEach
要素を繰り返す要素を1個ずつ順々に取り出して、渡された関数で処理していくという、単純なループ処理を行うメソッドがforEach
。
final list = [0, 1, 2, 3, 4];
list.forEach((v) {
print(v);
});
内部的にはfor-in
をメソッドとしてラップしているだけだったりするので、単なるループ処理を行うもの。
これ、イテレータの中で一番多く使われているような気がするけど、結構特殊なものなので注意が必要。
どういうことかというと、値を返さない戻り値がvoid
というメソッドは、Iterable
クラスの中にこれしかない。だからこれに囚われて「イテレータってこんなものなんだ」と思ってしまうと他のイテレータを理解しにくくなってしまうという困ったやつ。
あとそもそも、「メソッド(関数)の引数として関数を渡す」というのは高度で難しい概念だと思う。現代だとJavaScriptで頻繁に使われている手法なので一般に浸透している感じがあるけど、それ以前の太古の時代には実装の面倒さもあって限られたシチュエーションでしか使われていなかったテクニック。最近の人はあっさりと理解しているのだろうか?
ということで個人的には、単なるループのときはfor-in
を使った方が文脈的に正しいし楽で見やすいので良いと思っている。
この記事の後の部分で書いているような、イテレータで色々した後の中身について最終的に何かをしたいときだけforEach
を使うのがいいんじゃないかな。
要素の数を調べる
まずは小手調べということで、イテレータの補助的な働きをするものから。
要素の数や、中身が空かどうかを調べられる。メソッドではなくプロパティ。
final list = [0, 1, 2, 3, 4];
final emptyList = [];
print(list.length); // -> 5
print(emptyList.length); // -> 0
print(list.isEmpty); // -> false
print(emptyList.isEmpty); // -> true
print(list.isNotEmpty); // -> true
print(emptyList.isNotEmpty); // -> false
最初や最後の要素を取り出す
最初や最後、あるいは1個だけを取り出すためのプロパティがある。
空のリストに対して呼び出して、該当する要素がなかったら例外をスローしてくるので、例外をキャッチするか、あらかじめ数を調べておいて範囲をはみ出さないようにする必要がある。
こういう取得系の動作で例外をスローするのは大仰で、「該当する要素がないときはnull
を返せば良いのでは?」という気がするけど、それだと要素としてnull
を持っているときに判別できなくて困るので、こうした作りになっている。null
がある言語の宿命と言える。
final list = [0, 1, 2, 3, 4];
final emptyList = [];
final singleList = ['only'];
print(list.first); // -> 0
print(list.last); // -> 4
// print(emptyList.first); // 例外発生!
// print(emptyList.last); // 例外発生!
print(singleList.single); // -> only
// print(list.single); // 例外発生!
// print(emptyList.single); // 例外発生!
しかしsingle
の使い道が謎。「要素が1個だけのときに中身を取り出す」というもので、要素が空だったり2個以上だったりすると例外を投げてくる。何に使うんだこれ。
contains
指定した引数を要素に含むかどうかを調べるここからはメソッドの紹介。
contains
では、引数と同じ要素がコレクションに含まれているかどうかをbool
で返す。
「同じ」というのは「等しい」という意味で、デフォルトのcontains
では内部で==
を使って比較しているので、それぞれのクラスの==
の実装にもよるけど、基本的には「同じオブジェクト」ではなく「同じ値を持っている」かどうかを見ている。
final list = [0, 1, 2, 3, 4];
final emptyList = [];
print(list.contains(1)); // -> true
print(list.contains(99)); // -> false
print(list.contains('1')); // -> false (型が異なるので==がfalse)
print(emptyList.contains(2)); // -> false
あと、サブクラスでは==
と異なる方法で比較していることがあり得る。基本的にはあまり気にしなくて良い部分ではあるけど。例えばハッシュを使ったマップのキーの比較を行うような場合には影響があることもある、とドキュメントには書いてある。
any
とevery
要素が条件を満たすかを調べるイテレータに含まれる要素が、引数として渡された関数の条件を満たすかどうかを調べる。最終的に結果としてbool
が返ってくる。
引数で渡す関数は、要素を1つ取ってbool
を返すもの。
any
とevery
は、以下のような違いがある。
-
any
:要素のどれか1つでも条件を満たしていれば、最終的な結果がtrue
になる。 -
every
:すべての要素が条件を満たしていれば、最終的な結果がtrue
になる。(関数が1個でもfalse
を返すと、最終的な結果がfalse
になる)
final list = [0, 1, 2, 3, 4];
print(list.any((v) => v < 99)); // -> true
print(list.any((v) => v < 3)); // -> true (3より小さい要素が含まれるため)
print(list.every((v) => v < 99)); // -> true
print(list.every((v) => v < 3)); // -> false(3より小さく*ない*要素が含まれるため)
個人的には、ファットアロー=>
と比較演算子<=``>=
なんかが絡むと見にくいので、普通の関数で書きたくなるところ。
elementAt
指定したインデックスの要素を取得する指定した「n番目」の要素を返す単純なやつ。
int
の引数で指定した順番のものを返すけど、引数が負の整数(要するにマイナスの数値)だったり、イテレータの長さ(length
)より大きかったりすると、例外を投げてくる。
final list = ['Zero', 'One', 'Two', 'Three', 'Four'];
print(list.elementAt(2)); // -> Two
// print(list.elementAt(-1)); // 例外発生!
// print(list.elementAt(99)); // 例外発生!
map
要素を元にして何らかの処理を加え、新しいイテレータを作るここからがいよいよイテレータの本番。
map
の基本
要素のそれぞれに対して、渡した関数の処理を加えて、新しいイテレータを生成するのがmap
。言葉で書くとややこしいけど、コードで見ると単純。
final list = [0, 1, 2];
final result = list.map((e) => e + 2);
print(result.toList()); // -> [2, 3, 4]
このコードでは、元々のリスト[0, 1, 2]
の各要素に対して「2を加える」という処理を行って、新しいイテレータを生成して、それを出力している。
最後にあるtoList()
は、Iterable
をList
へ変換するメソッド。これでIterable<int>
だったresult
がList<int>
となる。実は、これがなくてもprint
するだけなら問題ないけど、一応付けている。
ということで、map
に対して「1個の引数を取って何らかの値を返す関数」を渡せば、関数の結果から成る新しいイテレータが生成されることが分かる。
これは、元々の要素と別の型を返しても良いことでもある。例えば、加工を加えつつ文字列に変換するようなことができる。
final list = [0, 1, 2];
final result = list.map((e) {
final tmp = e + 5;
return 'map $e to $tmp';
});
print(result.toList()); // -> [map 0 to 5, map 1 to 6, map 2 to 7]
すごく便利に使えるのが想像できると思う。夢が広がりんぐ(死語)。
map
でループが書けそうな気がするけど
map
の動きについてもっと考えてみると、単純なループですらmap
で書けそうなことに気付く。だってforEach
と同じようなことをしているんだし。
例えば以下のようなコードを書けば、リストの値をprint
できそうな気がする。
final list = [0, 1, 2];
list.map((e) {
print(e);
});
でもこれを実行してみれば分かるけど、何も出力されない。
なぜならmap
は「lazy」な動きをするから。lazyは直訳すると「怠惰」「動きがのろい」みたいな意味だけど、それではイメージが悪いので日本の業界では格好良く「遅延評価」と呼ぶ。遅延しているのに前向きな雰囲気がある。
lazyといえば
アニヲタ的には「lazy」と聞くと『けいおん!』の「Don't say "lazy"」が第一候補として思い浮かぶのではなかろうか。
澪が歌っているけど一番の怠け者は唯なのでは、とか思ったりする。
このlazyというのは、必要なときにならないと呼び出されない、という意味。イテレータを実行する必要があるときというのは、最終的な結果を得るとき。
すなわち、例えば以下のようにtoList()
ですべての結果を得るように書けば、そのタイミングで出力される。
final list = [0, 1, 2];
final lazy = list.map((e) {
print('map $e');
});
print('Are you lazy?');
lazy.toList(); // ここでmapが実行される。リストへの変換結果は捨てている。
// Are you lazy?
// map 0
// map 1
// map 2
「Are you lazy?」の後で出力されていることから、toList()
の時点でmap
の処理が実際に走っていることが分かる。
もうちょっと言えば、変数lazy
はIterable<Null>
型なんだけど、それがList<Null>
型という別の型に変換されるタイミングで呼び出されている。
イテレータのメソッドをつなげる
また、重要な考え方として、map
がイテレータIterable
を返すので、さらにmap
を重ねて呼び出せる、ということがある。これはmap
に限らず、イテレータを返すメソッドは他にもあり、これらを組み合わせることで、様々な処理を行える。
ひとまずmap
だけを使って書いてみると、こんな感じになる。
final list = [0, 1, 2];
final lazy = list.map((e) {
print('source: $e');
return e;
}).map((e) {
final tmp = e + 2;
print('plus two: $tmp');
return tmp;
}).map((e) {
final tmp = e * 2;
print('and double: $tmp');
return tmp;
});
lazy.toList();
// source: 0
// plus two: 2
// and double: 4
// source: 1
// plus two: 3
// and double: 6
// source: 2
// plus two: 4
// and double: 8
まずは1段目のmap
で元の値をprint
してから返し、2段目のmap
で2を足しつつprint
してから返し、3段目のmap
で2倍しつつprint
している。
特に意味がない例で、map
だけでは良さが分かりづらいものの、後述する色々なメソッドを組み合わせればとても便利になる。
ちなみにこの場合、出力された順序を見てもらえば分かるけど、「最初の要素がメソッドをずっと流れていって、それが終わったら次の要素がメソッドを流れていって……」という流れになっているのが分かる。ここを「1段ずつ全部処理される」と勘違いしていると、関数の呼び出し順が違っていて副作用でハマったりするので注意。
where
とその仲間
条件を満たすものだけを取得するwhere
は、メソッドに引数で渡された「要素を引数にとる関数」がtrue
を返したものだけで、新しいイテレータを作るメソッド。動作は遅延評価。
これは要するに条件で要素の絞り込みを行うもので、分かりやすいしとても有用。
final list = [0, 1, 2, 3, 4];
// 偶数だけを取得する。
final result1 = list.where((e) => e % 2 == 0);
print(result1.toList()); // -> [0, 2, 4]
// 奇数を取得して2倍する。
final result2 = list.where((e) => e % 2 == 1).map((e) => e * 2);
print(result2.toList()); // -> [2, 6]
この仲間としてfirstWhere
とlastWhere
がある。名前の通り、where
と同様の絞り込みをした結果の中から最初または最後の要素(イテレータではない)を返すもの。
該当する要素がなかったときの動作として、あらかじめ引数で渡した関数の戻り値を返すか、あるいは例外を投げるかを選択できる。
final list = [0, 1, 2, 3, 4];
// 要素を順番に見ていって、初めて条件を満たした値を取得する。
print(list.firstWhere((e) => 10 <= e * 5)); // -> 2
// 要素を順番に見ていって、最後に条件を満たした値を取得する。
print(list.lastWhere((e) => e * 3 < 10)); // -> 3
// 条件に当てはまる要素がなかったとき、orElseに何も指定していないと例外を投げる。
// print(list.firstWhere((e) => 99 <= e)); // 例外発生!
// 条件に当てはまる要素がなかったときに返す値を「返す関数」をorElseで設定する。
print(list.firstWhere((e) => 99 <= e, orElse: () => -1)); // -> -1
あとsingleWhere
という、「条件に当てはまるものが1個のときだけその要素を返し、それ以外の場合は指定した関数の戻り値を返すか例外を投げるか」という使いどころの難しいメソッドもある。
skip
と、いくつか取得するtake
と、その仲間
いくつか読み飛ばすskip
は指定した数だけ要素を読み飛ばして、残りの要素でイテレータを生成する。遅延評価。
要素の最初の何個かは不要なのでスキップしたい、というときに使用できる。
take
は指定した数だけの要素でイテレータを生成する。遅延評価。
どちらも負の値を指定すると例外を投げてくるのはelementAt
と同様。
ただ、要素数をはみ出すような大きい値を指定してもどちらも例外にはならない。skip
は空のイテレータを返すだけで、take
は取得できた分だけの要素のイテレータを返す。
これらを組み合わせると、「何番目から何個の要素が欲しい」という要望に対応できる。(実はList
ではgetRange
メソッドで同じことができるので、こういう使い方はしないと思うけど)
final list = [0, 1, 2, 3, 4];
print(list.skip(2).toList()); // -> [2, 3, 4]
print(list.skip(99).toList()); // -> []
print(list.take(2).toList()); // -> [0, 1]
print(list.take(99).toList()); // -> [0, 1, 2, 3, 4]
// 最初の2つを読み飛ばして、そこから2つを取得する。
print(list.skip(2).take(2).toList()); // -> [2, 3]
似たものとして、skipWhile
とtakeWhile
がある。
これは引数で渡された「要素を元にしてbool
を返す関数」がtrue
の間はskip
またはtake
するもの。
final list = [0, 1, 2, 3, 4];
print(list.skipWhile((v) => v != 2).toList()); // -> [2, 3, 4]
print(list.skipWhile((v) => v < 99).toList()); // -> []
print(list.takeWhile((v) => v < 2).toList()); // -> [0, 1]
print(list.takeWhile((v) => v < -1).toList()); // -> []
reduce
とfold
とjoin
要素をまとめる要素を集計したりしてまとめるというのはよくある処理。例えば整数のリストを合計したりとか、文字列としてまとめたいとか。そんなときに便利なメソッドがいくつかある。
reduce
reduce
は2つの引数をとる関数を使って、要素を最終的に1つの値にまとめるメソッド。
言葉で説明すると長ったらしくなるので、コードで見るのが簡単。ということで、リストの合計を求めるコードがこれ。
final list = [0, 1, 2, 3, 4];
print(list.reduce((value, element) => value + element)); // -> 10
// 処理の途中でprintしてみる。
list.reduce((value, element) {
print('$value + $element');
return value + element;
});
// 0 + 1
// 1 + 2
// 3 + 3
// 6 + 4
途中の変数状態を表示している下側の例を見れば分かる通り、関数に対して以下のような値が渡されている。
- 1回目:1つ目と2つ目の要素
- 2回目:1回目の結果と2つ目の要素
- 3回目:2回目の結果と3つ目の要素
- 4回目:3回目の結果と4つ目の要素
関数内で、渡された2つの値を足し合わせているので、最終的には合計された値を得られる。
具体的に何をしているかは、公式のドキュメントに書かれた中身のこのコードを見るのが分かりやすいかも。
E value = iterable.first;
iterable.skip(1).forEach((element) {
value = combine(value, element);
});
return value;
ここでcombine
はreduce
に渡された関数。
処理として、まず最初の要素をvalue
変数に取り出す。それから2個目以降の要素を順番に取り出して、combine
に対して前回の結果value
と取り出した要素element
を渡して、combine
の結果をvalue
に格納する、という処理を繰り返している。
そしてfirst
を使っているので、上の方で書いたとおり、要素が1個もなかったら例外を投げてくることが分かる。あとコレクションの要素が1個だけだったらforEach
のループに入らず(skip
が空のイテレータを返すため)、その値が返されることも分かる。
ちなみに、こうして累計を求める際の、値が累積されていく変数(上記の例だとvalue
)をアキュムレータ(Accumulator)と呼ぶことがあって、意味を明確にするために変数名をacc
にするようなことがあるので、覚えていたら便利かも。
アキュムレータ……インキュベータ……
個人的にはアキュムレータと聞くと、魔法少女に勧誘する白い悪魔を思い出してしまう。あっちはインキュベータで全然違うんだけど、「キュ」を含む語感が似ているので、つい。
fold
fold
は初期値と2つの引数をとる関数を使って、要素を最終的に1つの値にまとめるメソッド。
言葉だけを見るとreduce
と初期値があるかないかだけの違いしかないけど、実際はかなり異なる。
例えば上記のreduce
と同じようにリストの整数をすべて足し合わせた値を得る場合、以下のようなコードとなる。
final list = [0, 1, 2, 3, 4];
// この書き方だと、型推論でprevが Object? 型となってしまい、コンパイルエラーとなる。
// final result1 = list.fold(0, (prev, element) => prev + element);
// そのため、以下のような解決策をとる必要がある。
// 解決策その1:結果を格納する変数に型を付ける。(int result2)
int result2 = list.fold(0, (prev, element) => prev + element);
print(result2); // -> 10
// 解決策その2:foldへ渡す関数の引数に型を指定する。(int prev)
final result3 = list.fold(0, (int prev, element) => prev + element);
print(result3); // -> 10
// 解決策その3:foldに対してジェネリクスの型Tを指定する。これなら直接printできる。
print(list.fold<int>(0, (prev, element) => prev + element)); // -> 10
コードのコメントとしても書いているけど、型推論が上手く働かないことがある。
これは渡した関数の中の処理にもよるけど、型推論できずに変数をObject?型として扱ってしまい、関数内の処理を解釈できず(上記コードの場合はObject?型に+
がないため)、コンパイルエラーとなることがある。
そのため、何らかの方法で型を明確に指定してやる必要がある。
これだけを見るとfold
はreduce
に比べると型の指定が必要で面倒くさいだけなように思えるけど、実は、元の要素と異なる型へまとめられるという大きなメリットがある。
これは、reduce
とfold
のメソッド定義を見比べてみると分かる。
reduce(E combine(E value, E element)) → E
fold<T>(T initialValue, T combine(T previousValue, E element)) → T
ここで、E
は要素の型で、T
はそれとは異なる型を指し示している。
つまりreduce
は、要素と同じ型しか返せない。
対してfold
は、要素とは別の型T
の初期値と、T
とE
の引数からT
を返す関数を使って、最終的にT
を返せることになる。(もっと型に注目すれば、T
と書いてあるどこかの部分で型を指定すれば、型推論で型が明確になってコンパイルエラーを回避できる、というのが上記の解決策)
これを活用すると色々便利で、例えば以下のコードのようなことができる。
final list = [0, 1, 2, 3, 4];
// 文字列として連結する。あまり意味がないし、初回のループの影響で汚い結果になる。
String s = list.fold('', (String prev, int element) => '$prev, $element');
print(s); // -> , 0, 1, 2, 3, 4
// doubleのリストで、各要素を四捨五入して累計した結果をintで得る。
final list2 = [0.2, 0.5, 1.4, 2.2];
int value = list2.fold(0, (a, b) => a + b.round());
print(value); // -> 4
// reduceを使って同じことはできない。要素の型と最終的な型が異なるため。
// int value2 = list2.reduce((a, b) => a + b.round()); // コンパイルエラー!
// 処理の途中でprintしてみる。
list.fold(0, (int value, element) {
print('$value + $element');
return value + element;
});
// 0 + 0
// 0 + 1
// 1 + 2
// 3 + 3
// 6 + 4
最後にfold
の途中経過を出しているけど、reduce
に比べて、最初に「初期値と1個目の要素」の処理が走っていることが分かる。だからfold
は要素の数と同じ回数だけ関数が呼び出されるけど、reduce
は要素の数より1回だけ少ない。
join
要素を、渡された文字列で連結した文字列として返すのがjoin
。語るべきことがないくらい単純。
それぞれの要素をtoString()
した結果を連結して返す。
final list = [0, 1, 2, 3, 4];
print(list.join(', ')); // -> 0, 1, 2, 3, 4
print(list.join()); // -> 01234
followedBy
イテレータ同士を連結するイテレータ同士を連結したイテレータを返すのがfollowedBy
。遅延評価される。
final list1 = [0, 1, 2];
final list2 = [7, 8, 9];
final list3 = ['a', 'b', 'c'];
print(list1.followedBy(list2).toList()); // -> [0, 1, 2, 7, 8, 9]
// print(list1.followedBy(list3).toList()); // コンパイルエラー!
take
やskip
なんかと組み合わせると、好きなようにイテレータを作れる。
要素の型が異なるイテレータ同士を連結しようとするとコンパイルエラーになる。
おまけ
Map
をイテレータのように扱う
上の方でちらっと書いたけど、コレクションの代表的なものであるMap
は、Dartではイテレータを持たない(Iterable
クラスのサブクラスではない)扱いなので、そのままではここまで書いてきたような便利なメソッドを使えないことになる。
でもそこは、Map
のキーの一覧をList
で取得できる、このList
がIterable
であることを活用してイテレータを使うという方法がある。
例えばこんな感じ。
final m = {0: 'Zero', 1: 'One', 2: 'Two', 3: 'Three'};
// マップの値が3文字のものを抽出する。
print(m.keys.where((k) => m[k]!.length == 3).map((k) => m[k])); // -> (One, Two)
マップm
のキー(0~3)を取得して、where
でm
の値が3文字のキーを抽出し、map
でそのキーを元にして最終的なマップの値のイテレータを返している。
それぞれのメソッドの動作を理解すれば、組み合わせていろいろな処理を書ける。
null
が返ってくるときの捌き方
本筋ではないけど、すぐ上のマップの値が3文字のものを探す例では、where
の中でマップの要素を参照するときに返ってくる型がString?
なのでnull
が返ってくる可能性があり、そのままではlength
を呼び出せないので、!
を付けて強制的にString
へキャストしている。
存在するキーだけでアクセスしているので、m[k]
がnull
を返してくることはないと言えるので、この書き方でも問題はない。もちろん実行時のエラーを避けるために真面目にチェックするような処理もこんな感じで書ける。
final m = {0: 'Zero', 1: 'One', 2: 'Two', 3: 'Three'};
// m[k]がnullを返したときはwhereの中でfalseを返すようにnullチェックする。
print(m.keys.where((k) {
final v = m[k];
if (v == null) {
return false;
}
return v.length == 3;
}).map((k) => m[k]));
// 先にマップの値のイテレータにしてしまい、whereTypeでStringのみに絞り込む。
print(m.keys.map((k) => m[k]).whereType<String>().where((v) => v.length == 3));
後者ではwhereType<T>()
メソッドが出てきたけど、これはT
として渡された型に一致する要素だけのイテレータを返すもの。
map
はm[k]
を返すのでString?
型のイテレータ(Iterable<String?>
)で、whereType<String>()
によりString
型のみに絞り込んだイテレータを得れば、結果としてnull
の値がないIterable<String>
になる。
あと、イテレータの結果をprint
するとき、ここまでのコードではわざわざtoList
していたけど、Iterable
をそのままprint
して裏でtoString
を呼び出してもらう、というやり方もある。
これでもprint
するだけなら全然問題なくて、少し見た目が変わったり、途中が省略されたり、という違いがある程度。でも、イテレータの結果を戻り値として返すようなときは型の違いに注意が必要。
最後に
思うがままに書いていたら長くなりすぎてしまった。
Iterable
クラスのメソッドの動きを理解できれば、そのサブクラスである具体的なコレクションクラスの動作も理解できると思う。
この記事でイテレータの便利さが伝われば嬉しいところ。for
で要素をいじくり回すより、イテレータを使って見やすいコードを楽に書けばみんなハッピー!
Discussion