こんにちは!FlutterやDartの世界へようこそ。先輩フルスタックエンジニアの私です。
日々の開発で、APIから受け取る複雑なJSONデータや、独自のドメインモデルで定義した「木構造(ツリー構造)」を扱うことってよくありますよね。
「子ノードを再帰的にたどって値を書き換えたいけれど、if文や型キャスト(`as`)の嵐になってコードが読めない……」と頭を抱えた経験はありませんか?
ご安心ください。Dart 3で導入された「パターンマッチング」を使いこなせば、そんな複雑な再帰的データ構造の走査と変換が、まるで魔法のように美しく、安全に書けるようになるんです。
ここをクリアすれば、あなたのDartコードは一気に函数型言語のような洗練された美しさを手に入れられますよ。さあ、一緒にその極意をマスターしていきましょう!
—
1. 木構造(Tree Structure)とは何か?
まず、私たちがこれから扱う「木構造」のイメージを共有しておきましょう。
木構造とは、データが階層状(親子関係)に枝分かれしていくデータ構造のことです。身近な例だと、PCのフォルダ構造や、HTMLのDOMツリー、そしてプログラムの構文木(AST)などがこれに当たります。
Dartで表現すると、ざっくり以下のようなイメージです。
[ルートノード: 式]
├── [左の子: 数値(10)]
└── [右の子: 足し算ノード]
├── [左の子: 数値(5)]
└── [右の子: 数値(3)]
この「枝分かれの深さが不定で、どこまで続くか分からないデータ」を効率よく処理するために、私たちは再帰関数(自分自身を呼び出す関数)を使います。そして、その再帰の中で「今いるノードが何のデータ型で、どんな値を持っているか」をエレガントに判定する武器こそが、Dart 3のパターンマッチングなのです。
—
2. Dart 3 パターンマッチングの基本と強力な武器
Dart 3では、`switch`文や`switch`式が劇的に進化しました。単なる値の比較だけでなく、「データの形(パターン)に一致するかどうか」を構造的に検査し、同時に変数へ分解(Destructuring)できるようになったのです。
例えば、算術式の抽象構文木(AST)を考えてみましょう。
ノードには「数値」もあれば「足し算」もあり、「掛け算」もあります。これをDart 3の `switch` 式を使って表現すると、次のようになります。
実装例:シンプルな式の評価(Evaluation)
// 1. シールドクラス(Sealed class)でノードの型を網羅的に定義
sealed class Expr {}
class Num extends Expr {
final int value;
Num(this.value);
}
class Add extends Expr {
final Expr left;
final Expr right;
Add(this.left, this.right);
}
class Multiply extends Expr {
final Expr left;
final Expr right;
Multiply(this.left, this.right);
}
// 2. パターンマッチングを使った再帰的な評価関数
int evaluate(Expr expr) {
return switch (expr) {
// Num 型にマッチし、中の value を int v にバインドする
Num(value: var v) => v,
// Add 型にマッチし、左右の子ノードを再帰的に評価して足す
Add(left: var l, right: var r) => evaluate(l) + evaluate(r),
// Multiply 型にマッチし、左右の子ノードを再帰的に評価して掛ける
Multiply(left: var l, right: var r) => evaluate(l) evaluate(r),
};
}
どうですか?このコードの美しいところは、型キャストが一切不要な点です。
コンパイラ(Dart VM)は、`sealed class` とパターンマッチングの組み合わせにより、「すべてのパターンが網羅されていること(Exhaustiveness)」をコンパイル時に保証してくれます。万が一、新しいノードクラスを追加し忘れると、コンパイラが「おい、このケースが抜けているぞ!」と怒って教えてくれるんです。なんて賢いんでしょう!
—
3. 実践:ツリー構造を「変換(トランスフォーム)」する
走査(見るだけ)だけでなく、木構造のデータを「別の形に変換する」という要件も現場では非常によくあります。
例えば、「ツリー内にあるすべての数値を2倍に書き換えた新しいツリーを生成する」という処理を考えてみましょう。
コード例:再帰的なイミュータブル変換
// ツリー構造を走査し、すべての Num の値を 2 倍にした「新しいツリー」を返す関数
Expr doubleValues(Expr expr) {
return switch (expr) {
// 数値ノードなら、値を2倍にした新しい Num を返す(イミュータブルな変換)
Num(value: var v) => Num(v 2),
// 足し算ノードなら、左右の子をそれぞれ再帰的に変換して新しい Add を組む
Add(left: var l, right: var r) => Add(doubleValues(l), doubleValues(r)),
// 掛け算ノードも同様
Multiply(left: var l, right: var r) => Multiply(doubleValues(l), doubleValues(r)),
};
}
void main() {
// (10 + (5 3)) という式を組み立てる
Expr ast = Add(
Num(10),
Multiply(Num(5), Num(3)),
);
// 値を2倍に変換
Expr transformed = doubleValues(ast);
// 評価してみる
print(‘変換後の計算結果: ${evaluate(transformed)}’); // 結果は?
// 102 + (52 32) = 20 + (10 6) = 80 になります!
}
このように、データを破壊的(Mutable)に書き換えるのではなく、パターンマッチングと再帰を使って新しいイミュータブル(不変)なデータを組み立てていくのが、関数型プログラミングの醍醐味であり、Flutter/Dartのアーキテクチャとも非常に相性が良いアプローチです。
—
4. 陥りがちな罠とエラー回避のポイント
初学者の開発者や、他の言語から移行してきた方がよくハマるポイントをいくつか先回りして解説しておきますね。
罠1: 「パターンが網羅されていません」というコンパイルエラー
`switch` 式を使う際、すべての可能性を網羅していないと、Dartは容赦なくエラーを出します。
「そんなの分かってるよ、デフォルトケース(`_ => …`)を書けばいいんでしょ?」と思いがちですが、`sealed class` を使っている場合は、あえてデフォルトケースを書かずにすべてのサブクラスを列挙するのがベストプラクティスです。
こうすることで、将来仕様変更で新しいノードを追加した時に、コンパイラが修正漏れを防いでくれます。
罠2: プロパティパターンの書き間違い
Dartのパターンマッチングでは、オブジェクトのプロパティを指すときに `ClassName(propertyName: var variableName)` という構文を使います。
これをうっかり `ClassName(var propertyName)` のように書くと、文法エラーになります。
「オブジェクトのフィールド名 : 受け取る変数名」という対応関係をしっかり覚えておきましょう。
—
まとめ
いかがでしたでしょうか?
今回は、Dart 3のパターンマッチングと再帰関数を組み合わせて、複雑な木構造を安全かつ美しく走査・変換する方法を解説しました。
- `sealed class` でデータの種類を厳格に定義する
- `switch` 式とパターンマッチングで、型安全にデータを分解・処理する
- 再帰関数を使って、深さに関係なくツリー全体をエレガントに舐め尽くす
この3つの組み合わせを自分のものにできれば、JSONパース、AST処理、複雑なUIの状態ツリーなど、どんな難解なデータ構造が目の前に来ても怖くありません。
ここをクリアすれば、あなたのDartの基礎力は確実に次のステージに到達しています。ぜひ実際のプロジェクトのコードでも試してみてくださいね。それでは、また次回の記事でお会いしましょう!