Saltar al contenido principal

Colecciones iterables

Una guía interactiva para usar objetos Iterable, como listas y conjuntos.

Este tutorial te enseña cómo usar colecciones que implementan la clase Iterable —por ejemplo List y Set. Las iterables son bloques de construcción básicos para todo tipo de aplicaciones de Dart, y probablemente ya las estés usando, incluso sin darte cuenta. Este tutorial te ayuda a aprovecharlas al máximo.

Usando los editores de DartPad embebidos, puedes probar tus conocimientos ejecutando código de ejemplo y completando ejercicios.

Para aprovechar al máximo este tutorial, deberías tener conocimientos básicos de la sintaxis de Dart.

Este tutorial cubre el siguiente material:

  • Cómo leer elementos de un Iterable.
  • Cómo verificar si los elementos de un Iterable cumplen con una condición.
  • Cómo filtrar el contenido de un Iterable.
  • Cómo mapear el contenido de un Iterable a un valor diferente.

Tiempo estimado para completar este tutorial: 60 minutos.

Los ejercicios de este tutorial tienen fragmentos de código parcialmente completados. Puedes usar DartPad para probar tus conocimientos completando el código y haciendo clic en el botón Run. No edites el código de prueba en la función main o debajo.

Si necesitas ayuda, expande el menú desplegable Pista o Solución después de cada ejercicio.

¿Qué son las colecciones?

#

Una colección es un objeto que representa un grupo de objetos, llamados elementos. Los Iterables son un tipo de colección.

Una colección puede estar vacía o puede contener muchos elementos. Según el propósito, las colecciones pueden tener diferentes estructuras e implementaciones. Estos son algunos de los tipos de colección más comunes:

  • List: Usado para leer elementos mediante sus índices.
  • Set: Usado para contener elementos que pueden ocurrir solo una vez.
  • Map: Usado para leer elementos usando una clave.

¿Qué es un Iterable?

#

Un Iterable es una colección de elementos a los que se puede acceder secuencialmente.

En Dart, un Iterable es una clase abstracta, lo que significa que no puedes instanciarla directamente. Sin embargo, puedes crear un nuevo Iterable creando un nuevo List o Set.

Tanto List como Set son Iterable, por lo que tienen los mismos métodos y propiedades que la clase Iterable.

Un Map utiliza una estructura de datos diferente internamente, según su implementación. Por ejemplo, HashMap utiliza una tabla hash en la que los elementos (también llamados values) se obtienen mediante una clave. Los elementos de un Map también se pueden leer como objetos Iterable mediante la propiedad entries o values del mapa.

Este ejemplo muestra una List de int, que también es un Iterable de int:

dart
Iterable<int> iterable = [1, 2, 3];

La diferencia con una List es que con el Iterable, no puedes garantizar que la lectura de elementos por índice sea eficiente. Iterable, a diferencia de List, no tiene el operador [].

Por ejemplo, considera el siguiente código, el cual es inválido:

mal dart
Iterable<int> iterable = [1, 2, 3];
int value = iterable[1];

Si lees elementos con [], el compilador te indicará que el operador '[]' no está definido para la clase Iterable, lo que significa que no puedes usar [index] en este caso.

En su lugar, puedes leer elementos con elementAt(), el cual puede recorrer los elementos del iterable hasta llegar al elemento en esa posición.

dart
Iterable<int> iterable = [1, 2, 3];
int value = iterable.elementAt(1);

Dependiendo de la implementación del iterable y del número de elementos, elementAt puede tener una complejidad lineal y ser costoso. Si planeas acceder a elementos específicos repetidamente, considera llamar a .toList() en el iterable para convertirlo en una lista una vez, y luego usar el operador [].

dart
final items = veryLargeIterable().toList();

final tenthItem = items[9];
final hundredthItem = items[99];
final thousandthItem = items[999];
final lastItem = items.last;

Continúa con la siguiente sección para aprender más sobre cómo acceder a los elementos de un Iterable.

Leer elementos

#

Puedes leer los elementos de un iterable secuencialmente, usando un bucle for-in.

Ejemplo: Usar un bucle for-in

#

El siguiente ejemplo te muestra cómo leer elementos usando un bucle for-in.

