Saltar al contenido principal

¿Qué hay de nuevo en Dart 3? Expresión switch, patterns, records, modificadores de clases, etc.

· 9 min de lectura
Yayo Arellano
Software Engineer

Dart, el lenguaje de programación desarrollado por Google, ha experimentado importantes mejoras y novedades con el lanzamiento de su versión 3. Esta actualización trae consigo características y funcionalidades que hacen de Dart un lenguaje más eficiente, moderno y fácil de usar para el desarrollo de aplicaciones web y móviles.

Dart 3 fue presentado por Google durante el Google I/O 2023 y fue anunciado como el lanzamiento más grande hasta la fecha.

100% Verificación de nulos (100% Sound null safety)

A partir de Dart 2.12, se introdujo una nueva característica llamada verificación de nulos (null safety). Esta característica tiene como objetivo principal mejorar la seguridad y la estabilidad del código al proporcionar verificaciones de nulos más sólidas. Si intentas asignar un valor nulo a una variable no nula, el compilador generará un error en tiempo de compilación. Esto ayuda a reducir la posibilidad de errores relacionados con nulos y a mejorar la robustez del código.

Ahora en Dart 3 el lenguaje ahora cuenta con un 100% de verificación de nulos.

Records

Una nueva característica de Dart 3 son los Records que permiten que un solo objeto pueda contener varios objetos. Un caso de uso es cuando queremos regresar dos o más valores de una función.

Anteriormente, cuando queríamos regresar más de un objeto de una función teníamos que crear una clase extra o agregar un paquete como Tuple.

El siguiente fragmento de código muestra que para regresar la edad y el nombre de una función tenemos que crear una clase llamada Usuario:

// Ejemplo sin usar records
void main() {

final usuario = obtenerInformacion();

print('Edad: ${usuario.edad}');
print('Nombre: ${usuario.nombre}');
}

Usuario obtenerInformacion() {
return Usuario(18, 'Yayo');
}

class Usuario {
final int edad;
final String nombre;

Usuario(this.edad, this.nombre);
}

Ahora veamos la diferencia utilizando records:

// Ejemplo usando records
void main() {

final usuario = obtenerInformacion();

print('Edad: ${usuario.$1}');
print('Nombre: ${usuario.$2}');
}

(int, String) obtenerInformacion() {
return (18, 'Yayo');
}

Podemos ver que usando records, nos hemos ahorrado unas cuantas líneas de código y ya no es necesario crear la clase Usuario.

Patterns

Retomando el ejemplo anterior, un problema al usar records es que al no crear la clase Usuario no vamos a escribir usuario.nombre para acceder al nombre, sino que vamos a escribir usuario.$1, y esto hace el código más difícil de comprender. Pero utilizando patterns podemos volver a escribir un código fácil de comprender.

Usando patterns podemos mejorar la comprensión del código del ejemplo anterior:

void main() {
final usuario = obtenerInformacion();

print('Edad: ${usuario.edad}');
print('Nombre: ${usuario.nombre}');
}

({int edad, String nombre}) obtenerInformacion() {
return (edad: 12, nombre: 'Yayo');
}

Podemos ver que usando records y patterns podemos tener un código más legible y sin tener que crear una clase Usuario podemos utilizar usuario.nombre y usuario.edad.

Patterns: desestructuración

Incluso podemos utilizar la desestructuración que es una técnica de patterns para extraer y asignar valores de una estructura de datos en variables individuales de forma concisa y eficiente. El código quedaría así:

void main() {
final (:edad, :nombre) = obtenerInformacion();

print('Edad: $edad');
print('Nombre: $nombre');
}

({int edad, String nombre}) obtenerInformacion() {
return (edad: 12, nombre: 'Yayo');
}

Patterns: coincidencia de patrones (matching)

La coincidencia de patrones es una técnica que permite verificar si un valor cumple con un patrón específico y realizar acciones en función de ello. Se utiliza para realizar comparaciones más complejas y flexibles que las simples igualdades o desigualdades.

En el siguiente fragmento de código, podemos ver que ahora podemos utilizar condiciones dentro de in switch:

void main() {
int numero = 15;

switch (numero) {
case 1:
print('El numero es uno');

case > 1 && < 20:
print('El numero es mayor que diez y menor que 20');

case > 20:
print('El numero es mayor que 20');
}
}

También podemos usar patterns para ver si los case de un switch coinciden con una colección:

void main() {
final usuarios = ['Yayo', 'Carlos'];

switch (usuarios) {

case ['Yayo', 'Carlos']:
print('La lista contiene a Yayo y Carlos');

case ['Diego']:
print('La lista contiene a Diego');

case ['Diana']:
print('La lista contiene a Diana');
}
}

Validar un JSON con patterns

Podemos usar patterns para validar un JSON y obtener sus valores. Supongamos que tenemos el siguiente JSON:

var json = {
'user': ['Lily', 13]
};

Si queremos evitar errores en tiempo de ejecución tenemos que hacer varias validaciones como por ejemplo ver si el tipo de dato es correcto, si el JSON no está vacío, etc. El código sin usar patterns se vería así:

