Anunciando Dart 3

100% sound null safety. Registros, patrones y modificadores de clase.  Y un vistazo al futuro.

100% sound null safety. Records, patrones y modificadores de clase. Y un vistazo al futuro.

#

Hola desde Google I/O 2023. Hoy, en directo desde Mountain View, anunciamos Dart 3 — ¡el mayor release de Dart hasta la fecha! Dart 3 contiene tres grandes avances. Primero, hemos completado el viaje hacia el 100% sound null safety. Segundo, hemos añadido importantes nuevas características del lenguaje para records, patrones y modificadores de clase. Tercero, estamos dando una vista previa del futuro, donde ampliamos nuestro soporte de plataforma con código nativo para la web vía compilación a Wasm. Entrando en detalles.

Banner del release de Dart 3 con el logo de Dart y un ícono de diana.

100% sound null safety

#

Durante los últimos cuatro años, hemos evolucionado Dart hasta convertirlo en un lenguaje rápido, portable y moderno. ¡Ahora con Dart 3, es un lenguaje 100% sound null safe! Como hemos discutido antes, no creemos que ningún otro lenguaje de programación haya añadido sound null safety a un lenguaje existente. Así que, ha sido todo un viaje.

Línea de tiempo de null safety de Dart 3 desde la vista previa técnica de julio de 2020 hasta el release estable de mayo de 2023.

Con 100% de seguridad null en Dart, tenemos un sistema de tipos sólido. Puedes confiar en que si un tipo dice que un valor no es null, entonces nunca puede ser null. Esto evita ciertas clases de errores de programación, como las excepciones de puntero null. También permite a nuestros compiladores y runtimes optimizar el código de formas que no podrían sin la seguridad null. Esta decisión de diseño implicó un compromiso. Aunque las migraciones se volvieron un poco más difíciles, creemos que tomamos la decisión correcta para Dart.

Migrando a Dart 3

#

Una parte crítica para lograr sound null safety ha sido el apoyo inquebrantable de la comunidad de Dart: ¡el 99% de los top 1000 paquetes en pub.dev soportan null safety!