Ejemplo: Usar first y last

#

En algunos casos, quieres acceder solo al primer o al último elemento de un Iterable.

Con la clase Iterable, no puedes acceder a los elementos directamente, por lo que no puedes llamar a iterable[0] para acceder al primer elemento. En su lugar, puedes usar first, el cual obtiene el primer elemento.

Además, con la clase Iterable, no puedes usar el operador [] para acceder al último elemento, pero puedes usar la propiedad last.

En este ejemplo viste cómo usar first y last para obtener el primer y el último elemento de un Iterable. También es posible encontrar el primer elemento que cumpla con una condición. La siguiente sección muestra cómo hacer eso usando un método llamado firstWhere().

Ejemplo: Usar firstWhere()

#

Ya viste que puedes acceder a los elementos de un Iterable secuencialmente, y que puedes obtener fácilmente el primer o el último elemento.

Ahora, aprenderás a usar firstWhere() para encontrar el primer elemento que cumpla con ciertas condiciones. Este método requiere que pases un predicate, que es una función que devuelve true si la entrada cumple con una determinada condición.

dart
String element = iterable.firstWhere((element) => element.length > 5);

Por ejemplo, si quieres encontrar el primer String que tenga más de 5 caracteres, debes pasar un predicado que devuelva true cuando el tamaño del elemento sea mayor que 5.

Ejecuta el siguiente ejemplo para ver cómo funciona firstWhere(). ¿Crees que todas las funciones darán el mismo resultado?

En este ejemplo, puedes ver tres formas diferentes de escribir un predicado:

  • Como expresión: El código de prueba tiene una línea que utiliza la sintaxis de flecha (=>).
  • Como bloque: El código de prueba tiene múltiples líneas entre llaves y una sentencia return.
  • Como función: El código de prueba está en una función externa que se pasa al método firstWhere() como parámetro.

No hay una forma correcta o incorrecta. Utiliza la forma que funcione mejor para ti, y que haga que tu código sea más fácil de leer y entender.

El ejemplo final llama a firstWhere() con el parámetro con nombre opcional orElse, el cual proporciona una alternativa cuando no se encuentra un elemento. En este caso, se devuelve el texto 'None!' porque ningún elemento cumple con la condición proporcionada.

Ejercicio: Practicar la escritura de un predicado de prueba

#

El siguiente ejercicio es una prueba unitaria que falla y contiene un fragmento de código parcialmente completo. Tu tarea es completar el ejercicio escribiendo código para que las pruebas pasen. No necesitas implementar main().

Este ejercicio introduce singleWhere(). Este método funciona de manera similar a firstWhere(), but en este caso espera que solo un elemento del Iterable cumpla con el predicado. Si más de uno o ningún elemento en el Iterable cumple con la condición del predicado, entonces el método lanza una excepción StateError.

Tu objetivo es implementar el predicado para singleWhere() que cumpla con las siguientes condiciones:

  • El elemento contiene el carácter 'a'.
  • El elemento comienza con el carácter 'M'.

Todos los elementos en los datos de prueba son strings; puedes consultar la documentación de la clase para obtener ayuda.

Pista

Tu solución podría hacer uso de los métodos contains y startsWith de la clase String.

Solución
dart
String singleWhere(Iterable<String> items) {
  return items.singleWhere(
          (element) => element.startsWith('M') && element.contains('a'));
}

Comprobar condiciones

#

Cuando trabajas con Iterable, a veces necesitas verificar que todos los elementos de una colección cumplan con alguna condición.

Podrías tener la tentación de escribir una solución usando un bucle for-in como este:

mal dart
for (final item in items) {
  if (item.length < 5) {
    return false;
  }
}
return true;

Sin embargo, puedes lograr lo mismo usando el método every():

dart
return items.every((item) => item.length >= 5);

Usar el método every() da como resultado un código que es más legible, compacto y menos propenso a errores.

Ejemplo: Usar any() y every()

#

La clase Iterable proporciona dos métodos que puedes usar para verificar condiciones:

  • any(): Devuelve true si al menos un elemento cumple con la condición.
  • every(): Devuelve true si todos los elementos cumplen con la condición.

Ejecuta este ejercicio para verlos en acción.

