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

Dartの数値型、intとdoubleの深淵へようこそ!~プラットフォームの壁を越えるための知恵~

皆さん、こんにちは!Dartの世界へようこそ。今回は、プログラミングの基本中の基本でありながら、実は奥深い「数値型」について、じっくりと紐解いていきましょう。特に、`int` (整数型) と `double` (浮動小数点数型) の内部表現、そしてそれらを扱う上で避けては通れない `num` 型の注意点に焦点を当てていきます。

「え、数値なんて他の言語と一緒でしょ?」と思われたそこのあなた!Dartは、その洗練された設計思想ゆえに、数値の扱いにも独特の哲学が息づいています。ここをしっかり理解すれば、あなたのDartコードはより堅牢で、予測可能なものになるはずです。さあ、一緒にDartの数値世界の深淵を覗き込んでみましょう!

1. Dartの数値型:`int` と `double` の基本

まずは、Dartにおける整数の`int`と浮動小数点数の`double`の基本的な使い方からおさらいしましょう。

void main() {
// 整数型 (int) の宣言
int integerValue = 10;
print(‘整数値: $integerValue’); // 出力: 整数値: 10

// 浮動小数点数型 (double) の宣言
double doubleValue = 3.14;
print(‘浮動小数点値: $doubleValue’); // 出力: 浮動小数点値: 3.14

// 整数リテラルでも、小数点以下があればdoubleになる
double inferredDouble = 5.0;
print(‘推論されたdouble値: $inferredDouble, 型: ${inferredDouble.runtimeType}’); // 出力: 推論されたdouble値: 5.0, 型: double

// 整数型に小数点以下を含む値を代入しようとするとエラー
// int errorInt = 2.5; // これはコンパイルエラーになります!
}

ご覧のように、Dartでは `int` と `double` を明確に区別して宣言します。`int` には整数のみ、`double` には小数点を含む数値を格納できます。

2. `var`, `final`, `const` と数値型

変数宣言でよく使う `var`, `final`, `const` と数値型を組み合わせたときの挙動も確認しておきましょう。

void main() {
// var: 型推論され、再代入可能
var count = 5; //Dartはcountをint型だと推論します
print(‘var count: $count, 型: ${count.runtimeType}’); // 出力: var count: 5, 型: int
count = 10; // 再代入OK
print(‘再代入後のvar count: $count’); // 出力: 再代入後のvar count: 10

// final: 一度だけ値が設定され、その後は変更不可
final String name = ‘Dart’;
print(‘final name: $name’); // 出力: final name: Dart
// name = ‘Flutter’; // これはコンパイルエラーになります! (final変数には再代入できません)

// const: コンパイル時に値が決定され、変更不可。静的定数
const double PI = 3.14159;
print(‘const PI: $PI’); // 出力: const PI: 3.14159
// PI = 3.14; // これはコンパイルエラーになります! (const変数には再代入できません)

// constはコンパイル時定数なので、計算結果をconstにできます
const int twoTimesTwo = 2 2;
print(‘const twoTimesTwo: $twoTimesTwo’); // 出力: const twoTimesTwo: 4

// finalとconstの違い:
// final変数は実行時に値が決まり、その値は不変になります。
// const変数はコンパイル時に値が決まり、その値は不変になります。
// 実行時まで値が確定しない可能性がある場合はfinalを使います。
// 例: final dateTimeNow = DateTime.now(); // 実行時まで値は不明

// constに実行時値は代入できません
// const runtimeValue = DateTime.now(); // これはコンパイルエラーになります!
}

  • `var`: 型推論され、後から値を変更できます。動的な値の格納に適しています。
  • `final`: 一度だけ値が設定され、その後は変更できません。実行時に値が決まる場合でも使用できます。
  • `const`: コンパイル時に値が決定され、変更できません。プログラムの定数として扱われます。

3. Dartの数値型の「内部表現」:ここが肝心!

さて、ここからが本題です。Dartの `int` と `double` は、内部でどのように表現されているのでしょうか?そして、それが私たちのコードにどう影響するのでしょうか?

3.1. `int` 型:プラットフォーム依存の「整数」

