Este no es el típico artículo que enseña cómo aprender un nuevo lenguaje de programación, como Dart. En su lugar, quiero compartir las características que realmente me sorprendieron y me gustaron al comenzar a trabajar con este lenguaje.
Introducción
Dart es un lenguaje de programación de código abierto desarrollado por Google que ya tiene ¡14 años! – en la fecha en que estoy escribiendo esto – cargado con un montón de características: orientado a objetos, con garbage collection, inspirado en C…, además, Dart puede compilar no solo código nativo para tu sistema operativo favorito sino que también que también para WebAssembly, JavaScript, aplicaciones de Android e iOS (con ayuda de Flutter). Dart es un lenguaje de programación realmente interesante.
Características
Vale, ok. Dejémonos de preámbulos y vamos a lo que realmente hemos venido a leer aquí: ¿Qué cosas lo hacen único? He aquí un listado de las cosas que yo he visto que me gustaron. El orden es casi que aleatorio, pero bueno, listo primero las que más me gustan.
¡Vamos a ello!
Rethrow
Sí, justo, Dart soporta excepciones. Si hay algo que ha salido mal, podemos ejecutar un throw Exception(«¡Algo ha ido mal!») y esto se comportará tal y como ya habrás podido deducir. Pero ¿sabemos qué hace el lenguaje de programación cuando gestiona las excepciones? SI no lo sabes, te lo explico rápidamente. Cada vez que se lanza una excepción, ésta se guarda en la pila excepciones, hasta que suceda algo de lo siguiente: las gestionamos con un try/catch o… bueno… la aplicación explota en nuestra cara. Será entonces cuando veamos la traza de las excepciones. Muchos lenguajes de programación van acumulando la información de todas las excepciones capturadas y lanzadas. Por ejemplo Java:
java.lang.RuntimeException: Final exception
at com.example.MyClass.methodD(MyClass.java:40)
at com.example.MyClass.methodA(MyClass.java:10)
at com.example.Main.main(Main.java:5)
Caused by: java.lang.IllegalStateException: Intermediate exception
at com.example.MyClass.methodC(MyClass.java:30)
at com.example.MyClass.methodB(MyClass.java:20)
Caused by: java.lang.IllegalArgumentException: Original exception
at com.example.MyClass.methodE(MyClass.java:50)
En el fondo esto no está mal, pero estas trazas pueden acumularse y entonces se vuelve una ristra de texto que no hay quien lo lea.
Con rethrow podemos relanzar la excepción que hemos capturado, sin añadirla en la pila de trazas; con ello tendremos unas trazas más reducidas y concretas. Veamos un ejemplo:
void main() {
try {
dividirNumeros(10, 0);
} catch (e) {
print('Atrapado el error en la función main: $e');
}
}
void dividirNumeros(int a, int b) {
try {
if (b == 0) {
throw Exception('División por cero');
}
print(a / b);
} catch (e) {
print('¡Capturamos un error!: $e');
rethrow; // <-- Capturamos el error y lo propagamos
}
Seguridad de nulabilidad sólida
La seguridad de nulabilidad sólida (o sound null safety en inglés) es la capacidad que tiene Dart para asegurarse de que las variables no puedan ser nulas a menos que lo especifiquemos explícitamente. Dart hace revisiones en tiempo de compilación para reforzar esta regla, evitando errores de asignación de nulos en tiempo de ejecución.
String? nombre; // ¡Nota el signo de interrogación!
nombre = null; // nombre permite la asignación de nulos...
nombre = "Juan"; // ... y obviamente también de cadenas de texto
print(nombre);
Funciones de primera clase
Las funciones de primera clase o first-class functions son “ciudadanos de primera clase” en Dart, y esto significa que las funciones pueden:
- Ser asignadas a variables.
- Ser pasadas como argumentos a otras funciones.
- Ser usadas como return en las funciones.
- Y ¡ser almacenadas en estructuras de datos como listas o mapas!
Esto nos permite flexibilidad en nuestro código mejorando su mantenimiento:
void main() {
// Definir una función que toma otra función como parámetro
void realizarOperacion(int a, int b, int Function(int, int) operacion) {
int resultado = operacion(a, b);
print('El resultado es $resultado');
}
// Definir una función simple
int sumar(int x, int y) => x + y;
// Pasar la función como argumento
realizarOperacion(5, 3, sumar);
// Usar una función anónima como argumento
realizarOperacion(5, 3, (x, y) => x * y);
}
Lenguaje estáticamente tipado… ¿o no?
Que sí, entiendo que podemos hablar de esta característica por horas sin llegar a un acuerdo. Pero al menos Dart nos ofrece la capacidad de tipar nuestro código con:
- Números:
- int para números enteros, o
- double para números con decimales.
- Cadenas de texto con String.
- Valores booleanos con bool.
- Arrays… bueno en Dart se llaman List.
- Set, una colección de datos únicos.
- Map, conjunto de datos con un índice.
- Incluso variables que pueden almacenar lo que quieras, con dynamic.
void main() {
// Número entero (int)
int edad = 30;
// Número decimal (double)
double altura = 1.75;
// Cadena de texto (String)
String nombre = "Juan";
// Valor booleano (bool)
bool esEstudiante = true;
// Lista (List)
List<String> frutas = ['manzana', 'plátano', 'naranja'];
// Conjunto (Set)
Set<int> numerosUnicos = {1, 2, 3, 3};
// Mapa (Map)
Map<String, String> capitales = {'España': 'Madrid', 'Francia': 'París'};
// Dinámico (dynamic)
dynamic variableDinamica = 'Puede ser cualquier cosa';
variableDinamica = 42;
}
Parámetros de función opcionales
Los parámetros de las funciones en Dart pueden ser opcionales. Por ejemplo, vamos este inicio de una función con parámetros opcionales:
void describeAnimal(String nombre, [String? especie, int? edad]) {
Verás que los parámetros especie y edad son opcionales. Lo sabrás porque están dentro de corchetes. Además, también te habrás fijado que ambas pueden ser nulos; esto es porque si la función se llama de este modo…
describeAnimal('León');
…tanto especie como edad serán nulos.
Para llamarlos con datos, la función deberá ser llamada de este modo:
describeAnimal(Michi, 'Gato', 3);
Entonces especie será Gato y edad será 3.
Por último, podríamos asignarles valores por defecto, de modo que si se llama la función sin especificar estos dos parámetros, estos tengan un valor diferente a nulo:
void describeAnimal(String nombre, [String especie='Gato', int edad=3]) {
Parámetros de función con nombre
Este tipo de parámetros también viene bien en momentos determinados. Es básicamente que los parámetros de la función pueden tener un nombre asignado y no tienen porqué ser llamados en el mismo orden en el que fueron definidos en la función.
Mira este trozo de código:
main() {
saludar(nombre: 'Juan');
saludar(titulo: 'Sr.', nombre: 'Juan');
}
void saludar({required String nombre, String? titulo}) {
print('Hola, $titulo $nombre');
}
Aquí se define una función que acepta dos parámetros, un nombre, requerido y un titulo, que es opcional. De hecho, todos los parámetros con nombre de una función son opcionales por defecto.
Fíjate que al llamar la función la primera vez, omitimos el valor para titulo, por lo que será nulo. El resultado de la primera llamada será: Hola, null Juan
Pero observa detenidamente la segunda vez que llamamos a la función saludar. Verás que posicionalmente escribimos el titulo primero y el nombre después. ¡Totalmente diferente a como fueron definidos en la función!, pero a Dart a esto le da igual y nos mostrará: Hola, Sr. Juan
¡Mola!
Constructores factoría
Este tipo de constructores son especiales que devuelven una instancia de la clase, una instancia ya creada o un subtipo de una instancia. Los constructores factoría son usados para implementar singletons, cacheo y creación de objetos condicionales.
Ahora presta atención a esto, que te hará explotar la cabeza 🤯:
class Articulo {
final String nombre;
final bool esBorrador;
// Constructor factoría para artículos borradores
factory Articulo.Borrador(String nombre) {
return Articulo(nombre, true);
}
// Constructor factoría para artículos publicados
factory Articulo.Publicado(String nombre) {
return Articulo(nombre, false);
}
// Constructor... tradicional
Articulo(this.nombre, this.esBorrador);
}
void main() {
Articulo borrador = Articulo.Borrador('Artículo de prueba');
Articulo publicado = Articulo.Publicado('Artículo definitivo');
Articulo personalizado = Articulo('Artículo personalizado', true);
// Artículo de prueba, true
print("${borrador.nombre}, ${borrador.esBorrador}");
// Artículo definitivo, false
print("${publicado.nombre}, ${publicado.esBorrador}");
// Artículo personalizado, true
print("${personalizado.nombre}, ${personalizado.esBorrador}");
}
En este trozo de código definimos tres constructores: dos constructores de factoría Articulo.Borrador y Articulo.Publicado, y el constructor tradicional, el de toda la vida. Los constructores de factoría tienen lógica de negocio dentro y, cuando terminan de definirla, llaman al constructor tradicional. Si con esto no te convenzo para que pruebes Dart, entonces ya nada lo hará.
Últimas impresiones
Como has podido comprobar, Dart es un lenguaje potente, moderno y con esteroides. Hay más cosas que me gustaron, como los mixins (un modo de compartir código entre clases tipo los traits de PHP), la gestión de múltiples hilos, la palabra clave late para variables y mucho más.
Espero te haya gustado el artículo y que los ejemplos que haya puesto te hayan servido para entender un poco mejor los diferentes atributos que tiene Dart que más me gustaron. ¿Te perdiste nuestro último artículo? ¡Léelo aquí!