内容

Dart 中的数字

Dart 应用程序通常针对多个平台。例如,Flutter 应用程序可能针对 iOS、Android 和 Web。只要应用程序不依赖于特定于平台的库或以特定于平台的方式使用数字,代码就可以相同。

此页面详细介绍了本机数字实现和 Web 数字实现之间的差异,以及如何编写代码以消除这些差异的影响。

Dart 数字表示

#

在 Dart 中,所有数字都是通用 Object 类型层次结构的一部分,并且有两个具体的、用户可见的数字类型:int(表示整数值)和 double(表示分数值)。

Object is the parent of num, which is the parent of int and double

根据平台的不同,这些数字类型具有不同的隐藏实现。特别是,Dart 有两种非常不同的目标类型进行编译

  • 本机:通常是 64 位移动或桌面处理器。
  • Web:JavaScript 作为主要的执行引擎。

下表显示了 Dart 数字的通常实现方式

表示本机 int本机 doubleWeb intWeb double
64 位带符号二进制补码
64 位浮点数

对于本机目标,你可以假设 int 映射到带符号 64 位整数表示,double 映射到与底层处理器匹配的 64 位 IEEE 浮点表示。

但在 Web 上,Dart 编译为 JavaScript 并与之互操作,存在一个数字表示:64 位双精度浮点值。为了提高效率,Dart 将 intdouble 都映射到此单个表示。可见的类型层次结构保持不变,但底层的隐藏实现类型不同且相互交织。

下图说明了本机和 Web 目标的特定于平台的类型(蓝色)。如图所示,本机上 int 的具体类型仅实现 int 接口。但是,Web 上 int 的具体类型同时实现了 intdouble

Implementation classes vary by platform; for JavaScript, the class that implements int also implements 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) - 190071992547409919007199254740991
math.pow(2, 53)90071992547409929007199254740992
math.pow(2, 53) + 190071992547409939007199254740992
math.pow(2, 62)46116860184273879044611686018427388000
math.pow(2, 63) - 192233720368547758079223372036854776000
math.pow(2, 63)-92233720368547758089223372036854776000
math.pow(2, 64)018446744073709552000

标识

#

在原生平台上,doubleint 是不同的类型:一个值不能同时是 doubleint。在网络上,情况并非如此。由于这种差异,同一性在不同平台上可能不同,尽管相等性 (==) 不会不同。

下表显示了一些使用相等性和同一性的表达式。相等性表达式在原生平台和网络上是相同的;同一性表达式通常不同。

表达式原生Web
1.0 == 1truetrue
identical(1.0, 1)falsetrue
0.0 == -0.0truetrue
identical(0.0, -0.0)falsetrue
double.nan == double.nanfalsefalse
identical(double.nan, double.nan)truefalse
double.infinity == double.infinitytruetrue
identical(double.infinity, double.infinity)truetrue

类型和类型检查

#

在网络上,底层的 int 类型类似于 double 的子类型:它是一个没有小数部分的双精度值。事实上,网络上对 x is int 形式的类型检查,如果 x 是一个带有零值小数部分的数字 (double),则返回 true。

因此,以下内容在网络上为真

  • 所有 Dart 数字(num 类型的值)都是 double
  • Dart 数字可以同时是 doubleint

这些事实会影响 is 检查和 runtimeType 属性。一个副作用是 double.infinity 被解释为 int。由于这是一个特定于平台的行为,因此它将来可能会改变。

表达式原生Web
1 is inttruetrue
1 is doublefalsetrue
1.0 is intfalsetrue
1.0 is doubletruetrue
(0.5 + 0.5) is intfalsetrue
(0.5 + 0.5) is doubletruetrue
3.14 is intfalsefalse
3.14 is doubletruetrue
double.infinity is intfalsetrue
double.nan is intfalsefalse
1.0.runtimeTypedoubleint
1.runtimeTypeintint
1.5.runtimeTypedoubledouble

按位运算

#

出于网络上的性能原因,int 上的按位 (&|^~) 和移位 (<<>>>>>) 运算符使用原生 JavaScript 等价项。在 JavaScript 中,操作数被截断为 32 位整数,并被视为无符号整数。这种处理可能会对较大的数字产生令人惊讶的结果。特别是,如果操作数为负数或不适合 32 位,则它们很可能在原生平台和网络之间产生不同的结果。

下表显示了当操作数为负数或接近 32 位时,原生平台和网络平台如何处理按位和移位运算符

表达式原生Web
-1 >> 0-14294967295
-1 ^ 2-34294967293
math.pow(2, 32).toInt()42949672964294967296
math.pow(2, 32).toInt() >> 121474836480
(math.pow(2, 32).toInt()-1) >> 121474836472147483647

字符串表示

#

在网络上,Dart 通常会让 JavaScript 将数字转换为字符串(例如,对于 print)。下表演示了转换第一列中的表达式如何导致不同的结果。

表达式本机 toString()网络 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 代码已经在本机和网络平台上运行多年,并且数字实现差异很少成为问题。常见的典型代码(例如遍历一系列小整数和索引列表)的行为相同。

如果您有比较字符串结果的测试或断言,请以平台弹性方式编写它们。例如,假设您正在测试包含嵌入数字的字符串表达式的值

dart
void main() {
  var count = 10.0 * 2;
  var message = "$count cows";
  if (message != "20.0 cows") throw Exception("Unexpected: $message");
}

前面的代码在本地平台上成功,但在网络上抛出异常,因为在网络上 message"20 cows"(没有小数)。作为替代,您可以按如下方式编写条件,以便在本地和网络平台上都通过

dart
if (message != "${20.0} cows") throw ...

对于位操作,请考虑显式操作 32 位块,这些块在所有平台上都一致。要强制解释 32 位块的带符号,请使用 int.toSigned(32)

对于精度很重要的其他情况,请考虑其他数字类型。BigInt 类型在本地和网络上都提供任意精度的整数。fixnum 包即使在网络上也提供严格的 64 位带符号数字。但是,请谨慎使用这些类型:它们通常会导致代码明显更大且更慢。