Dartの `int` 型は、他の多くの言語とは異なり、プラットフォームに依存した整数表現を持ちます。

  • 64-bit プラットフォーム (ほとんどのモダンなデスクトップ、サーバー、モバイルデバイス): 64ビットの符号付き整数として表現されます。これは、 `-2^63` から `2^63 – 1` までの範囲をカバーします。
  • 32-bit プラットフォーム (一部の古いデバイスや特定の組み込み環境): 32ビットの符号付き整数として表現されます。これは、 `-2^31` から `2^31 – 1` までの範囲をカバーします。

このプラットフォーム依存性、一体どういうことかというと、非常に大きな整数を扱う場合に、ターゲットとなるプラットフォームによって扱える範囲が変わってくる可能性があるということです。

void main() {
// 64-bit integer の最大値に近い値
int largeInt = 9223372036854775807; // 2^63 – 1
print(’64-bit int (max): $largeInt’);

// 64-bit integer の最小値に近い値
int smallInt = -9223372036854775808; // -2^63
print(’64-bit int (min): $smallInt’);

// 32-bit integer の最大値
int maxInt32 = 2147483647; // 2^31 – 1
print(’32-bit int (max): $maxInt32′);

// 32-bit integer の最小値
int minInt32 = -2147483648; // -2^31
print(’32-bit int (min): $minInt32′);

// Dart VM は通常 64-bit なので、ここでは 64-bit として扱われます。
// しかし、もし 32-bit 環境でコンパイル・実行された場合、
// maxInt32 より大きい値を代入しようとするとオーバーフローする可能性があります。
// 例: int overflowInt = maxInt32 + 1; (32-bit 環境では問題になる可能性)
}

ポイント: ほとんどの開発環境では64-bitなので、普段意識することは少ないかもしれません。しかし、特に組み込みシステムや、非常に大きな数値を扱う必要がある場合は、このプラットフォーム依存性を念頭に置く必要があります。

3.2. `double` 型:IEEE 754 標準の「倍精度浮動小数点数」

一方、`double` 型は、プラットフォームに依存せず、IEEE 754 標準の倍精度浮動小数点数として扱われます。これは、ほとんどのプログラミング言語で浮動小数点数を表現する際のデファクトスタンダードです。

void main() {
// double 型は IEEE 754 倍精度浮動小数点数で表現されます。
// これは、科学計算などで一般的に使用される形式です。

double preciseDouble = 0.1 + 0.2;
print(‘0.1 + 0.2 = $preciseDouble’); // 出力: 0.1 + 0.2 = 0.30000000000000004

// あれ?0.3 にならない…?
// これは浮動小数点数表現の「誤差」によるものです。
// 2進数で正確に表現できない小数が存在するため、ごくわずかな誤差が生じることがあります。

// 比較する際は、誤差を考慮した範囲で行うのが一般的です。
double epsilon = 0.000000000000001; // 非常に小さな値 (許容誤差)
if ((preciseDouble – 0.3).abs() < epsilon) { print('0.1 + 0.2 は、ほぼ 0.3 とみなせます。'); } else { print('0.1 + 0.2 は、0.3 とは異なると判定されました。'); } } IEEE 754 の注意点:

  • 精度の限界: `double` 型は、非常に広い範囲の数値を表現できますが、その精度には限界があります。特に、`0.1 + 0.2` が `0.3` にならないような、ごくわずかな誤差が生じることがあります。これは、2進数では `0.1` や `0.2` を正確に表現できないためです。
  • 比較の難しさ: 浮動小数点数同士を直接 `==` で比較すると、期待通りの結果にならないことがあります。比較する際は、許容できる誤差の範囲 (`epsilon`) を設けて、その範囲内であれば等しいとみなすのが一般的です。

3.3. Dartにおける「無限大」と「非数 (NaN)」

`double` 型には、特殊な値として「無限大」と「非数 (NaN: Not a Number)」も存在します。

