【テクニカル・上級編】Dart 3のパターンマッチングにおける「失敗」の伝播:if-caseとswitch式の挙動の違い – Dart コア文法・オブジェクト指向・Null安全解析バイブル

1. パターンマッチングの真相:Dart CFEとDart VMのレイヤで何が起きているか

Dart 3で導入されたパターンマッチングは、単なるシンタックスシュガーではない。Dartのフロントエンドコンパイラである CFE (Common Front End) と、実行時エンジンである Dart VM / AOTコンパイラ の命令生成における本質的なパラダイムシフトである。

従来の `if` 文や `switch` 文は、単一のスカラ値や条件式の評価(スカラ比較・ブール評価)を行い、命令ポインタ(Instruction Pointer)を分流させる単純な制御構造に過ぎなかった。しかし、Dart 3のパターンは、評価対象(Scrutinee)に対する以下の3つの操作をアトミックに複合実行する抽象化層として機能する。

1. 構造の検証 (Structural Validation): オブジェクトの型、形状、あるいはフィールドの存在チェック
2. 値の非構築・抽出 (Destructuring & Extraction): 構造内部のデータへのアクセスおよびローカル変数(レジスタ/スタック)への束縛
3. ガード条件の評価 (Guard Evaluation): 抽出された値に対する述語論理(`when` 節)の実行

開発者が理解すべき真実は、「パターンの不一致(失敗)」とは、言語仕様上の例外(Exception)ではなく、CFEが生成する制御フローグラフ(CFG: Control Flow Graph)における基本ブロック(Basic Block)間の条件付きジャンプ命令の帰結であるという点だ。

CFEによるKernel ASTへのコンパイル処理

DartコードがDart VMの低レベル中間表現(IL: Intermediate Representation)やAOTの機械語へと変換される前段階で、CFEはパターンを Kernel AST と呼ばれる高レベルIRに展開する。

例えば、ネストされたオブジェクトのデストラクチャリング伴うパターンは、CFEによって一連の明示的な型テスト(`is`)、プロパティゲッター呼び出し、Nullチェック、および論理AND条件に即座に脱糖(Desugaring)される。

この脱糖プロセスの過程で、「パターンが失敗した際に命令ポインタをどこへ逃がすか」の設計が、`if-case` と `switch` 式で決定的に異なる。

—

2. `if-case` の失敗伝播:ローカル分岐とレジスタスコープの無効化

`if-case` 構文における「失敗」の評価は、ローカルな制御フローの分岐(Local Branching) として処理される。評価対象がパターンにマッチしなかった場合、処理は即座に中断され、制御は `else` ブロック(存在しない場合は次の命令)へとジャンプする。

実行時における失敗の伝播メカニズム

`if-case` が評価される際、Dart VMのスタックフレーム内では以下の挙動が保証される。

  • アトミックなスコープ限定: パターン抽出によって定義される変数は、パターン評価が完全に成功するまで現在のスコープにバインドされない。評価プロセスの途中で(例えばネストされた述語や `when` ガードで)失敗が発生した場合、それまで一時レジスタに保持されていた抽出値は直ちに破棄される。
  • 副作用の非逆転性: ただし、パターンの抽出過程でゲッター(Getter)が呼び出された場合、そのゲッターが持つ副作用(サイドエフェクト)は失敗時にロールバックされない。プロパティ抽出は評価順序に従って順次実行されるためである。

`if-case` の失敗メカニズムを示す低レイヤコード解析

import ‘dart:developer’;

abstract class Node {}
class Leaf extends Node {
final int value;
Leaf(this.value);
}
class Branch extends Node {
final Node left;
final Node right;
Branch(this.left, this.right);
}

// 複雑なネストパターンとガード節を持つ関数
void processNode(Node node) {
print(‘— processNode Execution Start —‘);

// CFEはこの if-case を複数の条件判定ジャンプへ分解する
if (node case Branch(
left: Leaf(value: final lVal),
right: Leaf(value: final rVal)
) when evaluateGuard(lVal, rVal)) {

// パターンマッチおよびガード条件が「すべて」成功した場合のみこの基本ブロックに入る
// lVal, rVal はこのスコープでのみ有効なローカル変数(スタック割り当て)となる
print(‘SUCCESS: Matching Branch with lVal=$lVal, rVal=$rVal’);
} else {
// 【失敗の伝播先】
// パターン構築(型チェック失敗、Nullチェック失敗、またはwhenガードでのfalse)の
// いずれの時点で失敗しても、即座に命令ポインタはこの基本ブロックにジャンプする
print(‘FAILURE: Pattern match failed or guard returned false. Short-circuited to else block.’);
}
}

bool evaluateGuard(int left, int right) {
print(‘ [Guard Check] Evaluating evaluateGuard($left, $right)’);
// 意図的にガードを失敗させるロジック
return left + right > 100;
}

void main() {
final tree1 = Branch(Leaf(10), Leaf(20)); // ガードで失敗するケース (10 + 20 <= 100) final tree2 = Branch(Leaf(60), Leaf(50)); // マッチング成功するケース (60 + 50 > 100)
final tree3 = Leaf(999); // 最上層の型チェック(Branch)で即座に失敗するケース

processNode(tree1);
processNode(tree2);
processNode(tree3);
}

