【実務・中級編】Dartの数値型(int, double)の内部表現と、num型を扱う際の注意点 – Dart コア文法・オブジェクト指向・Null安全解析バイブル

Dart数値型の深淵:int, double, numの内部と挙動を掌握する

諸君、君たちは日々、Dartを使ってフロントエンドやバックエンドのロジックを構築しているだろう。その中で、数値型、特に`int`や`double`を何の疑いもなく使ってはいないだろうか? 表面的な理解でコードを書き続けることは、将来的なバグの温床となり、パフォーマンスのボトルネックを生み出す。

私はDart VMの深淵に触れ、AOTコンパイラの挙動を手の内に入れた者として、ただの型定義に留まらない、その内部表現とプラットフォーム間の挙動の差異、そして`num`型がもたらす潜在的な危険性について、君たちに『真の知見』を授けよう。この知識は、堅牢かつ高性能なシステムを設計するための基盤となるはずだ。

1. Dart数値型の階層:numの真の姿

Dartにおいて、全ての数値型は抽象クラス`num`を継承している。その具体的な実装が`int`と`double`だ。

// 抽象クラス num の定義 (概念的な表現)
abstract class num {
// 数値演算のための基本的なインターフェースを定義
// +, -, , /, %, abs(), ceil(), floor() など
}

// 整数型
class int extends num {
// 64ビット符号付き整数として実装
}

// 浮動小数点数型
class double extends num {
// IEEE 754 double-precision floating-point (64ビット) として実装
}

この階層構造自体はシンプルに見えるが、重要なのは`num`が抽象であるという点だ。これは、`num`型の変数に値を代入する際には、必ず`int`か`double`のいずれかの具体的なインスタンスが格納されることを意味する。そして、この「どちらであるか」が、VM内部の挙動、AOTコンパイルの結果、そしてJavaScriptへのトランスパイルにおいて決定的な差を生む。

2. `int`型の内部表現:VMの最適化とWebの罠

`int`型は、一見するとただの整数に過ぎない。しかし、その内部ではDart VMの巧妙な最適化と、Webプラットフォームへの対応における重要な制約が潜んでいる。

2.1. Dart VM上での`int`:SmiとMint

Dart VMは、パフォーマンスを最大化するために`int`型に対して独自の最適化を施している。

  • Smi (Small Integer): ほとんどの一般的な整数値(32ビット環境では約-2^30から2^30-1、64ビット環境では約-2^62から2^62-1の範囲)は、「Smi」として表現される。これは、オブジェクトのポインタ自体に値を埋め込むことで、ヒープアロケーション(メモリ確保)とガベージコレクションのオーバーヘッドを完全に回避する仕組みだ。Smiは非常に高速に扱われる。
  • Mint (Materialized Integer): Smiの範囲を超える大きな整数値は、通常のオブジェクトとしてヒープに確保される。これを「Mint」と呼ぶ。MintはSmiよりも処理にコストがかかるが、Dartは64ビット符号付き整数として、非常に広い範囲(-2^63から2^63-1)の`int`をネイティブにサポートする。

このSmiとMintの切り替えはVMが自動的に行うため、開発者が意識することは少ない。しかし、極端に大きな整数値を頻繁に扱うような計算ロジックでは、Mintの生成がパフォーマンスに影響を与える可能性があることを頭の片隅に入れておくべきだ。

2.2. Webプラットフォームでの`int`:JavaScriptの制約

問題はWebプラットフォームだ。DartコードがJavaScriptにトランスパイルされる際、Dartの`int`はJavaScriptの`Number`型に変換される。

JavaScriptの`Number`型は、IEEE 754 double-precision floating-point format(64ビット浮動小数点数)で数値を表現する。これは、整数も浮動小数点数も区別なく同じ形式で扱うということだ。この仕様には、整数を安全に表現できる範囲に明確な制約がある。

  • JavaScriptで安全な整数範囲: `-(2^53 – 1)` から `2^53 – 1` まで。

これは具体的に `-9,007,199,254,740,991` から `9,007,199,254,740,991` までとなる。

Dart VMがネイティブにサポートする64ビット整数の範囲(約 `±9 10^18`)と比べると、JavaScriptの安全な整数範囲(約 `±9 10^15`)はかなり狭い。この範囲を超える整数をJavaScript上で扱うと、精度が失われ、予期せぬ結果やバグにつながる。