void main() {
// 無限大 (Infinity)
double positiveInfinity = double.infinity;
print(‘正の無限大: $positiveInfinity’); // 出力: 正の無限大: Infinity

double negativeInfinity = double.negativeInfinity;
print(‘負の無限大: $negativeInfinity’); // 出力: 負の無限大: -Infinity

// ゼロ除算で発生
print(’10 / 0 = ${10 / 0}’); // 出力: 10 / 0 = Infinity
print(‘-10 / 0 = ${-10 / 0}’); // 出力: -10 / 0 = -Infinity

// 非数 (NaN: Not a Number)
double nanValue = double.nan;
print(‘NaN: $nanValue’); // 出力: NaN: NaN

// 不定な計算結果で発生
print(‘0 / 0 = ${0 / 0}’); // 出力: 0 / 0 = NaN
print(‘sqrt(-1) = ${(-1.0).sqrt()}’); // 出力: sqrt(-1) = NaN

// NaN は自分自身とも等しくない!
print(‘NaN == NaN: ${nanValue == nanValue}’); // 出力: NaN == NaN: false

// NaN かどうかを判定するには isNaN プロパティを使う
print(‘isNaN(nanValue): ${nanValue.isNaN}’); // 出力: isNaN(nanValue): true
print(‘isNaN(positiveInfinity): ${positiveInfinity.isNaN}’); // 出力: isNaN(positiveInfinity): false
}

  • 無限大 (`double.infinity`, `double.negativeInfinity`): 非常に大きな数や、ゼロ除算などで発生します。
  • 非数 (`double.nan`): `0 / 0` のような不定な計算結果や、負の数の平方根などを計算した際に発生します。NaN は自分自身とも等しくないという、非常に特殊な性質を持っています。NaNかどうかを判定するには、`isNaN` プロパティを使用します。

4. `num` 型:数値型への「抽象化」

Dartには、`int` と `double` の両方を扱うことができる `num` 型という抽象的な型も用意されています。

void main() {
// num 型は int または double のどちらかになれます。
num number1 = 10; // int 型になる
print(‘number1: $number1, 型: ${number1.runtimeType}’); // 出力: number1: 10, 型: int

num number2 = 3.14; // double 型になる
print(‘number2: $number2, 型: ${number2.runtimeType}’); // 出力: number2: 3.14, 型: double

// num 型の変数に、int と double の両方を代入できます。
number1 = 20.5; // double 型に変わる
print(‘number1 (再代入後): $number1, 型: ${number1.runtimeType}’); // 出力: number1 (再代入後): 20.5, 型: double

// num 型は int と double の共通のメソッド(abs, toString など)を使えます。
num value = -5;
print(‘abs(-5): ${value.abs()}’); // 出力: abs(-5): 5

value = -2.7;
print(‘abs(-2.7): ${value.abs()}’); // 出力: abs(-2.7): 2.7

// num 型は int と double の演算結果を保持できます。
num result = 10 1.5; // double 型になります
print(’10 1.5 = $result, 型: ${result.runtimeType}’); // 出力: 10 1.5 = 15.0, 型: double

result = 10 ~/ 3; // int 型になります (~/ は整数除算)
print(’10 ~/ 3 = $result, 型: ${result.runtimeType}’); // 出力: 10 ~/ 3 = 3, 型: int
}

`num` 型を扱う際の注意点:

1. 型推論の挙動: `var` や `final` で `num` 型として宣言した場合、最初に代入された値の型(`int` または `double`)に固定されます。後から異なる型の数値を代入しようとすると、型エラーになる場合があります。

void main() {
var numVar = 10; // numVar は int 型と推論されます
print(numVar.runtimeType); // 出力: int

// numVar = 3.14; // これはコンパイルエラーになります! (int 型の変数に double は代入できません)

num numDynamic = 10; // numDynamic は num 型ですが、値は int
print(numDynamic.runtimeType); // 出力: int
numDynamic = 3.14; // これは OK! num 型は double にもなれます。
print(numDynamic.runtimeType); // 出力: double
}

このように、`var numVar = 10;` と書いた場合、`numVar` は `int` 型として扱われ、後から `double` を代入しようとするとエラーになります。`num numDynamic = 10;` と書いた場合は `num` 型として宣言されており、後から `double` を代入することができます。

2. メソッドの実行: `num` 型の変数に対してメソッドを呼び出す場合、そのメソッドが `num` 型自体に定義されているか、または `int` と `double` の両方に共通して定義されている必要があります。例えば、`isEven` のような `int` 型固有のプロパティは、`num` 型の変数(たとえそれが `int` 型の値を持っていても)からは直接呼び出せません。