実行結果例

— processNode Execution Start —
[Guard Check] Evaluating evaluateGuard(10, 20)
FAILURE: Pattern match failed or guard returned false. Short-circuited to else block.
— processNode Execution Start —
[Guard Check] Evaluating evaluateGuard(60, 50)
SUCCESS: Matching Branch with lVal=60, rVal=50
— processNode Execution Start —
FAILURE: Pattern match failed or guard returned false. Short-circuited to else block.

命令パイプラインの視点

`tree3`(`Leaf(999)`)の評価において、VMは `Branch` 型チェックの第一評価で即座に偽判定を下す。この瞬間、内部の `left` や `right` へのアクセス命令、および `evaluateGuard` の関数呼び出し命令は 完全にスキップ(ショートサーキット) され、CPUのブランチ予測命令に従って `else` ブロックのアドレスへと非同期遅延なしにコントロールが移る。

—

3. `switch` 式の失敗伝播:網羅性判定と式文脈における例外射出

`if-case` がローカルな分岐制御構造であるのに対し、`switch` 式(Switch Expression)は単一の評価値を生成しなければならない式(Expression Context) である。この文脈の違いが、失敗時の振る舞いに劇的な差異を生み出す。

1. 網羅性チェック(Exhaustiveness Checking)と静的失敗

`switch` 式における最大の防御壁は、コンパイル時に実行される Space Algebra(空間代数)に基づく網羅性解析 である。

CFEは、評価対象の静的型空間 $T$ に対し、各アーム(Arm)がカバーするパターンの和集合 $P_1 \cup P_2 \cup … \cup P_n$ を計算する。$T \setminus \bigcup P_i \neq \emptyset$ (カバーされない空間が存在する)場合、コンパイラはコードのビルドを絶対的に拒否する。

sealed class Event {}
class UserLogin extends Event { final String userId; UserLogin(this.userId); }
class UserLogout extends Event {}

// コンパイルエラーの例:
// Bad: コンパイラは Event のすべてのサブタイプがカバーされていないことを検知する
// String handleEventFailure(Event event) => switch (event) {
// UserLogin(:final userId) => ‘User logged in: $userId’,
// };
// Error: The type ‘Event’ is not exhaustively matched by the switch expression.

2. 動的評価失敗と `StateError` / `ReachabilityError` の射出

コンパイル時の網羅性チェックを通過したにもかかわらず、実行時にいずれのアームのパターンにもマッチしなかった場合、何が起きるか。

これは通常、以下のような状況で発生する。

  • ガード節(`when`)が存在し、型としては網羅されているが、すべてのガードが `false` を返した場合
  • 動的型付け(`dynamic` 型の評価対象)により静的チェックをバイパスした場合

`switch` 式は「値を返さなければならない」という型システムの不変条件(Invariant)を持つ。したがって、すべてのアームでマッチングが失敗した瞬間、制御フローは `else` ブロックに流れるのではなく、Dart VMによって即座に非補獲領域からの例外(`StateError`)が投げられ、スタックフレームが破綻(Unwind)する。

`switch` 式の失敗伝播とスタックアンワインドの検証コード

import ‘dart:developer’;

sealed class DeviceStatus {}
class Online extends DeviceStatus { final int latencyMs; Online(this.latencyMs); }
class Offline extends DeviceStatus {}

/// switch 式による値の評価
/// ガード条件の失敗がどのように例外へと昇華するかを実証する
String evaluateNetworkHealth(DeviceStatus status) {
// CFE は DeviceStatus が sealed であるため、Online と Offline で網羅されていると判断する。
// しかし、Online のアームには `when` ガードが存在するため、実行時に失敗する可能性がある。
return switch (status) {
Online(:final latencyMs) when latencyMs < 50 => ‘EXCELLENT ($latencyMs ms)’,
Online(:final latencyMs) when latencyMs >= 50 && latencyMs < 200 => ‘ACCEPTABLE ($latencyMs ms)’,
// 注意: latencyMs >= 200 の Online オブジェクトが渡された場合、上記の Online パターンはすべて「失敗」する。
// かつ、以下の Offline パターンにもマッチしない。
Offline() => ‘DISCONNECTED’,
};
}

void main() {
print(‘Case 1: ‘ + evaluateNetworkHealth(Online(20)));
print(‘Case 2: ‘ + evaluateNetworkHealth(Offline()));

try {
print(‘Case 3 (Boundary Failure):’);
// latencyMs = 500 はどの when ガードにも適合しない。
// 静的解析を通過するが、実行時に「式の評価失敗」が確定する。
final result = evaluateNetworkHealth(Online(500));
print(result);
} catch (e, stackTrace) {
print(‘\n[CRITICAL RUNTIME FAILURE] Exception Caught!’);
print(‘Exception Type: ${e.runtimeType}’);
print(‘Message: $e’);
print(‘Stack Trace Segment:\n${stackTrace.toString().split(‘\n’).take(3).join(‘\n’)}’);
}
}

