この記事は 2026 年 8 月 22 日に投稿しました。

目次
1. はじめに
こんにちは、iOS のエディタアプリPWEditorとその後継PWEditorNextの開発者の二俣です。
2018年5月から毎日技術ブログを更新しています。
今回はDartのHashSetの各要素を0個以上の要素に展開し、それらを1つのIterableとして取得できるexpandメソッドについてです。
2. DartのHashSetの各要素を複数の要素に展開する
DartのHashSetでは、expandメソッドを使用すると、各要素から0個以上の要素を生成し、それらを1つにつないだIterableを取得できます。
expandメソッドの形式は以下になります。
Iterable<T> expand<T>( Iterable<T> toElements(E element), )
引数には、HashSetの各要素を受け取り、展開後の要素を持つIterableを返す関数を指定します。
例えば、次のHashSetがあるとします。
final numbers = HashSet<int>(); numbers.addAll([ 1, 2, 3, ]);
それぞれの数値を、
元の値 元の値 × 10
という2つの値に展開する場合は、次のように記述できます。
final result = numbers.expand(
(number) => [
number,
number * 10,
],
);
print(result);
各要素について、
1 → 1, 10 2 → 2, 20 3 → 3, 30
のように展開され、それらが1つのIterableにつながります。
実装例
import 'dart:collection'; void main() { final numbers = HashSet<int>(); numbers.addAll([ 1, 2, 3, ]); final result = numbers.expand( (number) => [ number, number * 10, ], ); print(result); }
例えば結果は、
(1, 10, 2, 20, 3, 30)
のようになります。
ただし、HashSetでは要素の反復順序が保証されていません。
そのため、必ずこの順番になるとは限りません。
重要なのは、各要素について返されたIterableの要素が、順番につながった結果を取得できるという点です。
文字列を展開することもできます。
例えば、
import 'dart:collection'; void main() { final languages = HashSet<String>(); languages.addAll([ 'Dart', 'Swift', 'Kotlin', ]); final result = languages.expand( (language) => [ language, language.length, ], ); print(result); }
この例では、それぞれの文字列を、
文字列そのもの 文字列の長さ
という2つの要素に展開しています。
ただし、この場合は文字列と整数が混在するため、結果の型は共通して扱える型になります。
同じ型にそろえたい場合は、例えば、
final result = languages.expand<String>( (language) => [ language, '${language.length}', ], );
のようにできます。
この場合は、すべての要素がStringになります。
また、expandメソッドはHashSet固有のメソッドではありません。
HashSetはIterableとして扱えるため、Iterableで定義されているexpandメソッドを利用できます。
そのため、Listなどでも同じように使用できます。
final numbers = [ 1, 2, 3, ]; final result = numbers.expand( (number) => [ number, number * 10, ], ); print(result);
このように、expandメソッドは、
1つの要素から複数の要素を生成する 複数のIterableを1つにつなげる 入れ子になったデータを平坦化する
といった処理に利用できます。
3. 要素を0個・1個・複数個に展開する
expandメソッドでは、それぞれの要素から返す要素数を自由に変更できます。
必ず、
1つの要素 → 2つの要素
にする必要はありません。
各要素から、
0個 1個 複数個
の要素を返すことができます。
複数の要素に展開する
例えば、
import 'dart:collection'; void main() { final numbers = HashSet<int>(); numbers.addAll([ 1, 2, 3, ]); final result = numbers.expand( (number) => [ number, number * 2, number * 3, ], ); print(result); }
とすると、それぞれの値から3つの値を生成できます。
例えば、
1 → 1, 2, 3 2 → 2, 4, 6 3 → 3, 6, 9
のように展開されます。
1つの要素に展開する
返すIterableの要素を1つだけにすることもできます。
final result = numbers.expand(
(number) => [
number * 10,
],
);
この場合、
1 → 10 2 → 20 3 → 30
のような変換になります。
ただし、各要素を必ず1つの要素へ変換するだけであれば、通常はmapメソッドの方が分かりやすいでしょう。
final result = numbers.map(
(number) => number * 10,
);
0個の要素に展開する
空のIterableを返すことで、元の要素を結果に含めないようにできます。
例えば、偶数だけを残し、それぞれ2つの値に展開してみます。
import 'dart:collection'; void main() { final numbers = HashSet<int>(); numbers.addAll([ 1, 2, 3, 4, ]); final result = numbers.expand((number) { if (number.isOdd) { return <int>[]; } return [ number, number * 10, ]; }); print(result); }
奇数の場合は、
<int>[]
を返しているため、その要素からは何も生成されません。
偶数の場合は、
[ number, number * 10, ]
を返します。
したがって、
1 → なし 2 → 2, 20 3 → なし 4 → 4, 40
のような処理になります。
このようにexpandメソッドでは、条件によって生成する要素数を変更できます。
入れ子のIterableを平坦化する
expandメソッドの代表的な使い方の1つが、複数のIterableを1つにつなげる処理です。
例えば、HashSetに複数のList<int>を格納するとします。
final numbers = HashSet<List<int>>(); numbers.addAll([ [1, 2], [3, 4], [5, 6], ]);
この状態では、各要素はList<int>です。
[1, 2] [3, 4] [5, 6]
これを1つのIterableに展開する場合は、
final result = numbers.expand(
(list) => list,
);
print(result);
とできます。
結果は、
(1, 2, 3, 4, 5, 6)
のようになります。
つまり、
Iterable<Iterable<T>>
のような入れ子になったデータを、
Iterable<T>
として扱いたい場合にも便利です。
なお、元がHashSetであるため、各List<int>がどの順番で処理されるかは保証されません。
4. expandメソッドとmapメソッドの違い
expandメソッドと似た用途のメソッドとして、mapメソッドがあります。
どちらも、
元の要素を別の要素へ変換する
ために利用できますが、大きな違いがあります。
mapメソッドでは、基本的に、
1つの要素 → 1つの要素
へ変換します。
一方、expandメソッドでは、
1つの要素 → 0個以上の要素
へ展開できます。
例えば、
final numbers = HashSet<int>(); numbers.addAll([ 1, 2, 3, ]);
があるとします。
mapメソッドで各値を2倍にする場合は、
final result = numbers.map(
(number) => number * 2,
);
print(result);
とできます。
これは、
1 → 2 2 → 4 3 → 6
という1対1の変換です。
一方、expandメソッドでは、
final result = numbers.expand(
(number) => [
number,
number * 2,
],
);
print(result);
とすることで、
1 → 1, 2 2 → 2, 4 3 → 3, 6
のように、1つの要素から複数の要素を生成できます。
mapでListを返した場合
違いを分かりやすくするために、mapメソッドからListを返してみます。
final result = numbers.map(
(number) => [
number,
number * 10,
],
);
print(result);
この場合、結果は、
([1, 10], [2, 20], [3, 30])
のような、
Iterable<List<int>>
になります。
一方、expandメソッドでは、
final result = numbers.expand(
(number) => [
number,
number * 10,
],
);
print(result);
とすると、
(1, 10, 2, 20, 3, 30)
のような、
Iterable<int>
になります。
つまり、
map 各要素を別の1要素へ変換する expand 各要素を複数の要素へ展開し、それらを1つにつなげる
という違いがあります。
例えば、文字列を1文字ずつに展開する場合にもexpandメソッドを利用できます。
import 'dart:collection'; void main() { final words = HashSet<String>(); words.addAll([ 'Dart', 'App', ]); final result = words.expand( (word) => word.split(''), ); print(result); }
それぞれの文字列について、
Dart → D, a, r, t App → A, p, p
のように展開されます。
このように、
変換後の要素数が常に1つ
であればmap、
変換後の要素数が0個または複数になる
場合や、
変換後のIterableを1つにつなげたい
場合はexpandを使用すると分かりやすくなります。
5. expandメソッドを使用する場合の注意点
HashSetでexpandメソッドを使用する場合、いくつか覚えておきたい点があります。
戻り値はHashSetではなくIterable
expandメソッドの戻り値はHashSetではありません。
戻り値は、
Iterable<T>
です。
例えば、
final numbers = HashSet<int>(); numbers.addAll([ 1, 2, 3, ]); final result = numbers.expand( (number) => [ number, number * 10, ], );
のresultは、
Iterable<int>
になります。
展開結果をListとして利用したい場合は、
final result = numbers
.expand(
(number) => [
number,
number * 10,
],
)
.toList();
とできます。
Setにしたい場合は、
final result = numbers
.expand(
(number) => [
number,
number * 10,
],
)
.toSet();
とできます。
なお、Setに変換した場合は重複した要素が取り除かれます。
例えば、
final numbers = HashSet<int>(); numbers.addAll([ 1, 2, ]); final result = numbers .expand( (number) => [ number, number * 2, ], ) .toSet(); print(result);
では、
1 → 1, 2 2 → 2, 4
となり、2が重複します。
toSet()を使用すると重複が取り除かれるため、
{1, 2, 4}
のようになります。
expandメソッドの結果には重複を含められる
元のHashSetには同じ要素を複数格納できません。
しかし、expandメソッドが返すのはIterableなので、展開後の結果には同じ値が複数含まれる場合があります。
例えば、
import 'dart:collection'; void main() { final numbers = HashSet<int>(); numbers.addAll([ 1, 2, 3, ]); final result = numbers.expand( (number) => [ number, 0, ], ); print(result); }
各要素から必ず0も生成しているため、結果には0が複数回現れます。
つまり、
元がHashSetだから展開後も重複しない
わけではありません。
重複を除きたい場合は、
final result = numbers
.expand(
(number) => [
number,
0,
],
)
.toSet();
のようにできます。
遅延評価される
expandメソッドが返すIterableは遅延評価されます。
つまり、
final result = numbers.expand((number) { print('expand: $number'); return [ number, number * 10, ]; });
と記述した時点では、必ずしも各要素に対する展開処理が実行されるわけではありません。
例えば、
print(result);
や、
for (final value in result) { print(value); }
のように、結果を実際に反復したときに処理されます。
例えば、
import 'dart:collection'; void main() { final numbers = HashSet<int>(); numbers.addAll([ 1, 2, 3, ]); final result = numbers.expand((number) { print('expand: $number'); return [ number, number * 10, ]; }); print('created'); for (final value in result) { print('value: $value'); } }
とすると、
created
が表示された後に、各要素の展開処理が実行されます。
また、同じIterableを再度反復すると、展開処理も再度実行されます。
例えば、
for (final value in result) { print(value); } for (final value in result) { print(value); }
とすると、expandに渡した関数も反復のたびに呼び出されます。
展開結果を一度だけ生成して保持しておきたい場合は、
final result = numbers
.expand(
(number) => [
number,
number * 10,
],
)
.toList();
のようにtoList()などで実体化しておくことができます。
HashSetの要素の順序
HashSetでは、要素の反復順序が保証されていません。
例えば、
import 'dart:collection'; void main() { final numbers = HashSet<int>(); numbers.addAll([ 1, 2, 3, ]); final result = numbers.expand( (number) => [ number, number * 10, ], ); print(result); }
とした場合、
(1, 10, 2, 20, 3, 30)
という順番になるとは限りません。
元のHashSetが、
3 1 2
の順に反復された場合は、
(3, 30, 1, 10, 2, 20)
のようになります。
ただし、それぞれの要素から返されたIterable内部の順序は維持されます。
例えば、
(number) => [ number, number * 10, ]
であれば、あるnumberについて、
number number * 10
という順序で結果に追加されます。
そのため、HashSetでexpandメソッドを使用する場合は、元の要素がどの順番で処理されるかに依存しない処理にしておくとよいでしょう。
空のIterableを返せる
expandメソッドでは、各要素から空のIterableを返すこともできます。
例えば、
final result = numbers.expand( (number) => number.isEven ? [number] : <int>[], );
とすると、奇数からは何も生成されないため、結果には偶数だけが残ります。
この処理は、
final result = numbers.where(
(number) => number.isEven,
);
でも実現できます。
単純に条件に合う要素を絞り込むだけならwhereメソッドの方が分かりやすいでしょう。
一方、
final result = numbers.expand( (number) => number.isEven ? [ number, number * 10, ] : <int>[], );
のように、
条件に合わない要素 → 0個 条件に合う要素 → 複数個
という処理を1回で行いたい場合にはexpandメソッドが便利です。
nullを含むHashSetの場合
nullを格納できるHashSetでもexpandメソッドを使用できます。
例えば、
final languages = HashSet<String?>(); languages.addAll([ 'Dart', 'Flutter', null, ]);
とします。
nullを除外しながら、それぞれの文字列を展開する場合は、
final result = languages.expand<String>( (language) { if (language == null) { return <String>[]; } return [ language, language.toUpperCase(), ]; }, ); print(result);
とできます。
nullの場合は空のList<String>を返しているため、結果には含まれません。
このようにexpandメソッドを使用すると、
不要な要素は除外する 必要な要素は複数の値へ展開する
という処理をまとめて記述できます。
なお、expandメソッドはHashSet固有のメソッドではなく、Iterableで定義されています。
そのため、HashSet以外のさまざまな反復可能オブジェクトでも利用できます。
リファレンス
6. おわりに
DartのHashSetの各要素を0個以上の要素に展開し、それらを1つのIterableとして取得するには、expandメソッドを使用できます。
例えば、
final numbers = HashSet<int>(); numbers.addAll([ 1, 2, 3, ]); final result = numbers.expand( (number) => [ number, number * 10, ], );
とすると、それぞれの値を、
1 → 1, 10 2 → 2, 20 3 → 3, 30
のように複数の要素へ展開できます。
expandメソッドでは、それぞれの要素から返す要素数を自由に変更できます。
そのため、
0個 1個 複数個
のいずれにも展開できます。
例えば、
final result = numbers.expand( (number) => number.isEven ? [ number, number * 10, ] : <int>[], );
とすると、
奇数 → 何も生成しない 偶数 → 2つの値を生成する
という処理もできます。
また、入れ子になったIterableを1つにつなげる用途にも便利です。
final values = HashSet<List<int>>(); values.addAll([ [1, 2], [3, 4], [5, 6], ]); final result = values.expand( (list) => list, );
とすると、複数のList<int>に含まれている要素を、1つのIterable<int>として扱えます。
mapメソッドとの違いとして、
map 1つの要素を1つの要素へ変換する expand 1つの要素を0個以上の要素へ展開し、それらを1つにつなげる
という点があります。
なお、expandメソッドの戻り値はHashSetではなくIterableです。
Listとして利用したい場合は、
final result = numbers
.expand(
(number) => [
number,
number * 10,
],
)
.toList();
とできます。
重複を除いたSetとして利用したい場合は、
final result = numbers
.expand(
(number) => [
number,
number * 10,
],
)
.toSet();
とできます。
また、expandメソッドが返すIterableは遅延評価されます。
結果を反復したときに各要素の展開処理が行われ、同じ結果を再度反復すると展開処理も再度実行されます。
さらに、HashSetは反復順序を保証しないため、展開前の要素が追加順に処理されることを前提にしないよう注意が必要です。
expandメソッドは、1つの要素から複数の値を生成したい場合や、複数のIterableを1つにつなげたい場合に便利なメソッドです。
私が技術者として所属している ニューラル では、 組み込み・制御系ソフトウェアを中心に、モバイルアプリや業務アプリなど、さまざまなソフトウェアの開発を行っています。
組み込み分野では、リアルタイムOSを利用したシステム開発をはじめ、弊社製品のハイブリッドOS Bi-OS など、長年培ってきた組み込み・制御系の技術を活かした開発に取り組んでいます。
また、iOS(Swift/SwiftUI)やWindows(C#)をはじめとしたアプリケーション開発、Webシステムの開発などにも対応しています。
組み込みソフトウェア、モバイルアプリ、Windowsアプリ、Webシステムなど、ソフトウェア開発に関するご相談がございましたら、お気軽にお問い合わせください。
また、ニューラルでは一緒にソフトウェア開発に取り組んでいただける技術者の方も募集しています。
ご興味がございましたら、お気軽にお問い合わせください。
EMAIL : info-nr@newral.co.jp / m-futamata@newral.co.jp
TEL : 042-523-3663
FAX : 042-540-1688