Dart 中的数字
Dart 应用通常面向多个平台。例如,一个 Flutter 应用可能面向 iOS、Android 和 Web。只要应用不依赖于特定平台的库或以平台相关的方式使用数字,代码就可以是相同的。
本页详细介绍了原生和 Web 数字实现之间的差异,以及如何编写代码使这些差异不再重要。
Dart 数字表示
#在 Dart 中,所有数字都是公共的 Object
类型层次结构的一部分,有两个具体、用户可见的数值类型:int
,表示整数值;以及 double
,表示小数(分数)值。
根据平台的不同,这些数值类型具有不同的、隐藏的实现。特别地,Dart 编译目标分为两类,差异很大:
- 原生: 最常见的是 64 位移动或桌面处理器。
- Web: 将 JavaScript 作为主要执行引擎。
下表显示了 Dart 数字通常如何实现
表示 | 原生 int | 原生 double | Web int | Web double |
---|---|---|---|---|
64 位有符号二进制补码 | ✅ | |||
64 位浮点数 | ✅ | ✅ | ✅ |
对于原生目标,可以假定 int
映射到有符号 64 位整数表示,而 double
映射到与底层处理器匹配的 64 位 IEEE 浮点表示。
但在 Web 上,Dart 编译为 JavaScript 并与其进行互操作,这里只有一种数字表示:一个 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 时可见。在原生平台上,后者溢出为 -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 上以下表达式为 true
- 所有 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 通常委托给 JavaScript 将数字转换为字符串(例如,用于 print
)。下表演示了将第一列中的表达式转换为字符串可能会导致结果不同。
表达式 | 原生 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 上会抛出异常,因为在 Web 上 message
是 "20 cows"
(没有小数)。作为替代方案,你可以按如下方式编写条件,使其在原生和 Web 平台上都能通过:
if (message != "${20.0} cows") throw ...
对于位操作,考虑明确地对 32 位块进行操作,这在所有平台上都是一致的。要强制将 32 位块解释为有符号数,请使用 int.toSigned(32)
。
对于精度重要的其他情况,考虑使用其他数值类型。 BigInt
类型在原生和 Web 上都提供任意精度整数。fixnum
软件包即使在 Web 上也提供严格的 64 位有符号数。然而,请谨慎使用这些类型:它们通常会导致代码体积更大、运行更慢。