Dart の数値
Dart アプリは、複数のプラットフォームを対象とすることがよくあります。たとえば、Flutter アプリは iOS、Android、Web を対象とする場合があります。アプリがプラットフォーム固有のライブラリに依存しない限り、または数値の使い方がプラットフォームに依存しない限り、コードは同じでかまいません。
このページでは、ネイティブと Web の数値実装の違い、およびそれらの違いが重要にならないようにコードを記述する方法について説明します。
Dart の数値表現
#Dart では、すべての数値は共通の Object 型階層の一部であり、ユーザーが直接認識できる数値型は 2 つあります。整数値を表す int と、浮動小数点値を表す double です。
プラットフォームによっては、これらの数値型は異なる、隠された実装を持ちます。特に、Dart はコンパイルされる 2 つの非常に異なる種類のターゲットがあります。
- ネイティブ: 最も一般的なのは、64 ビットのモバイルまたはデスクトッププロセッサです。
- Web: 主な実行エンジンとして JavaScript。
次の表は、Dart の数値が通常どのように実装されているかを示しています。
| 表現 | ネイティブ int | ネイティブ double | Web int | Web double |
|---|---|---|---|---|
| 64 ビット符号付き 2 の補数 | ✅ | |||
| 64 ビット浮動小数点 | ✅ | ✅ | ✅ |
ネイティブターゲットの場合、int は符号付き 64 ビット整数表現に、double は基盤となるプロセッサに一致する 64 ビット IEEE 浮動小数点表現にマッピングされると想定できます。
ただし、Web では、Dart は JavaScript にコンパイルされ、JavaScript と相互運用するため、数値表現は 1 つだけです。それは 64 ビット倍精度浮動小数点値です。効率化のため、Dart は int と double の両方をこの単一の表現にマッピングします。表示される型階層は同じですが、基盤となる隠された実装型は異なり、絡み合っています。
次の図は、ネイティブおよび Web ターゲットのプラットフォーム固有の型 (青色) を示しています。図が示すように、ネイティブの int の具象型は int インターフェイスのみを実装します。しかし、Web の int の具象型は int と double の両方を実装します。
Web 上の int は、小数部分のない倍精度浮動小数点値として表現されます。実際には、これはかなりうまく機能します。倍精度浮動小数点数は 53 ビットの整数精度を提供します。ただし、int 値は常に double 値でもあるため、いくつかの予期しない結果につながる可能性があります。
動作の違い
#ほとんどの整数および浮動小数点演算は、本質的に同じ動作をします。ただし、特にコードが精度、文字列フォーマット、または基盤となる実行時型について厳密な期待を持っている場合には、重要な違いがあります。
このセクションで説明されているように、算術結果が異なる場合、その動作はプラットフォーム固有であり、変更される可能性があります。
精度
#次の表は、精度が原因で一部の数値式がどのように異なるかを示しています。ここでは、math は dart:math ライブラリを表し、math.pow(2, 53) は 253 です。
Web では、整数は 53 ビットを超えると精度が失われます。特に、253 と 253+1 は切り捨てのため同じ値にマッピングされます。ネイティブでは、ネイティブ数値は 64 ビット (値に 63 ビット、符号に 1 ビット) を持つため、これらの値を区別できます。
オーバーフローの影響は、263-1 と 263 を比較するとわかります。ネイティブでは、後者は 2 の補数演算で予想されるように -263 にオーバーフローします。Web では、これらの値は異なる方法で表現されるため、オーバーフローしません。それらは精度の低下による近似値です。
| 式 | ネイティブ | Web |
|---|---|---|
math.pow(2, 53) - 1 | 9007199254740991 | 9007199254740991 |
math.pow(2, 53) | 9007199254740992 | 9007199254740992 |
math.pow(2, 53) + 1 | 9007199254740993 | 9007199254740992 |
math.pow(2, 62) | 4611686018427387904 | 4611686018427388000 |
math.pow(2, 63) - 1 | 9223372036854775807 | 9223372036854776000 |
math.pow(2, 63) | -9223372036854775808 | 9223372036854776000 |
math.pow(2, 64) | 0 | 18446744073709552000 |
同一性
#ネイティブプラットフォームでは、double と int は異なる型です。どの値も同時に double と int の両方になることはできません。Web では、これは真実ではありません。この違いのため、等価性 (==) はそうではありませんが、同一性はプラットフォーム間で異なる場合があります。
次の表は、等価性と同一性を使用するいくつかの式を示しています。等価性式はネイティブと Web で同じです。同一性式は通常異なります。
| 式 | ネイティブ | Web |
|---|---|---|
1.0 == 1 | true | true |
identical(1.0, 1) | false | true |
0.0 == -0.0 | true | true |
identical(0.0, -0.0) | false | true |
double.nan == double.nan | false | false |
identical(double.nan, double.nan) | true | false |
double.infinity == double.infinity | true | true |
identical(double.infinity, double.infinity) | true | true |
型と型チェック
#Web では、基盤となる int 型は double のサブタイプのようなものです。小数部分のない倍精度値です。実際、Web 上での x is int という形式の型チェックは、x がゼロ値の小数部分を持つ数値 (double) の場合に true を返します。
その結果、Web では次のようになります。
- すべての Dart 数値 (
num型の値) はdoubleです。 - Dart 数値は、同時に
doubleとintの両方になることができます。
これらの事実は、is チェックと runtimeType プロパティに影響します。副作用として、double.infinity は int として解釈されます。これはプラットフォーム固有の動作であるため、将来変更される可能性があります。
| 式 | ネイティブ | Web |
|---|---|---|
1 is int | true | true |
1 is double | false | true |
1.0 is int | false | true |
1.0 is double | true | true |
(0.5 + 0.5) is int | false | true |
(0.5 + 0.5) is double | true | true |
3.14 is int | false | false |
3.14 is double | true | true |
double.infinity is int | false | true |
double.nan is int | false | false |
1.0.runtimeType | double | int |
1.runtimeType | int | int |
1.5.runtimeType | double | double |
ビット演算
#Web 上のパフォーマンス上の理由から、int のビット演算 (&、|、^、~) およびシフト (<<、>>、>>>) 演算子は、ネイティブ JavaScript の同等物を使用します。JavaScript では、オペランドは 32 ビット整数に切り捨てられ、符号なしとして扱われます。この扱いは、大きな数値で予期しない結果を招く可能性があります。特に、オペランドが負の値であったり、32 ビットに収まらない場合、ネイティブと Web の間で異なる結果を生成する可能性が高くなります。
次の表は、オペランドが負の値または 32 ビットに近い場合、ネイティブプラットフォームと Web プラットフォームがビット演算およびシフト演算子をどのように処理するかを示しています。
| 式 | ネイティブ | Web |
|---|---|---|
-1 >> 0 | -1 | 4294967295 |
-1 ^ 2 | -3 | 4294967293 |
math.pow(2, 32).toInt() | 4294967296 | 4294967296 |
math.pow(2, 32).toInt() >> 1 | 2147483648 | 0 |
(math.pow(2, 32).toInt()-1) >> 1 | 2147483647 | 2147483647 |
文字列表現
#Web では、Dart は通常、数値を文字列に変換する (たとえば、print の場合) ことを JavaScript に委譲します。次の表は、最初の列の式を変換すると異なる結果につながる可能性があることを示しています。
| 式 | ネイティブ toString() | Web toString() |
|---|---|---|
1 | "1" | "1" |
1.0 | "1.0" | "1" |
(0.5 + 0.5) | "1.0" | "1" |
1.5 | "1.5" | "1.5" |
-0 | "0" | "-0.0" |
math.pow(2, 0) | "1" | "1" |
math.pow(2, 80) | "0" | "1.2089258196146292e+24" |
どうすべきか?
#通常、数値コードを変更する必要はありません。Dart コードは長年、ネイティブと Web の両方のプラットフォームで実行されており、数値実装の違いが問題になることはほとんどありません。範囲の小さい整数の反復処理やリストのインデックス付けなど、一般的で典型的なコードは同じように動作します。
文字列結果を比較するテストまたはアサーションがある場合は、プラットフォームに耐性のある方法で記述してください。たとえば、数値が埋め込まれた文字列式の値をテストしているとします。
void main() {
var count = 10.0 * 2;
var message = "$count cows";
if (message != "20.0 cows") throw Exception("Unexpected: $message");
}前のコードはネイティブプラットフォームでは成功しますが、Web では message が Web 上で「20 cows」(小数点なし) になるため、エラーになります。代替として、次のように条件を記述すると、ネイティブと Web の両方のプラットフォームで合格します。
if (message != "${20.0} cows") throw ...ビット操作については、すべてのプラットフォームで一貫性のある 32 ビットチャンクで明示的に操作することを検討してください。32 ビットチャンクの符号付き解釈を強制するには、int.toSigned(32) を使用します。
精度が重要なその他のケースでは、他の数値型を検討してください。BigInt 型は、ネイティブと Web の両方で任意の精度の整数を提供します。fixnum パッケージは、Web 上でも厳密な 64 ビット符号付き数値を提供します。ただし、これらの型は慎重に使用してください。しばしば、大幅に大きく遅いコードになります。