こんにちは!FlutterやDartの開発現場で、日夜コードと格闘している先輩エンジニアです。
皆さんは、JSONデータやUIのツリー構造、あるいは数式のような「再帰的な木構造(Tree Structure)」を扱うとき、コードが複雑になって頭が痛くなった経験はありませんか?「あっちの条件分岐、こっちの型チェック……」と書いているうちに、自分が何を書いているのか分からなくなる、というのはよくある話です。
でも、安心してください。Dart 3で導入された「パターンマッチング」を使いこなせるようになると、この再帰的な構造の走査と変換が、驚くほどエレガントで、見通しの良いコードに生まれ変わります。
ここをクリアすれば、あなたのDartの表現力は一気にワンランク上のステージに到達しますよ。さあ、一緒にその扉を開いていきましょう!
—
1. 再帰的木構造とは何か?(イメージで掴む)
プログラミングにおける「木構造」とは、枝分かれしていくデータ構造のことです。身近な例だと、パソコンのフォルダ(フォルダの中にフォルダやファイルがある)や、WebのDOMツリー、APIから返ってくるJSONなどがまさにこれです。
Dartでこれを表現するとき、典型的な「密封クラス(`sealed class`)」を使います。
[ Node (根) ]
/ \
[ Branch (枝) ] [ Leaf (葉: 値を持つ) ]
/ \
[Leaf] [Leaf]
親が子を持ち、その子がさらに子を持つ……というように、自分自身をネストして持っているから「再帰的」と呼びます。
—
2. Dart 3 パターンマッチングの基本兵器
Dart 3では、`switch`式や `pattern` が導入され、データの形(構造)をそのままパターンとしてマッチさせることができるようになりました。
従来の `if-else` や `is` 演算子による型キャストの嵐とはおさらばです。パターンマッチングは、「データの形状をスキャンし、合致した瞬間に変数へバインド(抽出)する」という強力な武器になります。
—
3. 実践:JSON風の木構造を走査・変換してみよう
それでは、具体的にコードを見ていきましょう。
今回は、数値や文字列、そしてそれらをまとめる「リスト」や「マップ」のような再帰的データ構造を定義し、それを「HTML風の文字列に変換する」という処理を書いてみます。
以下のコードをそのままDartPadなどで実行してみてください。
// 1. Dart 3の `sealed class` を使って、木構造の型を完全に網羅する
sealed class JsonNode {}
// 葉(値を持つノード)
class JsonText extends JsonNode {
final String value;
JsonText(this.value);
}
// 枝(子要素のリストを持つノード)
class JsonElement extends JsonNode {
final String tagName;
final List
JsonElement(this.tagName, this.children);
}
// 2. パターンマッチングを使った再帰的走査・変換関数
String renderHtml(JsonNode node) {
// switch式とパターンマッチングの組み合わせ
return switch (node) {
// パターン①: JsonText型にマッチし、中身の `value` を `text` 変数にバインドする
JsonText(value: var text) => text,
// パターン②: JsonElement型にマッチし、`tagName` と `children` を取り出す
JsonElement(tagName: var tag, children: var kids) :
// 子要素たちを再帰的に変換し、結合する
‘<$tag>${kids.map(renderHtml).join()}$tag>‘,
};
}
void main() {
// 3. 木構造の構築
//
//
//
Hello Dart 3
//
Recursive Pattern Matching is awesome!
//
//
final domTree = JsonElement(‘html’, [
JsonElement(‘body’, [
JsonElement(‘h1’, [JsonText(‘Hello Dart 3’)]),
JsonElement(‘p’, [JsonText(‘Recursive Pattern Matching is awesome!’)]),
]),
]);
// 4. 実行と結果の確認
final htmlString = renderHtml(domTree);
print(htmlString);
// 出力結果:
Hello Dart 3
Recursive Pattern Matching is awesome!
}
コードのここがスゴい!
`switch (node)` の中で、`JsonText(value: var text)` と書くだけで、Dartが自動的に型を判別し、安全に `value` の中身を `text` という変数に取り出してくれています。
従来のオブジェクト指向のように、各クラスに `toHtml()` メソッドをバラバラに定義する必要はありません。「データ構造」と「それに対する処理(ビジネスロジック)」を綺麗に分離できるのが、パターンマッチングの真骨頂です。
—
4. 陥りやすい文法エラーと注意点
初心者のうちは、Dart 3のパターンマッチングでいくつかハマりやすいポイントがあります。ここで先回りしてクリアしておきましょう。
① 網羅性(Exhaustiveness)のエラー
`sealed class` を使っている場合、`switch` 式はすべてのサブクラスを網羅していなければコンパイルエラーになります。
もし将来、新しいノードの種類(例: `JsonNumber`)を追加したとき、`renderHtml` 関数でそれを処理し忘れると、Dartのコンパイラが「おい、ここが抜けてるぞ!」と教えてくれます。バグを未然に防ぐ最強の仕組みですね。
② プロパティパターンの構文ミス
オブジェクトのプロパティをパターンマッチさせる際、以下の書き間違いによく遭遇します。
// ❌ 間違い:変数名を直接書こうとしてしまう
JsonElement(tagName, children) => …
// ⭕️ 正しい:プロパティ名: var 変数名 の形式にする
JsonElement(tagName: var tag, children: var kids) => …
Dartのパターンマッチングでは、どのプロパティの値をどの変数にバインドするかを明示的に指定します(省略記法もありますが、最初は上記のように書くのが確実です)。
—
まとめ
いかがでしたか?
Dart 3のパターンマッチングと `sealed class` を組み合わせることで、複雑になりがちな再帰的な木構造の走査や変換が、こんなにもシンプルで堅牢に書けるようになります。
- データの形をそのままパターンとして記述できる
- `switch` 式でエレガントに分岐・抽出ができる
- コンパイラが網羅性をチェックしてくれるので安全
ここをマスターすれば、JSONパーサーや独自のAST(抽象構文木)、Flutterのウィジェットツリーを模したカスタム構造の操作など、どんなに複雑なデータ構造が来ても怖くありません。
ぜひ、あなたのプロジェクトでも使ってみてくださいね。
Dartの基本はこれでバッチリマスターできましたよ!次のステップへ進みましょう!