具体例で見てみよう。

void main() {
// Dart VMで安全な範囲の整数
int dartSafeInt = 9007199254740991; // 2^53 – 1
int dartLargeInt = 9007199254740992; // 2^53 (JSで安全ではない境界)
int dartVeryLargeInt = 90071992547409923; // 2^53 を大きく超える

print(‘— Dart VM上でのintの挙動 —‘);
print(‘dartSafeInt (VM): $dartSafeInt’);
print(‘dartLargeInt (VM): $dartLargeInt’);
print(‘dartVeryLargeInt (VM): $dartVeryLargeInt’);
print(‘dartLargeInt == dartSafeInt + 1 (VM): ${dartLargeInt == dartSafeInt + 1}’);

// JavaScriptへのトランスパイルを想定した挙動のシミュレーション
// 実際にはブラウザで実行する必要があるが、ここでは概念的に示す
print(‘\n— JavaScriptへのトランスパイル後のintの挙動 (概念) —‘);
// JavaScriptでは、大きな整数は浮動小数点数として表現されるため精度が失われる
// dartLargeInt の値は 9007199254740992
// JavaScriptでは、この値は 9007199254740992.0 と表現される
// しかし、これを超える値では末尾が丸められる可能性がある
// `Number.MAX_SAFE_INTEGER` が 9007199254740991

// JavaScriptで安全な範囲を超えると、同じ値として扱われる可能性がある
double jsLargeIntEquivalent = 9007199254740992.0; // JS Number型での表現
double jsLargeIntPlusOneEquivalent = 9007199254740993.0; // JS Number型での表現
double jsVeryLargeIntEquivalent = 90071992547409923.0; // JS Number型での表現

print(‘JS Large Int Equivalent: $jsLargeIntEquivalent’);
print(‘JS Large Int + 1 Equivalent: $jsLargeIntPlusOneEquivalent’);
print(‘JS Very Large Int Equivalent: $jsVeryLargeIntEquivalent’);
print(‘JS Large Int Equivalent == JS Large Int + 1 Equivalent (JS): ${jsLargeIntEquivalent == jsLargeIntPlusOneEquivalent}’);
// 期待される出力: false (Dart VM) -> true (JavaScriptで精度が失われた場合)

// DartのBigIntを使うことで、この問題を回避できる
BigInt bigIntSafe = BigInt.from(dartSafeInt);
BigInt bigIntLarge = BigInt.from(dartLargeInt);
BigInt bigIntVeryLarge = BigInt.parse(‘900719925474099234567890’);

print(‘\n— BigIntでの正確な整数表現 —‘);
print(‘BigInt Safe: $bigIntSafe’);
print(‘BigInt Large: $bigIntLarge’);
print(‘BigInt Very Large: $bigIntVeryLarge’);
print(‘BigInt Large == BigInt Safe + 1: ${bigIntLarge == bigIntSafe + BigInt.one}’);
}

/ 実行結果例 (Dart VM)
— Dart VM上でのintの挙動 —
dartSafeInt (VM): 9007199254740991
dartLargeInt (VM): 9007199254740992
dartVeryLargeInt (VM): 90071992547409923
dartLargeInt == dartSafeInt + 1 (VM): true

— JavaScriptへのトランスパイル後のintの挙動 (概念) —
JS Large Int Equivalent: 9007199254740992.0
JS Large Int + 1 Equivalent: 9007199254740993.0
JS Very Large Int Equivalent: 90071992547409920.0 // 最後の桁が0に丸められている!
JS Large Int Equivalent == JS Large Int + 1 Equivalent (JS): true // 精度が失われ、同じと判定される!

— BigIntでの正確な整数表現 —
BigInt Safe: 9007199254740991
BigInt Large: 9007199254740992
BigInt Very Large: 900719925474099234567890
BigInt Large == BigInt Safe + 1: true
/