En el ejemplo, any() verifica que al menos un elemento contiene el carácter a, y every() verifica que todos los elementos tienen una longitud igual o mayor que 5.

Después de ejecutar el código, intenta cambiar el predicado de any() para que devuelva false:

dart
if (items.any((item) => item.contains('Z'))) {
  print('At least one item contains "Z"');
} else {
  print('No item contains "Z"');
}

También puedes usar any() para verificar que ningún elemento de un Iterable cumple con una determinada condición.

Ejercicio: Verificar que un Iterable cumple una condición

#

El siguiente ejercicio ofrece práctica en el uso de los métodos any() y every(), descritos en el ejemplo anterior. En este caso, trabajas con un grupo de usuarios, representados por objetos User que tienen el campo miembro age.

Usa any() y every() para implementar dos funciones:

  • Part 1: Implement anyUserUnder18().
    • Devuelve true si al menos un usuario tiene 17 años o menos.
  • Part 2: Implement everyUserOver13().
    • Devuelve true si todos los usuarios tienen 14 años o más.
Pista

Recuerda usar los métodos any y every de la clase Iterable. Para obtener ayuda y ver ejemplos utilizando estos métodos, consulta la discusión anterior sobre ellos.

Solución
dart
bool anyUserUnder18(Iterable<User> users) {
  return users.any((user) => user.age < 18);
}

bool everyUserOver13(Iterable<User> users) {
  return users.every((user) => user.age > 13);
}

Filtrado

#

Las secciones anteriores cubren métodos como firstWhere() o singleWhere() que te pueden ayudar a encontrar un elemento que cumpla con un determinado predicado.

¿Pero qué pasa si quieres encontrar todos los elementos que cumplen con una determinada condición? Puedes lograrlo usando el método where().

dart
var evenNumbers = numbers.where((number) => number.isEven);

En este ejemplo, numbers contiene un Iterable con múltiples valores int, y where() encuentra todos los números que son pares.

La salida de where() es otro Iterable, y puedes usarlo como tal para iterar sobre él o aplicar otros métodos de Iterable. En el siguiente ejemplo, la salida de where() se utiliza directamente dentro del bucle for-in.

dart
var evenNumbers = numbers.where((number) => number.isEven);
for (final number in evenNumbers) {
  print('$number is even');
}

Ejemplo: Usar where()

#

Ejecuta este ejemplo para ver cómo se puede usar where() junto con otros métodos como any().

En este ejemplo, where() se usa para encontrar todos los números que son pares, luego any() se usa para verificar si los resultados contienen un número negativo.

Más adelante en el ejemplo, se vuelve a usar where() para encontrar todos los números mayores que 1000. Como no hay ninguno, el resultado es un Iterable vacío.

Ejemplo: Usar takeWhile

#

Los métodos takeWhile() y skipWhile() también pueden ayudarte a filtrar elementos de un Iterable.

Ejecuta este ejemplo para ver cómo takeWhile() y skipWhile() pueden dividir un Iterable que contiene números.

En este ejemplo, takeWhile() devuelve un Iterable que contiene todos los elementos antes de aquel que cumple con el predicado. Por otro lado, skipWhile() devuelve un Iterable que contiene todos los elementos a partir de e incluyendo el primero que no cumple con el predicado.

Después de ejecutar el ejemplo, cambia takeWhile() para tomar elementos hasta que llegue al primer número negativo.

dart
var numbersUntilNegative = numbers.takeWhile(
  (number) => !number.isNegative,
);

Ten en cuenta que la condición number.isNegative se niega con !.

Ejercicio: Filtrar elementos de una lista

#

El siguiente ejercicio ofrece práctica en el uso del método where() con la clase User del ejercicio anterior.

Usa where() para implementar dos funciones:

  • Part 1: Implement filterOutUnder21().
    • Devuelve un Iterable que contenga a todos los usuarios de 21 años o más.
  • Part 2: Implement findShortNamed().
    • Devuelve un Iterable que contenga a todos los usuarios con nombres de longitud 3 o menos.
Pista

Recuerda aprovechar el método where de la clase Iterable. Para obtener ayuda y ver ejemplos sobre el uso de where, consulta la discusión anterior sobre él.

