コードレビューの現場から:Dart 3 パターンマッチングの「見えないコスト」
テックリードの私だ。本日のコードレビューで、あるジュニアエンジニアが書いた美しいDart 3のパターンマッチングのコードに目を留めた。一見すると非常にモダンで、宣言的で、読みやすいコードだ。
しかし、AOTコンパイラとDart VMの挙動、そしてオブジェクト指向のカプセル化の裏側を知る者からすれば、このコードは「無駄なメソッド呼び出しの嵐」を引き起こし、ホットパスにおいてパフォーマンスを劣化させる潜在的な爆弾を抱えていた。
君たちは、Dart 3で導入されたObject Patterns(オブジェクトパターン)を使ってクラスのプロパティを分解する際、その裏で何が起きているか意識したことがあるか?
今回は、オブジェクトパターンによるゲッター分解のパフォーマンス的側面と、それを完璧に制御するための変数バインディングテクニックを、AOT/JITコンパイルの視点から紐解いていこう。
—
1. なぜ「Object Patterns」でゲッターが複数回呼ばれるのか?
まずは問題の核心から入る。Dart 3の `switch` 式や `if-case` におけるオブジェクトパターンは、次のように書ける。
// よくある書き方(だが、背後で何が起きているか要注意)
void processResponse(ApiResponse response) {
switch (response) {
// オブジェクトパターンによる分解
case ApiResponse(isSuccess: true, data: final payload):
handleData(payload);
case ApiResponse(isSuccess: false, error: final err):
handleError(err);
}
}
このコード、言語仕様としては完全に正しい。しかし、Dart VMはこのパターンマッチングをどのように評価するだろうか?
C++やRustのプリミティブな構造体分解とは異なり、Dartのクラスはカプセル化されたオブジェクトである。プロパティへのアクセスは、原則としてゲッター(メソッド呼び出し)経由で行われる。
コンパイラやVMの最適化(インライン化など)の度合いにもよるが、パターンマッチングの各条件評価の段階で、以下のような事態が発生し得る。
1. `isSuccess` が評価される際、`response.isSuccess` ゲッターが呼ばれる。
2. 次の条件やガード節、あるいは失敗時のマッチングの過程で、同じゲッターが再度評価される。
3. 特に、ゲッター内部で高コストな計算、文字列のフォーマット、あるいは非同期ではないにしろ重いオブジェクトの生成を行っている場合、意図しない多重評価(Redundant Evaluation)が発生する。
さらに、ゲッターが「呼ぶたびに新しいインスタンスを返す(防衛的コピー等)」ような実装になっている場合、参照の一貫性が崩れ、バグの温床となる。
—
2. 実務で直面する危険なアンチパターン
実際のフロントエンドや、複雑なステート管理、API連携のコードで考えてみよう。以下は、UIのステートを判定するコンポーネントのロジックだ。
// 【アンチパターン】重いゲッターを持つモデル
class UserSessionState {
final Map
UserSessionState(this.rawTokenData);
// 毎回Mapのパースやオブジェクト生成を行う高コストなゲッター
UserPermissions get permissions {
return UserPermissions.fromMap(rawTokenData[‘permissions’] ?? {});
}
bool get isExpired => DateTime.now().millisecondsSinceEpoch > (rawTokenData[‘exp’] ?? 0);
}
// 危険なパターンマッチング
void renderDashboard(UserSessionState session) {
switch (session) {
// ここで session.permissions が評価される
case UserSessionState(isExpired: false, permissions: UserPermissions(isAdmin: true)):
showAdminPanel();
break;
// 別のケース評価や内部の構造マッチングで、再度ゲッターが叩かれる可能性がある
case UserSessionState(isExpired: false, permissions: UserPermissions(canRead: true)):
showStandardPanel();
break;
default:
showLoginRedirect();
}
}
このコードの何がヤバいか? `permissions` ゲッターの内部で `UserPermissions.fromMap(…)` が走っている。パターンマッチングの評価フローの都合上、これが1回の分岐評価の中で複数回実行されるリスクがある。
これが60fps(あるいは120fps)を死守すべきFlutterのビルドフェーズや、ミリ秒単位の応答が求められるWeb/APIハンドラで起きたらどうなるか。ガベージコレクション(GC)の圧力が高まり、フレームドロップの直接的な原因となる。
—
3. 解決策:`when` ガードと変数バインディング(`as` / `var`)による防御的設計
この問題を解決し、パフォーマンスと堅牢性を両立させるための鉄則がこれだ。
> 「一度しか評価したくない高コストなプロパティやカスタムゲッターは、パターンマッチングの前に一度ローカル変数にバインドするか、`as` / `var` パターンを使って一度だけ評価させ、その変数を `when` ガードや後続のロジックで使い回せ」
プロダクションコードで使うべき、洗練された書き換え例を見てみよう。
模範的なプロダクションコード
class UserSessionState {
final Map
UserSessionState(this.rawTokenData);
UserPermissions get permissions =>
UserPermissions.fromMap(rawTokenData[‘permissions’] ?? {});
bool get isExpired =>
DateTime.now().millisecondsSinceEpoch > (rawTokenData[‘exp’] ?? 0);
}
class UserPermissions {
final bool isAdmin;
UserPermissions.fromMap(Map
: isAdmin = map[‘is_admin’] ?? false;
}
// 【推奨アプローチ】変数を事前にバインディングし、評価コストを1回に限定する
void renderDashboardOptimized(UserSessionState session) {
// 1. まず基本ステータスを評価(軽量なプリミティブ)
if (session.isExpired) {
showLoginRedirect();
return;
}
// 2. 重いゲッター(permissions)を一度だけ変数にバインディング
final currentPermissions = session.permissions;
// 3. バインドしたローカル変数に対してパターンマッチングや条件分岐を行う
switch (currentPermissions) {
case UserPermissions(isAdmin: true):
showAdminPanel();
case UserPermissions(isAdmin: false):
showStandardPanel();
}
}
もし、どうしても単一の `switch` 式の中で完結させたい場合は、変数パターン(Variable Pattern)を使ってオブジェクト自体をキャプチャし、ゲッターの呼び出し回数を制御する。
void renderDashboardExpression(UserSessionState session) {
// session を一時変数 `s` に束縛しつつ、isExpired を評価
if (session case UserSessionState(isExpired: false) when !session.isExpired) {
// ※ 注意: 上記の書き方だと session.isExpired が2回呼ばれる可能性があるため、
// 以下のように一度ローカル変数に落とすのが Dart VM 最適化上もベスト。
}
// ベストプラクティス:switchに入る前にプリミティブ評価を終わらせるか、
// オブジェクトそのものをキャプチャしてプロパティアクセスを1回にする。
var targetSession = session;
switch (targetSession) {
case UserSessionState(isExpired: false):
// ゲッターを明示的に1回だけ呼んでローカル変数へ
final perms = targetSession.permissions;
_handleValidSession(perms);
default:
showLoginRedirect();
}
}
void _handleValidSession(UserPermissions perms) {
switch (perms) {
case UserPermissions(isAdmin: true):
showAdminPanel();
default:
showStandardPanel();
}
}
—
4. チーフアーキテクトからの提言:Dartの言語特性をリスペクトせよ
Dartは、JavaScriptへのトランスパイル(Web)も、ネイティブマシンコードへのAOTコンパイル(Flutter/VM)もこなす極めて洗練された言語だ。しかし、「モダンな書き方だから美しい」という理由だけで、内部で何が行われているかをブラックボックスにしたまま機能を使うのは、プロフェッショナルエンジニアの仕事ではない。
オブジェクトパターンは強力な武器だが、以下の原則をコードレビューの共通認識として持ってほしい。
1. プリミティブなフィールド(`final int`, `final bool` など)の分解は、パフォーマンスコストがほぼゼロなので、心置きなくオブジェクトパターンを使ってよい。
2. 計算を伴うゲッター、オブジェクトを新規生成するゲッターをオブジェクトパターン内で直接分解するのは避ける。
3. 複雑な条件分岐は、無理に1つの巨大な `switch` 式に押し込もうとせず、ローカル変数へのバインディングや早期リターン(Early Return)を組み合わせることで、「ゲッターの評価回数を明確に1回に制限する」。
マシンに優しく、メモリ効率が高く、そして何よりバグを生まないコード。それこそが、私たちが目指すプロダクションクオリティだ。次のプルリクエストからは、この視点を持ってコードを書いてくれ。期待している。