コードレビューの最中、プロダクトの根幹を成すデータ処理レイヤーで次のようなコードを見かけたとしよう。
// どこかのAPIから取得した不敵な数値データ
num parseScore(Map
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
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
// バックエンドの気まぐれで、ある時は int、ある時は double が返ってくるカオスなJSON
return {
‘user_id’: 94821, // int
‘login_count’: 42, // int
‘satisfaction_rate’: 4.85, // double (環境によっては int の 5 で返ることもある)
};
}
/// 【エントリーポイント:フロントエンドのコンポーネント層を想定】
Future
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` を厳格に使い分ける」。この原則をチーム全体の共通認識として徹底してほしい。