Solución
dart
Iterable<User> filterOutUnder21(Iterable<User> users) {
  return users.where((user) => user.age >= 21);
}

Iterable<User> findShortNamed(Iterable<User> users) {
  return users.where((user) => user.name.length <= 3);
}

Mapeo

#

Mapear Iterables con el método map() te permite aplicar una función sobre cada uno de los elementos, reemplazando cada elemento por uno nuevo.

dart
Iterable<int> output = numbers.map((number) => number * 10);

En este ejemplo, cada elemento del Iterable de números se multiplica por 10.

También puedes usar map() para transformar un elemento en un objeto diferente—por ejemplo, para convertir todos los int a String, como puedes ver en el siguiente ejemplo:

dart
Iterable<String> output = numbers.map((number) => number.toString());

Ejemplo: Usar map para cambiar elementos

#

Ejecuta este ejemplo para ver cómo usar map() para multiplicar todos los elementos de un Iterable por 2. ¿Cuál crees que será la salida?

Ejercicio: Mapear a un tipo diferente

#

En el ejemplo anterior, multiplicaste los elementos de un Iterable por 2. Tanto la entrada como la salida de esa operación fueron un Iterable de int.

En este ejercicio, tu código toma un Iterable de User, y necesitas devolver un Iterable que contenga cadenas con el nombre y la edad de cada usuario.

Cada cadena en el Iterable debe seguir este formato: '{name} is {age}'—por ejemplo, 'Alice is 21'.

Pista

Recuerda aprovechar el método map de la clase Iterable. Para obtener ayuda y ver ejemplos utilizando map, consulta la discusión anterior sobre él.

Para concatenar múltiples valores en una sola cadena, considera usar la interpolación de cadenas.

Solución
dart
Iterable<String> getNameAndAges(Iterable<User> users) {
  return users.map((user) => '${user.name} is ${user.age}');
}

Ejercicio: Poniéndolo todo junto

#

Es hora de practicar lo que aprendiste, en un ejercicio final.

Este ejercicio proporciona la clase EmailAddress, la cual tiene un constructor que toma un string. Otra función proporcionada es isValidEmailAddress(), la cual comprueba si una dirección de correo electrónico es válida.

Constructor/funciónFirma de tipoDescripción
EmailAddress() EmailAddress(String address) Crea un EmailAddress para la dirección especificada.
isValidEmailAddress() bool isValidEmailAddress(EmailAddress) Devuelve true si la EmailAddress proporcionada es válida.

Escribe el siguiente código:

Parte 1: Implementar parseEmailAddresses().

  • Escribe la función parseEmailAddresses(), la cual toma un Iterable<String> que contiene direcciones de correo electrónico, y devuelve un Iterable<EmailAddress>.
  • Usa el método map() para mapear de un String a EmailAddress.
  • Crea los objetos EmailAddress usando el constructor EmailAddress(String).

Parte 2: Implementar anyInvalidEmailAddress().

  • Escribe la función anyInvalidEmailAddress(), la cual toma un Iterable<EmailAddress> y devuelve true si alguna EmailAddress en el Iterable no es válida.
  • Usa el método any() junto con la función proporcionada isValidEmailAddress().

Parte 3: Implementar validEmailAddresses().

  • Escribe la función validEmailAddresses(), la cual toma un Iterable<EmailAddress> y devuelve otro Iterable<EmailAddress> que contiene solo direcciones válidas.
  • Usa el método where() para filtrar el Iterable<EmailAddress>.
  • Usa la función proporcionada isValidEmailAddress() para evaluar si una EmailAddress es válida.
Solución
dart
Iterable<EmailAddress> parseEmailAddresses(Iterable<String> strings) {
  return strings.map((s) => EmailAddress(s));
}

bool anyInvalidEmailAddress(Iterable<EmailAddress> emails) {
  return emails.any((email) => !isValidEmailAddress(email));
}

Iterable<EmailAddress> validEmailAddresses(Iterable<EmailAddress> emails) {
  return emails.where((email) => isValidEmailAddress(email));
}

Qué sigue

#

¡Felicitaciones, has terminado el tutorial! Si quieres aprender más, aquí hay algunas sugerencias sobre dónde ir a continuación: