Dart 2: El legado del `void`
Una de las preguntas que más veo en StackOverflow, Gitter e incluso en los canales de soporte internos de Google es la diferencia entre…
Una de las preguntas que más veo en StackOverflow, Gitter, e incluso en los canales de soporte internos de Google es la diferencia entre los siguientes tipos integrados en Dart 2:
Object, dynamic, void, y Null.
Resumiendo, Null (o Bottom en otros lenguajes, es decir, "Nada") no debería usarse en la mayoría del código real de usuario, y sospecho que veremos más artículos y lints en el futuro cercano para desalentar su uso suavemente.
Los otros tres no son tan claros, porque en Dart 2 cualquier cosa puede ser dynamic,
Object o void en tiempo de ejecución, variando solo por la firma de tipo static. Así que veamos algunos ejemplos prácticos de
cuándo deberías usar cada firma de tipo.
Object
#Object es la clase raíz de la jerarquía de clases de Dart, y cualquier otra clase en Dart es una subclase de
Object — incluyendo tipos "primitivos" como int, double, o bool. Garantiza algunas cosas: una
propiedad hashCode, un
operador ==, un
método toString.
Prácticamente hablando, uso Object como un tipo unión de pobre — esperando que los usuarios usen el operador
is para determinar el tipo real de algo antes de usarlo. No uso dynamic, porque, como se describe en la siguiente sección, deshabilita el análisis estático importante y permite más fácilmente llegar a un estado inválido.
Object readProperty(String name) { ... }
void main() {
var age = readProperty('name');
if (age is int) {
print('I am $age years old');
} else if (age is String) {
print(age);
}
}
Otra opción es usar Object para declarar que no te importa cuál es el tipo interno de una estructura de datos, por ejemplo
List<Object> podría significar "una lista de cualquier cosa". Esto es útil cuando, por ejemplo, escribes una función que combina el
hashCode de cada elemento en una List:
int hashList(List<Object> elements) { ... }
Una propiedad agradable de Object (comparado con dynamic) es que obtendrás análisis inmediato y feedback del compilador si intentas llamar un método en él que no existe de forma fiable. Por ejemplo, lo siguiente produce un
error estático:
void main() {
Object a = 5;
a.aMethodThatDoesNotExist();
}
En la práctica sin embargo, Object es bastante (e intencionalmente) limitado. Mi esperanza es que Dart obtenga soporte para sobrecarga de métodos
y eso me permitirá disminuir drásticamente mi uso del tipo Object en código real.
dynamic
#Personalmente nunca uso el tipo dynamic en Dart 2. Desde mi perspectiva, es una especie de unión de
Object y una instrucción especial que le dice a las herramientas y compiladores que deshabiliten las verificaciones de análisis estático. Es decir, lo siguiente es legal, y solo presentará un error en tiempo de ejecución (¡no estáticamente!):
void main() {
dynamic x = 5;
x.aMethodThatDoesNotExist();
}
En Dart 1, dynamic estaba en todas partes, y cualquier otro tipo estático era para soporte de IDE y análisis estático — pero el compilador (y runtime) trataba todo como
dynamic. Todavía hay algunos "gotchas" desafortunados en Dart 2 que pueden accidentalmente
crear una variable de tipo dynamic, sin embargo:
computeAge() => 5; // Return type is dynamic
void main() {
var name; // Static type is dynamic
var animals = []; // Static and runtime type is List<dynamic>
}
Peor aún, las llamadas dynamic pierden información de tipos que es vital en Dart 2:
class User {
String name;
}
void main() {
var users = []; // Implicitly List<dynamic>, remember?
users.add(new User()..name = 'Matan');
// Runtime error: List<dynamic> is not a Iterable<String>
Iterable<String> names = users.map((u) => u.name);
}
La razón de este error es porque la llamada real aquí es:
users.map((dynamic u) => u.name);
… lo cual no tiene suficiente información de tipo estático para producir un Iterable<String>. Al corregir
users para tener el tipo adecuado (y evitar llamadas dynamic), todo funciona:
void main() {
// We also could have written `var users = <User>[
var users = [new User()..name = 'Matan'];
// OK!
Iterable<String> names = users.map((u) => u.name);
}
void
#Por último, tenemos void, el tipo más nuevo en Dart 2. En Dart 1 void solo era utilizable como tipo de retorno de una función (como
void main()), pero en Dart 2 ha sido generalizado, y es utilizable en otros lugares, por ejemplo
Future<void>.
Un tipo void es semánticamente similar a Object (podría ser cualquier cosa), excepto con restricciones adicionales — un tipo
void no puede usarse para nada (ni siquiera == o hashCode), y es inválido asignar algo a un tipo
void:
void foo() {}
void main() {
var bar = foo(); // Invalid
}
En la práctica, uso void para significar "cualquier cosa y no me importan los elementos" o, más comúnmente, para significar "omitido", como en
Future<void> o Stream<void>:
/// Clear the cache.
Future<void> purgeCache() { ... }
En el fragmento de código anterior, no quiero que los usuarios intenten usar el valor de retorno del Future proporcionado, ya que no es relevante. He visto ejemplos de uso de
Future<Null> para este propósito, y eso era en realidad un workaround antes de que
Future<void> fuera posible.
Por ejemplo, esto es estáticamente OK, pero en tiempo de ejecución es inválido en Dart 2:
import 'dart:async';
Future<String> _doAThing() async => 'Test';
Future<Null> doAThing() async => _doAThing();
void main() async {
// Future<String> is not a subtype of type FutureOr<Null>
await doAThing();
}
… mientras que usar Future<void> para doAThing() es válido y correcto.
Otro ejemplo podría ser un Stream que se dispara sin ningún dato de evento:
/// Fires an event when a user signs-out of the system.
Stream<void> get onLogOut { ... }
Otro uso más práctico es implementar una clase con argumentos de tipo genérico que no vas a usar. Por ejemplo, al implementar el popular
patrón Visitor, podemos ignorar el argumento de tipo
C (context) cuando no es usado pasando void:
abstract class Visitor<N, C> {
N visitNode(N node, [C context]);
}
class IdentityVisitor<N> extends Visitor<N, void> {
@override
N visitNode(N node, [_]) => node;
}
Espero que este breve artículo te ayude con decisiones de API sobre el uso de Object, dynamic,
void. ¡Deja comentarios si tienes otras preguntas o ideas!
Más de Dart
Historia del interop con JS en Dart
El soporte para Wasm acaba de llegar al beta actual de Flutter, gracias a un emocionante hito de interop con JavaScript alcanzado en Dart 3.3. Para…
Dart DevTools: Analizando el rendimiento de la aplicación con el CPU Profiler
Ya seas un desarrollador de backend escribiendo una herramienta de línea de comandos con Dart, o un ingeniero de UX construyendo aplicaciones con Flutter, el rendimiento del programa es crítico para el éxito de tu proyecto. Las herramientas de línea de comandos deberían minimizar la latencia y las apps deberían ser responsivas y ágiles sin fotogramas perdidos. Como desarrolladores, hacemos todo lo posible para escribir código performante pero a veces no está claro por qué nuestro código no funciona tan bien como esperaríamos.