【実務・中級編】Dartのnum型が持つ「実行時型判定」のコストと、int/doubleへのキャストが推奨される理由 – Dart コア文法・オブジェクト指向・Null安全解析バイブル

コードレビューの最中、プロダクトの根幹を成すデータ処理レイヤーで次のようなコードを見かけたとしよう。

// どこかのAPIから取得した不敵な数値データ
num parseScore(Map json) {
return json[‘score’];
}

一見すると、`int`も`double`も両方受け入れられる便利なコードに見えるかもしれない。特に、JSONのスキーマが曖昧なバックエンドとやり取りするフロントエンド開発や、型が揺れやすい非同期API連携の現場では、思考停止で`num`型を採用したくなる誘惑に駆られるはずだ。

だが、チーフアーキテクトとして断言しよう。Dartのプロダクションコードにおいて、`num`型を安易にインターフェースやドメインモデルの型として使い続けることは、実行時パフォーマンスの慢性的低下と、予期せぬ型エラー(TypeError)の温床を作る悪手である。

今回は、Dart VMの内部挙動、AOTコンパイラの最適化、そして堅牢なフロントエンド設計の観点から、なぜ`num`を捨て、`int`や`double`へと明示的にキャスト・型安全化しなければならないのかを徹底的に解説する。

—

1. なぜ `num` は危険なのか? Dart VMとAOTの裏側

Dartは強力な静的型付け言語でありながら、JIT(Just-In-Time)およびAOT(Ahead-Of-Time)コンパイルを通じてネイティブコードやJS/Wasmへと変換される。ここで、`int`とペンディングされた`num`がCPUやVM上でどう扱われるかを知る必要がある。

実行時型判定(Runtime Type Checking)のコスト

`num`型は抽象クラスであり、その実体は実行時に `int`(通常は64ビット整数、JSターゲットでは倍精度浮動小数点数)であるか、`double`(64ビット浮動小数点数)であるか不定である。

そのため、`num`型に対して四則演算や比較を行おうとすると、Dart VMは実行時までその値の具象型を確定できず、インラインキャッシュ(Inline Caching)のヒット率が低下する。
さらに悪いことに、プロパティやメソッドへのアクセスごとに暗黙の型判定(またはボクシング/アンボクシングのオーバーヘッド)が発生し、ホットパス(高頻度で実行されるループや描画フレーム更新処理など)において、JITの最適化(最適化コンパイル)が阻害される。

静的解析と安全性の崩壊

`num`を使う最大の弊害は、「型安全性の放棄」である。
例えば、UIコンポーネントのレイアウト計算やCSSピクセルの指定に`num`が流れ込んできたとしよう。`int`が期待される箇所に意図せず`double`(あるいはその逆)が混入することで、意図しない型不一致エラーが、コンパイル時ではなくユーザーのブラウザやデバイス上の実行時に爆発する。

—

2. 堅牢な設計パターン:境界で型を確定させよ

フロントエンド設計の鉄則として、「外部世界(APIやJSON)から渡ってきた曖昧なデータは、アプリケーションの境界(Boundary)で厳格にパースし、ドメインモデルへ変換する」という原則がある。

APIレスポンスを受け取った瞬間に`int`または`double`へ明示的にキャスト、あるいは変換を行い、内部のビジネスロジックやコンポーネントには一切の`num`を持ち込ませない。これがバグを根絶する唯一の解法だ。

実務で即戦力となるプロダクションコード例

以下のコードは、APIからの非同期レスポンスを受け取り、厳格に型を保証しながらコンポーネントへデータを供給する堅牢な実装パターンである。

import ‘dart:async’;