テクニカルリードからのコメント:
> 君のコードがIDや金額など、大きな整数値を扱うAPIレスポンスを処理する場合、このJavaScriptの制約は致命的なバグにつながる。例えば、`dartVeryLargeInt` のような値がJavaScriptに渡されると、末尾の桁が `0` に丸められてしまい、比較や計算が破綻する。Web向けに開発する場合、`BigInt`型の使用を強く検討しろ。`BigInt`は、いかなる大きさの整数も精度を保って表現できるが、パフォーマンスは`int`よりも劣るため、必要な場合にのみ利用すべきだ。

3. `double`型の内部表現:浮動小数点数の宿命

`double`型は、Dart VM、そしてJavaScript上でも、IEEE 754 double-precision floating-point format(64ビット浮動小数点数)として表現される。これは、小数点以下の数値を扱うための標準的な形式だが、その本質には「精度誤差」という避けられない宿命がつきまとう。

3.1. 精度誤差のメカニズム

浮動小数点数は、有限のビット数で実数を近似的に表現する。この際、特に2進数で正確に表現できない分数(例:1/10)は、どうしても誤差を含んでしまう。

void main() {
double a = 0.1;
double b = 0.2;
double c = 0.3;

print(‘— 浮動小数点数の精度誤差 —‘);
print(‘a = $a’); // 0.1
print(‘b = $b’); // 0.2
print(‘c = $c’); // 0.3

print(‘a + b = ${a + b}’); // 0.30000000000000004
print(‘a + b == c: ${a + b == c}’); // false

// 比較の際には許容誤差を考慮するか、整数に変換して比較する
const double epsilon = 0.0000001; // 許容誤差
print(‘fabs(a + b – c) < epsilon: ${ (a + b - c).abs() < epsilon }'); // true // 金融計算など、厳密な精度が求められる場合の対処法 // 1. 整数で管理する (例: 円単位ではなく、円単位 100 で管理し、表示時に変換) int priceYen = 100; // 100円 int taxRatePercent = 10; // 10% int taxAmount = (priceYen taxRatePercent) ~/ 100; // 整数除算 print('\n--- 整数での厳密な計算 ---'); print('本体価格: $priceYen円, 税率: $taxRatePercent%, 税額: $taxAmount円'); // 2. 外部ライブラリ (例: package:decimal) を利用する // decimal.Decimal は精度を失わずに計算できるが、int/doubleよりもパフォーマンスは低い // import 'package:decimal/decimal.dart'; // var d1 = Decimal.parse('0.1'); // var d2 = Decimal.parse('0.2'); // print('Decimal(0.1) + Decimal(0.2) == Decimal(0.3): ${d1 + d2 == Decimal.parse('0.3')}'); // true } / 実行結果例 --- 浮動小数点数の精度誤差 --- a = 0.1 b = 0.2 c = 0.3 a + b = 0.30000000000000004 a + b == c: false fabs(a + b - c) < epsilon: true --- 整数での厳密な計算 --- 本体価格: 100円, 税率: 10%, 税額: 10円 / テクニカルリードからのコメント:
> 浮動小数点数の比較で `==` を使うのは愚行だ。`0.1 + 0.2 == 0.3` が `false` になるのは、言語仕様ではなくIEEE 754という基盤技術の宿命だ。金融系や精密な科学計算など、厳密な精度が求められる領域では、`double`を直接使った比較や計算は絶対に避けるべきだ。
> 1. 整数型での管理: 可能な限り、最小単位(例: 円ではなく「銭」、ドルではなく「セント」)で`int`として値を保持し、表示時のみ`double`に変換するか、小数点以下を文字列として扱う。
> 2. 外部ライブラリの利用: `package:decimal`のような、任意精度をサポートするライブラリを導入しろ。ただし、パフォーマンスとメモリ使用量には注意が必要だ。

4. `num`型を扱う際の注意点とベストプラクティス

`num`型は抽象クラスであり、`int`と`double`の共通の親だ。一見すると便利に見えるが、多くのケースで`num`型を直接利用することはアンチパターンだ。

4.1. なぜ`num`を避けるべきか

1. 静的解析の恩恵の減少: `num`は`int`と`double`のどちらであるか不明なため、Dartの強力な型推論や静的解析が弱まる。これにより、コンパイル時に検出できたはずの型不一致エラーが実行時まで持ち越されるリスクが高まる。
2. コードの意図の不明瞭さ: 変数が`int`を意図しているのか、`double`を意図しているのかが曖昧になり、コードの可読性や保守性が低下する。
3. 潜在的な実行時エラー: `int`を期待する場所で`double`が渡されたり、その逆の状況で、暗黙の型変換が起こらない場合(例: `int.parse`に`double`を渡す)に実行時エラーとなる。
4. パフォーマンスへの微細な影響 (稀): VMは`num`型の変数に対して、それが`int`か`double`かを実行時にチェックする必要が生じる場合がある。これはSmiの直接操作のような最適化を妨げる可能性があるが、現代のVMではほとんど無視できるレベルであることが多い。しかし、コードの意図を明確にする上で、型を絞り込むことは常に推奨される。

4.2. `num`型を使うべき限定的なシナリオ

`num`型が真価を発揮するのは、以下のようなごく限定的なケースだ。

  • APIレスポンスの受け皿: 外部APIからJSONを受け取る際、数値フィールドが`int`と`double`のどちらで来るか不定な場合。しかし、受け取った後はすぐに適切な型に変換すべきだ。
  • ジェネリックな数値処理: 数値演算を抽象化するようなライブラリにおいて、`int`と`double`の両方に対応する関数を定義する場合。

4.3. 堅牢な設計パターンとプロダクションコード例

APIから受け取った数値が`num`型の場合でも、安全かつ堅牢に扱うためのパターンを提示する。

// 外部APIから取得したデータモデルの例
class Product {
final String id;
final String name;
final num price; // APIによっては整数(100)または浮動小数点数(99.99)が来る可能性がある
final num? quantity; // 数量は省略される可能性があり、intまたはdoubleのどちらでもありうる

Product({required this.id, required this.name, required this.price, this.quantity});

// JSONからProductを生成するファクトリコンストラクタ
factory Product.fromJson(Map json) {
// numで受け取った後、適切な型に変換または検証する
final num? rawPrice = json[‘price’];
final num? rawQuantity = json[‘quantity’];

// priceは必須かつ数値であると仮定。ここではdoubleとして扱う
if (rawPrice == null || rawPrice is! num) {
throw FormatException(‘Invalid or missing price for Product ${json[‘id’]}’);
}
// ここで敢えてdoubleに変換することで、以降の処理で型を固定する
final double productPrice = rawPrice.toDouble();

// quantityはオプションだが、もし存在すればintとして扱うことを意図
int? productQuantity;
if (rawQuantity != null) {
if (rawQuantity is int) {
productQuantity = rawQuantity;
} else if (rawQuantity is double) {
// doubleで来た場合、小数点以下がないか確認し、整数に丸める
if (rawQuantity == rawQuantity.roundToDouble()) {
productQuantity = rawQuantity.toInt();
} else {
// 小数点以下があるdoubleをintとして扱えない場合のエラー処理
throw FormatException(‘Quantity cannot be a fractional number for Product ${json[‘id’]}’);
}
} else {
throw FormatException(‘Invalid quantity type for Product ${json[‘id’]}’);
}
}

return Product(
id: json[‘id’] as String,
name: json[‘name’] as String,
price: productPrice, // ここでdouble型に固定されている
quantity: productQuantity, // ここでint?型に固定されている
);
}

@override
String toString() {
return ‘Product(id: $id, name: $name, price: $price, quantity: $quantity)’;
}
}

void main() {
final apiResponse1 = {
‘id’: ‘P001’,
‘name’: ‘Laptop’,
‘price’: 1200, // int
‘quantity’: 5, // int
};

final apiResponse2 = {
‘id’: ‘P002’,
‘name’: ‘Mouse’,
‘price’: 25.99, // double
};

final apiResponse3 = {
‘id’: ‘P003’,
‘name’: ‘Keyboard’,
‘price’: 75.0, // doubleだが整数値
‘quantity’: 2.0, // doubleだが整数値
};

final apiResponse4 = {
‘id’: ‘P004’,
‘name’: ‘Monitor’,
‘price’: ‘invalid’, // 無効な型
};

final apiResponse5 = {
‘id’: ‘P005’,
‘name’: ‘Desk’,
‘price’: 150.0,
‘quantity’: 1.5, // 小数点以下のある数量
};

try {
final p1 = Product.fromJson(apiResponse1);
print(p1); // Product(id: P001, name: Laptop, price: 1200.0, quantity: 5)
print(‘Type of p1.price: ${p1.price.runtimeType}’); // double
print(‘Type of p1.quantity: ${p1.quantity.runtimeType}’); // int

final p2 = Product.fromJson(apiResponse2);
print(p2); // Product(id: P002, name: Mouse, price: 25.99, quantity: null)
print(‘Type of p2.price: ${p2.price.runtimeType}’); // double

final p3 = Product.fromJson(apiResponse3);
print(p3); // Product(id: P003, name: Keyboard, price: 75.0, quantity: 2)
print(‘Type of p3.quantity: ${p3.quantity.runtimeType}’); // int

Product.fromJson(apiResponse4); // これによりFormatExceptionがスローされる
} on FormatException catch (e) {
print(‘Error processing API response 4: $e’);
}

try {
Product.fromJson(apiResponse5); // これによりFormatExceptionがスローされる
} on FormatException catch (e) {
print(‘Error processing API response 5: $e’);
}
}

/ 実行結果例
Product(id: P001, name: Laptop, price: 1200.0, quantity: 5)
Type of p1.price: double
Type of p1.quantity: int
Product(id: P002, name: Mouse, price: 25.99, quantity: null)
Type of p2.price: double
Product(id: P003, name: Keyboard, price: 75.0, quantity: 2)
Type of p3.quantity: int
Error processing API response 4: FormatException: Invalid or missing price for Product P004
Error processing API response 5: FormatException: Quantity cannot be a fractional number for Product P005
/

テクニカルリードからのコメント:
> `Product.fromJson` の実装をよく見ろ。APIから受け取った`num`をそのまま保持するのではなく、内部では`double`や`int`といった具体的な型に変換し、厳密な型チェックとバリデーションを施している。
>
> `rawPrice.toDouble()`: `int`として来ても`double`として扱うことで、以降の処理で型を統一している。これにより、`price`が常に`double`であることが保証され、コードの意図が明確になる。
> `rawQuantity is int` や `rawQuantity is double` を使った型ガード: これはDartの強力な機能だ。実行時に型をチェックし、そのスコープ内で型を絞り込むことで、安全に`toInt()`などのメソッドを呼び出せる。
> `rawQuantity == rawQuantity.roundToDouble()`: `double`として来た値が、実際には整数値であるかどうかを判定する洗練された方法だ。これにより、`2.0`のような`double`を安全に`int`に変換できる。
>
> このようなアプローチは、コードの堅牢性を劇的に高め、将来的なバグの発生を未然に防ぐ。`num`はあくまで「受け皿」であり、処理の入り口で具体的な型に変換・検証することが、保守性の高いプロダクションコードの鉄則だ。

5. まとめ:型を掌握し、堅牢なシステムを構築せよ

Dartの数値型は、表面的なシンプルさの裏に、VMの最適化、JavaScriptへのトランスパイル時の制約、そして浮動小数点数演算の宿命といった、多層的な複雑性を抱えている。

  • `int`型: Dart VMでは64ビット整数をフルサポートするが、Web(JavaScript)環境では安全な整数範囲が `±2^53` に制限される。この範囲を超える値を扱う場合は、`BigInt`の使用を検討せよ。
  • `double`型: IEEE 754の標準に従うため、精度誤差は避けられない。厳密な計算や比較が必要な場合は、整数型での管理や`package:decimal`のような外部ライブラリを活用しろ。
  • `num`型: 汎用的に見えても、ほとんどのケースでアンチパターンだ。APIレスポンスの受け皿など、限定的な場合を除き、可能な限り`int`か`double`の具体的な型を明示し、型安全なコードを記述しろ。

これらの知見は、単なる言語仕様の解説ではない。これらは、君たちが日々直面するであろうバグ、パフォーマンス問題、そして保守性の低下といった課題を根本から解決するための、『Dartを掌握する極限の知見』だ。

数値型の本質を理解し、その特性を最大限に活かした堅牢な設計を心がけることで、君たちのプロダクトは間違いなく、次のレベルへと飛躍するだろう。コードレビューの場で、私がこれらの点を指摘するまでもなく、君たち自身がその重要性を理解し、実践することを期待する。

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