void main() {
num myNum = 5; // 値は int

// myNum.isEven; // これはコンパイルエラーになります!
// num 型には isEven プロパティは定義されていません。

// 型を明示的に確認・キャストすれば呼び出せます。
if (myNum is int) {
print(‘myNum は int です。 isEven: ${(myNum as int).isEven}’); // 出力: myNum は int です。 isEven: false
}

print(‘myNum.toString(): ${myNum.toString()}’); // 出力: myNum.toString(): 5
}

`myNum.toString()` は `num` 型に定義されているため、問題なく実行できます。`isEven` のような `int` 型固有の機能を使いたい場合は、`as int` のように明示的に型キャストする必要があります。

3. パフォーマンス: `num` 型は `int` や `double` よりも抽象度が高いため、わずかにパフォーマンスのオーバーヘッドが生じる可能性があります。パフォーマンスが非常に重要な場面では、可能な限り `int` や `double` を直接使用することが推奨されます。

5. 遭遇しやすいエラーとその回避策

5.1. 浮動小数点数の比較誤差

void main() {
double val1 = 0.1 + 0.2;
double val2 = 0.3;

// 直接比較は危険!
if (val1 == val2) {
print(‘誤差がない場合: val1 は val2 と等しい’);
} else {
print(‘誤差がある場合: val1 は val2 と等しくない (val1: $val1)’); // こちらが出力される
}

// 回避策: 許容誤差 (epsilon) を設ける
double epsilon = 0.000000000000001; // 非常に小さな値
if ((val1 – val2).abs() < epsilon) { print('許容誤差内: val1 は val2 とほぼ等しい'); // こちらが出力される } } 解説:
`0.1 + 0.2` が `0.3` にならないのは、前述の通り浮動小数点数の2進数表現の限界によるものです。直接比較すると、ごくわずかな差で「等しくない」と判定されてしまうことがよくあります。
回避策: 比較したい2つの値の差の絶対値が、あらかじめ定義した非常に小さな値(`epsilon`)より小さい場合に、それらを等しいとみなします。

5.2. `num` 型での型エラー

void main() {
num quantity = 10; // quantity は int 型として扱われる

// quantity = 5.5; // これは OK

// quantity.isOdd; // これはコンパイルエラー! num 型には isOdd はない
// print(‘quantity は奇数か? ${quantity.isOdd}’);

// 回避策: 型チェックとキャスト
if (quantity is int) {
print(‘quantity は整数です。 isOdd: ${quantity.isOdd}’); // 奇数かどうかを判定
} else {
print(‘quantity は整数ではありません。’);
}

// 別の例: int 型として確定させたい場合
int intQuantity = 10;
// intQuantity = 5.5; // コンパイルエラー
print(‘intQuantity の isOdd: ${intQuantity.isOdd}’); // OK
}

解説:
`num` 型は便利ですが、その曖昧さが型エラーの原因となることがあります。`num` 型の変数に `int` 型固有のメソッド(`isOdd`, `isEven` など)を直接呼び出そうとすると、Dartコンパイラは「`num` 型にはそんなメソッドはないよ!」と怒ります。
回避策: `is` 演算子で型をチェックし、必要であれば `as` キーワードで明示的に型キャストを行います。もし、数値が常に整数であることが確実なのであれば、最初から `int` 型で宣言する方が安全です。

6. まとめ:Dartの数値をマスターする鍵

今回は、Dartの `int` と `double` の内部表現、そして `num` 型の注意点について深掘りしてきました。

  • `int` 型はプラットフォーム依存の整数表現を持つため、極端に大きな数を扱う場合は注意が必要です。
  • `double` 型はIEEE 754標準ですが、浮動小数点数特有の誤差が生じるため、比較には注意が必要です。
  • `num` 型は `int` と `double` の両方を扱えますが、型推論の挙動やメソッド呼び出しには気をつける必要があります。

これらの特性を理解することで、あなたのDartコードはより堅牢で、意図した通りに動作するようになるはずです。特に、浮動小数点数の比較や、`num` 型の型推論には、開発の現場で頻繁に遭遇するポイントですので、ぜひ今回学んだことを意識してみてください。

ここをクリアすれば、Dartの数値演算の基本はバッチリマスターできますよ!これからもDartの旅を一緒に楽しみましょう!

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