Dado esto, esperamos que la gran mayoría de los paquetes y apps que han sido migrados a null safety funcionen con Dart 3. En solo unos pocos casos, una pequeña cantidad de limpieza relacionada en Dart 3 podría afectar algo de código. Algunas APIs de bibliotecas principales heredadas han sido eliminadas (#34233, #49529) y algunas herramientas han sido ajustadas (#50707). Si experimentas algún problema migrando al Dart 3 SDK, consulta la guía de migración a Dart 3. Aparte de eso, esperamos que disfrutes las nuevas bibliotecas principales y herramientas racionalizadas.

Principales características del lenguaje — Records, patrones y modificadores de clase

#

Dart 3 no se trata solo de cambiar el lenguaje existente. También se trata de añadir características y capacidades nuevas significativas. Estas incluyen records, patrones y modificadores de clase.

Construyendo datos estructurados con records

#

Tradicionalmente, una función de Dart solo podía retornar un único valor. Como resultado, las funciones que necesitaban retornar múltiples valores tenían que empaquetarlos en otros tipos de datos como maps o listas o definir nuevas clases que pudieran contener los valores. Usar estructuras de datos sin tipar debilitaba la seguridad de tipos. Tener que definir nuevas clases solo para transportar datos añade fricción durante el proceso de programación. Nos dejaste esto bastante claro: la solicitud del lenguaje para múltiples valores de retorno es nuestro cuarto issue mejor valorado.

Con los records, puedes construir datos estructurados con una sintaxis limpia y nítida. Considera esta función. Lee el nombre y la edad de un blob JSON y los retorna ambos en un record:

dart
(String, int) userInfo(Map<String, dynamic> json) {
  return (json['name'] as String, json['height'] as int);
}

Esto debería resultar familiar para todos los desarrolladores de Dart. Un record se ve como un literal de lista tal como [‘Michael’, ‘Product Manager’] pero usa paréntesis en lugar de corchetes. En Dart, los records son una característica general. Pueden usarse para más que valores de retorno de funciones. También puedes almacenarlos en variables, ponerlos en una lista, usarlos como claves en un map, o crear records que contengan otros records. Puedes añadir tanto campos sin nombre, como hicimos en el ejemplo anterior, y campos con nombre como (42, description: ‘Meaning of life’).

Los records son tipos de valor y no tienen identidad. Esto permite a nuestros compiladores borrar completamente el objeto record en algunos casos. Los records también vienen con un operador == y funciones hashCode definidos automáticamente. La documentación de records tiene más detalles.

Trabajando con datos estructurados con patrones y coincidencia de patrones

#

Los records simplifican cómo construyes datos estructurados. Esto no reemplaza el uso de clases para construir jerarquías de tipos más formales. Solo ofrece otra opción. En cualquier caso, podrías querer descomponer esos datos estructados en sus elementos individuales para trabajar con ellos. Aquí es donde entra en juego la coincidencia de patrones.

Considera una forma básica de un patrón. El siguiente patrón de record desestructura un record en dos nuevas variables name y height. Estas variables pueden luego usarse como cualquier otra variable, como en una llamada a print:

dart
var (String name, int height) = userInfo({'name': 'Michael', 'height': 180});
print('User $name is $height cm tall.');

Existen patrones similares para listas y maps. Para todos estos, puedes saltar elementos individuales con el patrón guion bajo:

dart
var (String name, _) = userInfo();

Los patrones brillan cuando se usan en una sentencia switch. Dart ha tenido soporte limitado para switch desde el principio. En Dart 3 hemos ampliado el poder y la expresividad de la sentencia switch. Ahora soportamos coincidencia de patrones en estos casos. Hemos eliminado la necesidad de añadir break al final de cada case. También soportamos operadores lógicos para combinar casos. El siguiente ejemplo muestra una sentencia switch limpia y nítida que analiza un código de carácter:

dart
switch (charCode) {
  case slash when nextCharCode == slash:
    skipComment();

  case slash || star || plus || minus:
    operator(charCode);

  case >= digit0 && <= digit9:
    number();

  default:
    invalid();
}

La sentencia switch proporciona una gran ayuda cuando necesitas una o más sentencias para cada case. En algunos casos, todo lo que quieres hacer es calcular un valor. Para ese caso de uso, proporcionamos una expresión switch muy concisa. Esta se parece a la sentencia switch, pero usa una sintaxis diferente ajustada para expresiones. La siguiente función de ejemplo retorna el valor de una expresión switch para calcular una descripción del día de la semana de hoy:

dart
String describeDate(DateTime dt) =>
  switch (dt.weekday) {
      1 => 'Feeling the Monday blues?',
      6 || 7 => 'Enjoy the weekend!',
      _ => 'Hang in there.'
  };

Una característica poderosa de los patrones es la capacidad de verificar la "exhaustividad". Esta característica asegura que el switch maneje todos los casos posibles. En el ejemplo anterior, estamos manejando todos los valores posibles de weekday, que es un int. Agotamos todos los valores posibles a través de la combinación de sentencias match para los valores específicos 1, 6 o 7, y luego usando un caso por defecto _ para los casos restantes. Para habilitar esta verificación para jerarquías de datos definidas por el usuario, como una jerarquía de clases, usa el nuevo modificador sealed en la parte superior de la jerarquía de clases como en el siguiente ejemplo:

dart
sealed class Animal {  }
class Cow extends Animal {  }
class Sheep extends Animal {  }
class Pig extends Animal {  }

String whatDoesItSay(Animal a) =>
    switch (a) { Cow c => '$c says moo', Sheep s => '$s says baa' };

Esto retorna el siguiente error, alertándonos de que olvidamos manejar el último subtipo posible, Pig:

line 6 • The type 'Animal' is not exhaustively matched by the switch cases
since it doesn't match 'Pig()'.

Finalmente, las sentencias if también pueden usar patrones. En el siguiente ejemplo, estamos usando coincidencia if-case contra un map-pattern para desestructurar el map JSON. Dentro de eso, estamos coincidiendo con valores constantes (strings como 'name' y 'Michael') y un patrón de test de tipo int h para leer un valor JSON. Si las coincidencias del patrón fallan, Dart ejecuta la sentencia else.

dart
final json = {'name': 'Michael', 'height': 180};

// Find Michael's height.
if (json case {'name': 'Michael', 'height': int h}) {
  print('Michael is $h cm tall.');
} else {
  print('Error: json contains no height info for Michael!');
}

Esto solo toca superficialmente todo lo que puedes hacer con patrones. Creemos que se volverán omnipresentes en todo el código Dart. Para aprender más, consulta la documentación de patrones y el codelab de patrones.

Controles de acceso granulares para clases con modificadores de clase

#

Una tercera característica del lenguaje en Dart 3 son los modificadores de clase. A diferencia de los records y patrones que esperamos que cada desarrollador de Dart use, esto es más bien una característica para usuarios avanzados. Aborda las necesidades de los desarrolladores de Dart que crean grandes superficies de API o construyen apps de clase empresarial.

Los modificadores de clase permiten a los autores de APIs soportar solo un conjunto específico de capacidades. Los valores por defecto permanecen sin cambios. Queremos que Dart siga siendo simple y accesible. Así que, como antes, las clases regulares pueden ser construidas, extendidas e implementadas, como se muestra en los siguientes ejemplos:

dart
class Vehicle {
  String make; String model;
  void moveForward(int meters) {  }
}

// Construct.
var myCar = Vehicle(make: 'Ford', model: 'T',);

// Extend.
class Car extends Vehicle {
  int passengers;
}

// Implement.
class MockVehicle implements Vehicle {
  @override void moveForward 
}

Los modificadores de clase soportan añadir restricciones a esto. Considera algunos casos de uso de ejemplo:

  • Con una interface class, puedes definir un contrato para que otros lo implementen. Una interface class no puede ser extendida.

  • Con una base class, puedes asegurar que todos los subtipos de tu clase hereden de ella, en lugar de implementar su interfaz. Esto asegura que los métodos privados estén disponibles en todas las instancias.

  • Con una final class, puedes cerrar la jerarquía de tipos previniendo cualquier subclase fuera de tu propia biblioteca. Como beneficio de ejemplo, esto permite al dueño de la API añadir nuevos miembros sin arriesgar cambios que rompan compatibilidad para los consumidores de la API.

Para más detalles, consulta la nueva documentación de modificadores de clase.

Una mirada hacia el futuro

#

Dart 3 no es solo un paso significativo adelante en términos de nuevas características que puedes usar hoy. También te damos una vista previa de lo que viene.

Lenguaje Dart

#

Los records, patrones y modificadores de clase son características nuevas muy grandes, así que es posible que haya partes de su diseño que puedan mejorarse. Continuaremos monitoreando tus comentarios y veremos si se necesitan actualizaciones en releases menores siguientes a Dart 3.

También estamos viendo algunas características más pequeñas y incrementales que son completamente no-rompientes y enfocadas en aumentar la productividad del desarrollador sin costo de migración. Dos ejemplos que estamos explorando son inline classes para envolver tipos existentes con "wrappers" de costo cero, y primary constructors; una característica que introduce una sintaxis mucho más concisa para definir clases con unos pocos campos y un constructor primario.

Anteriormente hemos discutido macros (también referido como meta-programación). Estamos particularmente enfocados en esto para habilitar mejor deserialización de JSON (y similar), y para habilitar data classes. Dado el tamaño y riesgo inherente de la metaprogramación, estamos tomando un enfoque muy exhaustivo, y por lo tanto no tenemos un cronograma concreto para compartir, incluso para finalizar decisiones de diseño.

Interoperabilidad nativa

#

Las apps en móvil y escritorio típicamente dependen de muchas APIs proporcionadas por la plataforma nativa, ya sean notificaciones, pagos, o obtener la ubicación del teléfono. Tradicionalmente en Flutter estas se acceden construyendo plugins, que requieren escribir tanto código Dart para la API como una cantidad de código específico de plataforma para proporcionar la implementación.

Ya soportamos interoperabilidad con código que compila a bibliotecas C con dart:ffi. Actualmente estamos trabajando en expandir esto para soportar interoperabilidad con Java y Kotlin en Android, e interoperabilidad con Objective C y Swift en iOS/macOS. Para una introducción a la interoperabilidad de Android, consulta el nuevo video de Google I/O 23 sobre interoperabilidad de Android.

Compilación a WebAssembly — apuntando a la web con código nativo

#

WebAssembly (abreviado Wasm) ha estado creciendo en madurez como un formato de instrucciones binario neutral respecto a la plataforma a través de todos los navegadores modernos. El framework Flutter ha usado Wasm desde hace tiempo. Es cómo entregamos el motor de renderizado gráfico SKIA, escrito en C++, al navegador vía un módulo compilado a Wasm. Desde hace tiempo tenemos interés en usar Wasm para desplegar código Dart también, pero hemos estado bloqueados. Dart, como muchos otros lenguajes orientados a objetos, usa recolección de basura. Durante el último año, hemos colaborado con varios equipos en el ecosistema de Wasm para añadir una nueva característica WasmGC al estándar WebAssembly. Esto ahora está casi estable en los navegadores Chromium y Firefox.

Nuestro trabajo en compilar Dart a módulos Wasm tiene dos objetivos de alto nivel para apps web:

  • Tiempo de carga: esperamos poder entregar payloads de despliegue con Wasm que el navegador pueda cargar más rápido, mejorando así el tiempo que toma llegar al punto donde el usuario puede interactuar con la app web.

  • Rendimiento: Las apps web impulsadas por JavaScript requieren compilación just-in-time para lograr un buen rendimiento. Los módulos Wasm son de más bajo nivel y más cercanos al código máquina, así que creemos que pueden entregar un rendimiento mayor con menos stuttering y tasas de frames más consistentes.

  • Consistencia semántica: Dart se enorgullece de ser altamente consistente entre nuestras plataformas soportadas. Sin embargo, en la web, hay algunas excepciones a esto. Por ejemplo, Dart web actualmente difiere en cómo se representan los números. Con módulos Wasm, podríamos tratar la web como una plataforma "nativa" con semánticas similares a otros targets nativos.

¡Estamos emocionados de anunciar la primera vista previa de compilación de Dart a Wasm hoy! Nuestro enfoque inicial está en el soporte de Flutter web. Aún es temprano, y tenemos mucho trabajo por completar, pero te invitamos a experimentar y ver si esto te emociona tanto como a nosotros.

Cierre

#

Gracias por leer hasta el final. Esperamos que esta publicación te haya emocionado sobre Dart 3, disponible hoy tanto en el Dart SDK independiente como en el Flutter 3.10 SDK.

Hemos completado una revisión mayor del lenguaje Dart con sound null safety y limpieza de bibliotecas principales y herramientas. Hay importantes nuevas características del lenguaje que hacen Dart más expresivo y nítido con records y patrones. Para grandes superficies de API, los modificadores de clase habilitan control detallado. También estamos incluyendo una vista previa del futuro con nuestro próximo soporte para WebAssembly.

Con todas estas características, creemos que Dart 3 ilustra nuestra visión a largo plazo: Construir el lenguaje de programación más productivo para crear apps rápidas en cualquier plataforma. ¡Esperamos que tú también lo creas!

Más de Dart