/// 【ドメインモデル】
/// すべての数値は意図が明確な int または double で保持する。num型は排除。
class UserMetrics {
final int userId;
final int loginCount;
final double satisfactionRate;

const UserMetrics({
required this.userId,
required this.loginCount,
required this.satisfactionRate,
});

/// 境界(Boundary)でのパースと厳格な型変換
factory UserMetrics.fromJson(Map json) {
return UserMetrics(
// intへの安全なキャスト (失敗時はデフォルト値や例外で早期Failさせる)
userId: _parseInt(json[‘user_id’], ‘user_id’),
loginCount: _parseInt(json[‘login_count’], ‘login_count’),

// JSONの数値は int で来るか double で来るか揺れるため、
// numに一度受けた上で .toDouble() を呼び出し、具象型を double に強制する
satisfactionRate: _parsenum(json[‘satisfaction_rate’], ‘satisfaction_rate’).toDouble(),
);
}

static int _parseInt(dynamic value, String fieldName) {
if (value is int) return value;
if (value is double) {
// 許容する場合のみキャスト。基本は厳格に弾くべき
return value.toInt();
}
if (value is String) {
final parsed = int.tryParse(value);
if (parsed != null) return parsed;
}
throw FormatException(‘Invalid type for field “$fieldName”. Expected int, got ${value.runtimeType}’);
}

static num _parsenum(dynamic value, String fieldName) {
if (value is num) return value;
if (value is String) {
final parsed = num.tryParse(value);
if (parsed != null) return parsed;
}
throw FormatException(‘Invalid type for field “$fieldName”. Expected numeric, got ${value.runtimeType}’);
}
}

/// 【非同期APIクライアントのシミュレーション】
Future> fetchRemoteMetrics() async {
// ネットワーク遅延の模倣
await Future.delayed(const Duration(milliseconds: 100));

// バックエンドの気まぐれで、ある時は int、ある時は double が返ってくるカオスなJSON
return {
‘user_id’: 94821, // int
‘login_count’: 42, // int
‘satisfaction_rate’: 4.85, // double (環境によっては int の 5 で返ることもある)
};
}

/// 【エントリーポイント:フロントエンドのコンポーネント層を想定】
Future main() async {
print(‘=== データの同期と型安全なパースを開始 ===’);

try {
// 1. 非同期API連携
final rawJson = await fetchRemoteMetrics();

// 2. 境界でドメインモデルへ変換(ここで num から具象型へ完全に確定させる)
final metrics = UserMetrics.fromJson(rawJson);

// 3. 実行時最適化された型による安全な処理
// ここでの metrics.satisfactionRate は完全に double 型であることが保証されているため、
// VMは余計な型チェックを行わず、高速にネイティブ演算を行える。
print(‘ユーザーID: ${metrics.userId} (型: ${metrics.userId.runtimeType})’);
print(‘ログイン回数: ${metrics.loginCount} (型: ${metrics.loginCount.runtimeType})’);
print(‘満足度: ${metrics.satisfactionRate} (型: ${metrics.satisfactionRate.runtimeType})’);

// UIコンポーネントへの受け渡し例
renderDashboard(metrics);

day} (FormatException e) {
// 予期せぬ型揺れによるバグを上流で確実にキャッチ
print(‘ [致命的なエラー]: データ構造が不正です -> ${e.message}’);
}
}

void renderDashboard(UserMetrics metrics) {
// 完全に型が保証された世界
if (metrics.satisfactionRate >= 4.0) {
print(‘UI描画: 優秀なパフォーマンスステータスを表示します。’);
} else {
print(‘UI描画: 改善アラートを表示します。’);
}
}

—

3. コードレビューの視点:テクニカルリードからの提言

チームメンバーが書いたコードの中に `num` や `dynamic` が野放しになっていた場合、以下の基準でコードレビューを行ってほしい。

1. 「なぜ `int` でも `double` でもなく `num` なのか?」と問う
大抵の場合、「どっちで来るか分からなかったから」という怠惰が理由である。APIの仕様が曖昧なら、上記コードのようにパース関数を用意し、受け取った瞬間に具象型へ強制変換させよ。
2. ホットパスでの型不確定性を排除する
アニメーションのフレーム計算、Canvasへの描画、大量のデータグリッドのフィルタリング処理などで`num`が使われていないかチェックせよ。JIT/AOTの最適化を殺し、フレームレート低下(Jank)の主原因となる。
3. 明示的な `.toInt()` / `.toDouble()` の活用
もし計算の過程で型が混ざる可能性があるなら、演算子を使う前に明示的なキャストを行い、コードを読む人間に対しても「この変数はここで確実にこの型になる」という意図を伝えること。

まとめ

Dartの`num`型は、一見すると柔軟で便利な型に見える。しかし、その利便性と引き換えに、「実行時型判定のコスト」「コンパイラ最適化の阻害」「型安全性の崩壊」という重い代償を支払うことになる。

プロダクションコードの品質を極限まで高め、予測可能で堅牢なシステムを構築したいのであれば、「境界でパースし、内部では `int` と `double` を厳格に使い分ける」。この原則をチーム全体の共通認識として徹底してほしい。

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