// Ejemplo sin usar patterns
if (json is Map<String, Object?> &&
json.length == 1 &&
json.containsKey('user')) {
var user = json['user'];
if (user is List<Object> &&
user.length == 2 &&
user[0] is String &&
user[1] is int) {
var nombre = user[0] as String;
var edad = user[1] as int;
print('El usuario $nombre tiene $edad años');
}
}

Pero si utilizamos patterns en 3 líneas de código podemos validar el JSON:

if (json case {'user': [String nombre, int edad]}) {
print('El usuario $nombre tiene $edad años');
}

Todas las validaciones necesarias se realizan dentro del if-case:

  • Se valida que json es un objeto tipo Map
  • Al ser un objeto de tipo Map automáticamente se valida que no sea null
  • Se valida que json contiene la llave user
  • Los tipos de dato de la lista son String e int
  • Se crean las variables locales nombre y edad

Expresión switch (switch expression)

Todos conocemos la estructura switch case que nos ayuda a asignar o ejecutar una lógica específica de acuerdo al resultado de la comparación evaluada.

Veamos un sencillo ejemplo, suponiendo que el primer día de la semana es lunes y es el día uno, martes es el día dos y así hasta llegar al domingo. Dado un número entero queremos imprimir a qué día corresponde. Usando el clásico switch case el código se verá así:

// Ejemplo usando el clásico switch case
void main() {
final diaActual = obtenerDia(5);
print('Hoy es $diaActual'); // Hoy es Viernes
}

String obtenerDia(int dia) {
switch (dia) {
case 1:
return 'Lunes';
case 2:
return 'Martes';
case 3:
return 'Miércoles';
case 4:
return 'Jueves';
case 5:
return 'Viernes';
case 6:
return 'Sábado';
case 7:
return 'Domingo';
default:
return 'El dia no existe';
}
}

Podemos ver que tuvimos que escribir varias veces case y return. Pero con Dart 3 y switch expression podemos hacer el código más compacto y simple de leer:

// Ejemplo usando el nuevo expresión switch
void main() {
final diaActual = obtenerDia(5);
print('Hoy es $diaActual'); // Hoy es Viernes
}

String obtenerDia(int dia) {
return switch (dia) {
1 => 'Lunes',
2 => 'Martes',
3 => 'Miércoles',
4 => 'Jueves',
5 => 'Viernes',
6 => 'Sábado',
7 => 'Domingo',
_ => 'El dia no existe',
};
}

En este ejemplo, la expresión dia se evalúa y se compara con distintos casos utilizando la sintaxis =>. Si el valor de dia es 1, el resultado es 'Lunes'. El guion bajo _ se utiliza como un caso por defecto para cualquier otro valor que no coincida con los casos anteriores.

Y si queremos solo saber si es día es "entre semana" o es un "fin de semana" podemos escribir el siguiente código:

void main() {
final diaActual = obtenerDia(4);
print('Hoy es $diaActual'); // Entre semana
}

String obtenerDia(int dia) {
return switch (dia) {
>= 1 && <= 5 => 'Entre semana',
6 || 7 => 'Fin de semana',
_ => 'El dia no existe',
};
}

Clases selladas (sealed class)

En Dart 3, la palabra clave utilizada para definir una clase sellada es sealed. Al igual que en otros lenguajes de programación, una clase sellada en Dart es una clase que no puede ser heredada por otras clases fuera de su declaración. Esto quiere decir que las subclases de la clase sellada deben ser declaradas en el mismo archivo.

Las clases selladas en Dart se utilizan a menudo en combinación con las expresiones switch. La estructura switch puede hacer uso exhaustivo de las subclases de una clase sellada, asegurando que se manejen todos los casos posibles.

Aquí tienes un ejemplo de cómo utilizar una clase sellada en una expresión switch:

void main() {
final resultado = evaluarResultado(Success());
print(resultado); // Exito: Peticion exitosa
}

String evaluarResultado(Result result) {
return switch (result) {
Success(mensaje: final mensaje) => 'Exito: $mensaje',
Error(mensaje: final mensaje) => 'Error: $mensaje',
};
}

sealed class Result {
final String mensaje;

Result(this.mensaje);
}

class Success extends Result {
Success() : super('Peticion exitosa');
}

class Error extends Result {
Error() : super('Peticion no exitosa');
}
note

El ejemplo anterior funciona porque la clase Result es sealed. Si no lo fuera tendríamos un error non_exhaustive_switch_expression

Modificadores de clase

Los modificadores de clase son palabras reservadas utilizadas para controlar la visibilidad y el comportamiento de una clase.

En Dart 3 se han agregado los siguientes modificadores de clase:

  • base
  • final
  • interface
  • sealed

En este artículo no hablaremos de cada uno de ellos, pero podemos visitar la documentación para ver en detalle el funcionamiento.

Conclusión

En Dart 3 se han agregado varias características que van a hacer la vida de los programadores mucho más fácil al hacer que Dart sea un lenguaje más expresivo.

Para mí en particular lo mejor son patterns y records porque ahora puedo hacer más con mucho menos código.

Espero que te haya gustado este artículo y si tienes alguna pregunta no olvides seguirme en mis redes sociales.