こんにちは!FlutterやDartの開発現場で、日夜コードと格闘している先輩エンジニアです。
皆さんは、Dart 3で導入された「パターンマッチング」、もう使いこなしていますか?
「名前は聞いたことあるけれど、従来の `is` 演算子と何が違うの?」「どうやって使い分ければいいの?」と、少しモヤモヤしていませんか?
ここをクリアすれば、あなたのDartコードは劇的に安全になり、冗長なボイラープレート(お決まりのコード)から解放されますよ。今日は、従来の `is` 演算子とDart 3の「型テストパターン」の決定的な違いを、VM(仮想マシン)の裏側の挙動まで少しだけ覗きながら、分かりやすく紐解いていきましょう!
—
1. 従来の `is` 演算子:何が問題だったのか?
まずは、おなじみの `is` 演算子を振り返ってみましょう。
例えば、APIから受け取った動的なデータ(`Object?` 型)を処理する、こんなコードを書いたことはありませんか?
void processLegacy(Object? data) {
if (data is String) {
// ここで data はコンパイラによって String 型に「昇格 (Promotion)」される
print(‘文字列の長さ: ${data.length}’);
} else if (data is int) {
print(‘2倍の値: ${data 2}’);
}
}
「あれ、これの何がいけないの? 普通に動くじゃん」と思いましたよね。
確かに、ローカル変数であれば `is` チェックの後に自動で型が昇格するため、キャスト(`(data as String)` のような記述)を書く必要はなくなりました。
しかし、この `is` 演算子には、実務でコードが複雑化するにつれて牙をむく「構造的な弱点」があります。
1. 複合的な構造をチェックしようとすると地獄になる
例えば、「`data` が `Map` で、かつキー `’status’` が `String` で、キー `’code’` が `int` である場合」といった、データの「形(構造)」を検証しようとすると、`is` 演算子だけでは歯が立ちません。無数の `if` 文と手動のキャストの嵐になり、コードが読めなくなります。
2. 変数のスコープとフロー解析の限界
`is` はあくまで「この変数はこの型か?」という真偽値(`bool`)を返すだけです。そのため、複数の値を同時に分解して検証するような「スマートな処理」が苦手なのです。
—
2. Dart 3の真骨頂:型テストパターンとは何か?
そこで登場するのが、Dart 3の パターンマッチング(型テストパターン) です。
先ほどのコードを、Dart 3の `switch` 式と型テストパターンを使って書き換えてみましょう。
void processModern(Object? data) {
switch (data) {
// 型テストパターン: data が String 型なら、s という変数にバインドする
case String s:
print(‘文字列の長さ: ${s.length}’);
// 型テストパターン: data が int 型なら、i という変数にバインドする
case int i:
print(‘2倍の値: ${i 2}’);
// どの型にも当てはまらない場合(従来の default に相当)
case _:
print(‘未知のデータ型です’);
}
}
一見すると `is` を使った `if-else` と似ていますが、決定的な違いがあります。
それは、「型の確認」「変数の宣言(バインド)」「スコープの限定」がワンストップで行われる点です。
🧠 ここがDart VMの知見:コンパイラの視点
Dartのコンパイラ(およびCFA:制御フロー解析器)にとって、`switch` によるパターンマッチングは非常に好都合です。
`switch` の各 `case` アームに入る時点で、型が厳格に保証された新しいローカル変数(上記の `s` や `i`)が生成されます。これにより、元の変数(`data`)のスコープ汚染を防ぎ、安全にレジスタやメモリ上の値をハンドリングできるため、実行時エラー(TypeError)の可能性を完全にコンパイル時に関門として弾くことができます。
—
3. 決定的な違いの比較表
整理のために、`is` 演算子とDart 3の型テストパターンの違いを比較してみましょう。
| 比較項目 | 従来の `is` 演算子 (+ 型昇格) | Dart 3 型テストパターン (`switch` / `if-case`) |
| :— | :— | :— |
| 主な目的 | 変数が特定の型であるかの真偽値判定 | 型の判定 + 値の抽出(バインド)を同時に行う |
| 複雑な構造の分解 | 不得意(手動でネストしたチェックが必要) | 得意(オブジェクトやリストの構造を丸ごとパターン化できる) |
| 可読性 (メンテナンス性) | 条件が増えると `if-else if` が肥大化する | 網羅的な `switch` により、スッキリ記述できる |
| 網羅性チェック | なし(自分で `else` を書き忘れるとバグになる) | コンパイラが「網羅されているか」を静的チェックしてくれる |
—
4. 応用編:ネストした構造と「型ガード」の組み合わせ
Dart 3のパターンマッチングの真価は、単なる型のチェックに留まりません。「型をチェックしながら、その中身の構造も同時に検証する」という離れ業が可能です。
例えば、次のような「JSONのようなネストしたデータ」を安全に処理するシナリオを考えてみましょう。
void handleApiResponse(Object? response) {
switch (response) {
// 1. Mapであり、かつ特定のキーと型の構造を持っているかを一度に検証
case {‘status’: ‘success’, ‘data’: String message}:
print(‘成功メッセージ: $message’);
// 2. エラーコードが int 型の場合をキャッチ
case {‘status’: ‘error’, ‘code’: int errorCode}:
print(‘エラーコード: $errorCode’);
// 3. 構造が一致しない場合
default:
print(‘予期せぬレスポンス構造です’);
}
}
どうでしょうか? `is` 演算子を使ってこれを書こうとしたら、`response is Map` を確認し、キャストして、キーが存在するか確認して……と、数十行のボイラープレートが必要になりますよね。それが、パターンマッチングを使えばたった数行で、しかも美しく記述できます。
—
5. 初学者が陥りやすい文法エラーと注意点
ここで、皆さんが実際にコードを書くときにハマりがちなポイントをいくつか共有しておきますね。
⚠️ 注意点1: 変数名のエラー(シャドーイング)
`case String s:` の `s` は、その `case` ブロック内だけで有効な新しい変数です。同じスコープ内で既に使われている変数名と被るとコンパイルエラーになることがあるため、意味のある分かりやすい名前(型名の頭文字など)をつけましょう。
⚠️ 注意点2: `if-case` も強力に使える
「わざわざ `switch` を書くほどでもない、1つの条件だけをサクッとチェックしたい!」という場合は、`if-case` 構文が使えます。
void checkAge(Object? input) {
// input が int 型のときだけ処理する(guard 句も組み合わせ可能)
if (input case int age when age >= 18) {
print(‘成人です ($age歳)’);
} else {
print(‘条件に合いません’);
}
}
※ここで登場した `when` 句こそが、厳密な意味での「型ガード(Type Guard)」です。型が一致した上で、さらに追加の条件式(`age >= 18`)でフィルタリングできます。
—
まとめ:Dartを掌握するために
今日のポイントをまとめます。
1. `is` 演算子は単なる「真偽値の判定(+型昇格)」であり、複雑なデータの検証には向かない。
2. Dart 3の型テストパターンは、「型の確認」「変数へのバインド」「構造の分解」を同時に安全に行える。
3. `when` 句(型ガード)を組み合わせることで、表現力が飛躍的に向上する。
Dart 3のパターンマッチングをマスターすると、データの受け渡しや状態管理(BLocやRiverpodのステート分岐など)のコードが劇的に洗練されます。「型安全」を言語の力で最大限に引き出すDartらしいアプローチを、ぜひ明日の開発から取り入れてみてくださいね。
ここをクリアしたあなたなら、もうDartの基本はバッチリマスターできていますよ!次のステップへ自信を持って進んでいきましょう。