実行結果例

Case 1: EXCELLENT (20 ms)
Case 2: DISCONNECTED
Case 3 (Boundary Failure):

[CRITICAL RUNTIME FAILURE] Exception Caught!
Exception Type: StateError
Message: Bad state: No matching case found logic for Online(500)
Stack Trace Segment:
0 evaluateNetworkHealth (file:///path/to/bin/main.dart:10:10)
1 main (file:///path/to/bin/main.dart:28:20)

—

4. アーキテクチャ比較:`if-case` vs `switch` 式の失敗伝播の根本的相違

両者の失敗処理メカニズムの違いを、コンパイラ設計およびランタイムの観点から下表に集約する。

| 評価軸 | `if-case` ステートメント | `switch` 式 (Expression) |
| :— | :— | :— |
| 失敗の帰結 (Outcome of Failure) | 条件判定の偽評価。命令ポインタが `else` または直後の Block へ移動 | 式の評価不能。`StateError` の非同期/同期例外射出によるコールスタックの解体 |
| 網羅性検証 (Exhaustiveness) | 不要(不一致が正常な分岐制御として設計されている) | コンパイル時強制(型システムにおける代数的データ型の証明) |
| 評価コンテキスト | Statement (文文脈): 副作用または局所的実行の制御 | Expression (式文脈): 右辺値(R-value)の生成、単一レジスタへの結果書き込み |
| ガード(`when`)失敗時の挙動 | 即座に短絡評価し、次の条件文または `else` へ分岐 | 次のケースのアームへフォールバック。全不一致時は例外発生 |
| メモリ/スタック最適化 | パターン内変数は判定成功時のみスタックフレームの有効範囲に解放 | レジスタ割り当てが最適化され、成功したアームの評価結果のみが返却値用レジスタに保持される |

VMの決定木(Decision Tree)最適化の違い

`switch` 式において、CFEおよびVM AOTコンパイラは、単なる線形な `if-else` の連続ではなく、型タグ(Class ID / Tag)に基づいた ジャンプテーブル(Jump Table)または二分決定木(Binary Decision Tree) を生成する。

[switch 式の内部評価決定木 (AOT IL)]

[Scrutinee Type Tag Check]
/ \
(Tag == Online) (Tag == Offline)
/ \
[Guard Check 1] [Return ‘DISCONNECTED’]
/ \
(True) (False)
/ \
[Return ‘EXCELLENT’] [Guard Check 2]
/ \
(True) (False)
/ \
[Return ‘ACCEPTABLE’] [THROW StateError] <--- 【例外射出ノード】 `if-case` の場合、最終ノードは `[THROW StateError]` ではなく、明示的な `[Jump to Else / EndBlock]` 命令となる。このわずかな生成ILの差異が、非常に高いスループットを要求されるシステム(レイテンシクリティカルなパケットルーティングや取引エンジンなど)において、分岐予測のヒット率やCPUキャッシュ効率に直接影響を与える。 ---

5. 超高堅牢性・高スループットシステムのための実装指南

Dart 3のパターンマッチングを大規模なプロダクションコードに適用する際、アーキテクトは「失敗がどのように伝播するか」を考慮して構文構成を完全に使い分ける必要がある。

指針 1: 「ドメインモデルの変換」には `switch` 式を選択し、静的防壁を構築せよ

データ構造の変換(APIドメインモデルからUI状態へのマッピングなど)において、未知の状態や漏れはシステムの不整合を引き起こす。この場合、`switch` 式の網羅性保証と失敗時の例外射出を最大限に活用すべきである。

  • ガード節(`when`)を `switch` 式のアームで多用しすぎないこと。ガード節はコンパイラの静的網羅性チェックの「穴」となり、実行時の `StateError` を誘発する原因となる。
  • 可能な限り、ガード条件はパターンの型定義自体(Sealed Hierarchy)に昇華させること。

指針 2: 「構造のフィルタリング・探査」には `if-case` を使用せよ

不完全なデータ構造や、一部のパターンにのみ関心がある場合(イベントバスからの特定のイベントの抽出、ネストされたJSONパースなど)は、`if-case` を使用する。

  • パターン不一致が「エラー」ではなく「単なる非該当」である場合、`switch` 式で `_ => null` のようなダミーアームを作るよりも、`if-case` の持つ「ローカル分岐」モデルを使用する方が、意図が明確であり、中間オブジェクト生成のコンパイラ最適化(Escape Analysis)にも寄与する。

結論

Dart 3のパターンマッチングにおける「失敗」とは、単にコードが実行されないことではない。

  • `if-case` における失敗は、「局所的な計算のショートサーキットと安全な分岐」 である。
  • `switch` 式における失敗は、「式の不変条件の破壊に対するシステムの自己防衛(例外射出)」 である。

このコンパイラレベル、VMレベルでの挙動の違いを深く理解して選択されたコードのみが、極限のパフォーマンスと高い型安全性を両立させるのである。

タイトルとURLをコピーしました