こんにちは!FlutterやDartの開発現場を楽しんでいますか?
他のプログラミング言語、例えばJavaScriptやPythonなどからDartの世界に入ってきたとき、「数値を扱うなら、とりあえず `num` 型にしておけば何でも入って便利だな」と思って使っていませんか? あるいは、動的言語のノリで変数に `num` を指定して安心している姿を見かけることがあります。
実は、その何気ない `num` 型の選択が、Dartの裏側で動くJIT/AOTコンパイラやDart VMに余計な負荷をかけ、パフォーマンスを密かに削り取っているとしたら……?
今回は、Dartの数値型の裏側にある「実行時型判定のコスト」と、なぜ私たちが `int` や `double` へ明示的にキャスト(あるいは型指定)すべきなのかを、Dartのコアを知り尽くした視点から優しく、そして深く紐解いていきます。
ここをクリアすれば、Dartの型システムの本質とパフォーマンスチューニングの基本はバッチリマスターできますよ!
—
1. `num` 型とは何者か?(おさらいと基本)
Dartにおいて、整数の `int` と浮動小数点数の `double` は、どちらも `num` という抽象的なスーパークラスを継承しています。
図解すると、このようなツリー構造になっています。
[ num (抽象クラス) ]
├── int (整数)
└── double (浮動小数点数)
そのため、変数に `num` と宣言しておけば、次のように整数も小数も自由に入れることができます。
void main() {
num score = 42; // intが代入される
print(score); // 42
score = 3.14; // doubleが代入される
print(score); // 3.14
}
「おっ、一つの変数で整数も小数も扱えて柔軟で便利じゃないか!」と思いましたよね?
はい、文法上はエラーになりませんし、動くには動きます。しかし、ここからがDartの実行エンジン(Dart VM)の裏側の世界の話です。
—
2. なぜ `num` 型はパフォーマンスのボトルネックになるのか?
Dartは、非常に強力な型システムを持ち、AOT(Ahead-Of-Time)コンパイルによってネイティブコードに変換されることで爆速で動作します。コンパイル時に「この変数は絶対に `int` だ」とわかっていれば、CPUのネイティブなレジスタや演算器をダイレクトに叩くことができます。
しかし、変数が `num` 型である場合、コンパイラやDart VMはこう考えざるを得なくなります。
> 「この変数は `int` なのか、それとも `double` なのか、実行してみるまで分からないぞ……」
これが、実行時型判定(Runtime Type Check / Dispatch)のコストです。
実行時コストの正体
`num` 型の変数に対して足し算などの演算を行うとき、Dart VMは「今、中に入っているのは整数なのか小数なのか?」を実行時に判断し、整数の演算回路を使うべきか、浮動小数点数の演算回路を使うべきかをディスパッチ(振り分け)しなければなりません。
さらに、FlutterのUI描画やゲームループのように、1秒間に60回〜120回(60fps / 120fps)画面を更新するような極限のパフォーマンスが求められる場面で、無駄な型チェックやボクシング(数値などをオブジェクトとしてラップする処理)が発生すると、フレームドロップ(カクつき)の原因になります。
—
3. 陥りやすい罠:APIからのJSONレスポンス
実務で最もやりがちなのが、バックエンド(サーバー)から送られてきたJSONデータを処理する場面です。
// サーバーから受け取ったと仮定するJSONマップ
Map
void main() {
// 動的に取得した数値をとりあえず num で受けてしまう
num price = jsonResponse[‘price’];
// ここで int にしたいが、そのまま計算に使ってしまう
var taxIncluded = price 1.1;
print(taxIncluded);
}
このコード、一見すると何の問題もなさそうに見えます。しかし、大規模なデータ処理や、ミリ秒単位で処理速度を競うアルゴリズムの中では、`num` を経由することによる型推論の曖昧さが最適化の邪魔をします。
安全かつ高速に `int` や `double` へキャストする
もしJSONや外部入力から数値を受け取る場合や、一時的に `num` にならざるを得ない場合は、早い段階で明示的な型変換(キャスト)を行いましょう。
void main() {
Map
// 1. 取得した時点で、具体的な型(double)に確定(キャスト)させる
// as double を使うか、あるいは各型が持つパースメソッドを使う
double price = jsonResponse[‘price’] as double;
// 2. 整数であることが確実なら int に寄せる
int id = jsonResponse[‘id’] as int;
// これにより、以降の演算は完全に型が確定し、
// DartのAOTコンパイラが限界までコードを最適化(インライン化など)できる
var finalPrice = price 1.1;
print(‘ID: $id, Price: $finalPrice’);
}
—
4. コンパイル時定数 (`const`) との決定的な違い
もう一つ、Dartを極める上で絶対に知っておいてほしいのが、`const` との組み合わせです。
`const` は「コンパイル時に値が完全に決まっていなければならない」という強力な制約を持っています。もし `num` 型の変数に `const` を付けようとすると、コンパイラは怒り出します。
// ❌ コンパイルエラーになる例
// const num value = 10;
// 理由:num型では「それがintなのかdoubleなのか」がコンパイル時に一意に定まらないため、
// constの要件を満たせない。
必ず次のように、具体的な型を明示する必要があります。
// ⭕️ 正しい書き方
const int maxRetryCount = 3; // 完全にintとして確定
const double piValue = 3.1415926535; // 完全にdoubleとして確定
型を明確にすることは、メモリの効率的な配置(メモリレイアウトの最適化)にも直結します。Dartのコンパイラに「これは純粋な整数だ」と伝えることは、エンジニアからコンパイラへの最高のプレゼントなのです。
—
まとめ:今日のまとめと実践のポイント
- `num` 型は万能薬ではない: `int` と `double` の親クラスであり便利に見えるが、実行時型判定のコストを発生させる原因になる。
- パフォーマンスへの影響: 頻繁に呼び出されるループやUIの描画処理内では、`num` の使用を避け、最初から `int` や `double` を指定する。
- 境界でのキャスト: APIレスポンスなどで型が曖昧なデータを受け取った際は、放置せずに速やかに `as int` や `as double` で型を確定(キャスト)させる。
- コンパイル時の最適化: 型を明確にすることで、DartのAOTコンパイラが最大限に最適化を行える環境を作る。
「動くからいいや」ではなく、「なぜその型を選ぶのか」を意識できるようになると、あなたの書くDart/Flutterコードは見違えるほど洗練され、堅牢で高速になります。
今日から変数宣言をする際、`num` を使いたくなったら一度立ち止まり、「これは `int` か? それとも `double` か?」と自分に問いかけてみてくださいね。
あなたのDartマスターへの旅を、これからも応援しています!