Saltar al contenido principal

Números en Dart

Aprende cómo los números de Dart son ligeramente diferentes en la web, cuándo podría importar esto y cómo puedes ajustar tu código.

Las aplicaciones Dart a menudo se dirigen a múltiples plataformas. Por ejemplo, una aplicación Flutter puede dirigirse a iOS, Android y la web. El código puede ser el mismo, siempre y cuando la aplicación no dependa de librerías específicas de la plataforma ni utilice números de una manera que dependa de la plataforma.

Esta página tiene detalles sobre las diferencias entre las implementaciones de números nativas y web, y cómo escribir código de manera que esas diferencias no importen.

Representación de números en Dart

#

En Dart, todos los números forman parte de la jerarquía común del tipo Object, y existen dos tipos numéricos concretos y visibles para el usuario: int, que representa valores enteros, y double, que representa valores fraccionarios.

Object es el padre de num, que a su vez es el padre de int y double

Dependiendo de la plataforma, esos tipos numéricos tienen implementaciones ocultas diferentes. En particular, Dart tiene dos tipos de destinos muy diferentes a los que compila:

  • Nativo: en la mayoría de los casos, un procesador móvil o de escritorio de 64 bits.
  • Web: JavaScript como motor de ejecución principal.

La siguiente tabla muestra cómo se implementan habitualmente los números en Dart:

Representación int nativo double nativo int en la web double en la web
Complemento a dos con signo de 64 bits
Punto flotante de 64 bits

Para destinos nativos, puedes asumir que int se mapea a una representación de entero con signo de 64 bits y double se mapea a una representación de punto flotante IEEE de 64 bits que coincide con la del procesador subyacente.

Pero en la web, donde Dart se compila e interactúa con JavaScript, existe una única representación numérica: un valor de punto flotante de doble precisión de 64 bits. Por cuestiones de eficiencia, Dart mapea tanto int como double a esta única representación. La jerarquía de tipos visible sigue siendo la misma, pero los tipos de implementación ocultos subyacentes son diferentes y están entrelazados.

La siguiente figura ilustra los tipos específicos de la plataforma (en azul) para los destinos nativos y web. Como muestra la figura, el tipo concreto para int en nativo implementa únicamente la interfaz int. Sin embargo, el tipo concreto para int en la web implementa tanto int como double.

Las clases de implementación varían según la plataforma; para JavaScript, la clase que implementa int también implementa double

Un int en la web se representa como a valor de punto flotante de doble precisión sin parte fraccionaria. En la práctica, esto funciona bastante bien: el punto flotante de doble precisión proporciona 53 bits de precisión entera. Sin embargo, los valores int siempre son también valores double, lo que puede provocar algunas sorpresas.

Diferencias de comportamiento

#

La mayoría de las operaciones aritméticas de enteros y doubles tienen esencialmente el mismo comportamiento. Hay, sin embargo, diferencias importantes, particularmente cuando tu código tiene expectativas estrictas sobre la precisión, el formato de las cadenas o los tipos de tiempo de ejecución subyacentes.

Cuando los resultados aritméticos difieren, como se describe en esta sección, el comportamiento es específico de la plataforma y está sujeto a cambios.

Precisión

#

La siguiente tabla demuestra cómo difieren algunas expresiones numéricas debido a la precisión. Aquí, math representa la librería dart:math, y math.pow(2, 53) es 253.

En la web, los enteros pierden precisión a partir de los 53 bits. En particular, 253 y 253+1 se mapean al mismo valor debido al truncamiento. En nativo, estos valores aún se pueden diferenciar porque los números nativos tienen 64 bits: 63 bits para el valor y 1 para el signo.

El efecto del desbordamiento es visible al comparar 263-1 con 263. En nativo, este último se desborda a -263, como se espera para la aritmética de complemento a dos. En la web, estos valores no se desbordan porque se representan de manera diferente; son aproximaciones debido a la pérdida de precisión.

Expresión Nativo 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

Identidad

#

En las plataformas nativas, double e int son tipos distintos: ningún valor puede ser tanto un double como un int al mismo tiempo. En la web, eso no es cierto. Debido a esta diferencia, la identidad puede diferir entre plataformas, aunque la igualdad (==) no lo hace.

La siguiente tabla muestra algunas expresiones que utilizan igualdad e identidad. Las expresiones de igualdad son las mismas en nativo y en la web; las expresiones de identidad suelen ser diferentes.

Expresión Nativo 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

Tipos y comprobación de tipos

#

En la web, el tipo int subyacente es como un subtipo de double: es un valor de doble precisión sin parte fraccionaria. De hecho, una comprobación de tipo en la web de la forma x is int devuelve true si x es un número (double) con una parte fraccionaria de valor cero.

Como resultado, lo siguiente es cierto en la web:

  • Todos los números de Dart (valores de tipo num) son double.
  • Un número de Dart puede ser tanto un double como un int al mismo tiempo.

Estos hechos afectan a las comprobaciones is y a las propiedades runtimeType. Un efecto secundario es que double.infinity se interpreta como un int. Debido a que este es un comportamiento específico de la plataforma, podría cambiar en el futuro.

Expresión Nativo 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

Operaciones bit a bit

#

Por razones de rendimiento en la web, los operadores bit a bit (&, |, ^, ~) y de desplazamiento (<<,>>, >>>) en int utilizan los equivalentes nativos de JavaScript. En JavaScript, los operandos se truncan a enteros de 32 bits que se tratan como sin signo. Este tratamiento puede dar resultados sorprendentes en números más grandes. En particular, si los operandos son negativos o no caben en 32 bits, es probable que produzcan resultados diferentes entre nativo y la web.

La siguiente tabla muestra cómo las plataformas nativa y web tratan los operadores de bits y de desplazamiento cuando los operandos son negativos o cercanos a los 32 bits:

Expresión Nativo 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

Representación de cadena

#

En la web, Dart generalmente delega en JavaScript para convertir un número en una cadena (por ejemplo, para un print). La siguiente tabla demuestra cómo la conversión de las expresiones de la primera columna puede conducir a resultados diferentes.

Expresión toString() nativo toString() en la web
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"

¿Qué deberías hacer?

#

Por lo general, no necesitas cambiar tu código numérico. El código Dart ha estado funcionando tanto en plataformas nativas como web durante años, y las diferencias en la implementación de los números rara vez son un problema. El código común y típico, como iterar a través de un rango de enteros pequeños e indexar una lista, se comporta de la misma manera.

Si tienes pruebas o aserciones que comparan resultados de cadenas, escríbelas de una manera resiliente a la plataforma. Por ejemplo, supón que estás probando el valor de expresiones de cadenas que tienen números incrustados:

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

El código anterior tiene éxito en las plataformas nativas pero lanza una excepción en la web porque message es "20 cows" (sin decimal) en la web. Como alternativa, podrías escribir la condición de la siguiente manera, para que pase tanto en plataformas nativas como web:

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

Para la manipulación de bits, considera operar explícitamente en bloques de 32 bits, los cuales son consistentes en todas las plataformas. Para forzar una interpretación con signo de un bloque de 32 bits, utiliza int.toSigned(32).

Para otros casos donde la precisión importe, considera otros tipos numéricos. El tipo BigInt proporciona enteros de precisión arbitraria tanto en nativo como en la web. El paquete fixnum proporciona números estrictos con signo de 64 bits, incluso en la web. Sin embargo, usa estos tipos con cuidado: a menudo dan como resultado un código significativamente más grande y lento.