Sets en Dart: Unicidad y eficiencia en colecciones

Un Set<T> es una colección que garantiza la unicidad de sus elementos; es decir, no puede contener duplicados. A diferencia de una List, donde el orden y la posición son los protagonistas, en un conjunto lo fundamental es la pertenencia: saber si un elemento está o no está en el grupo. En Dart, la implementación por defecto es LinkedHashSet [disponible desde Dart 1.0], lo que significa que, aunque matemáticamente un conjunto no tiene orden, Dart mantiene el orden de inserción para que la iteración sea predecible y útil.

Esta elección de diseño responde a un equilibrio entre rendimiento y usabilidad. Al usar una tabla de hash, la operación de verificar si un elemento existe (contains) tiene una complejidad de tiempo constante $O(1)$ en promedio, lo que hace que los conjuntos sean extremadamente rápidos para validaciones. Sin embargo, si usas HashSet de dart:collection, pierdes el orden de inserción para ganar un poco más de eficiencia en memoria y velocidad de hash. Si necesitas que los elementos se mantengan ordenados de forma natural (ej. de menor a mayor), debes usar SplayTreeSet.

Debes usar un Set cuando la identidad de los elementos sea más importante que su posición, o cuando necesites realizar operaciones de comparación entre grupos (como intersecciones). Si intentas usar un Set para acceder a elementos por su índice (como miSet[0]), el compilador fallará, ya que los sets no implementan el acceso posicional. Además, si usas un List para gestionar una colección de miles de elementos únicos y realizas búsquedas constantes con contains, tu aplicación se volverá lenta debido a que una List requiere recorrerla elemento a elemento ($O(n)$).

import 'dart:collection';

void main() {
  // El literal {} crea un Map vacío por defecto. 
  // Para un Set vacío, debemos especificar el tipo: <int>{}.
  final idsBrutos = [101, 102, 103, 101, 104, 102, 105];

  // Set.of crea un nuevo Set a partir de un Iterable.
  // Elimina automáticamente los duplicados presentes en la lista original.
  final idsProcesados = Set.of(idsRaw: idsBrutos);

  // El método add() devuelve un bool: true si se añadió, 
  // false si el elemento ya existía.
  final esNuevo = idsProcesados.add(106);
  final esDuplicado = idsProcesados.add(101);

  print('¿Se añadió el 106? $esNuevo');     // true
  print('¿Se añadió el 101? $esDuplicado'); // false

  // Usamos HashSet para máxima velocidad de búsqueda,
  // sabiendo que no podemos confiar en el orden de iteración.
  final bloqueados = HashSet<int>.from<int>([103, 999]);

  // Operaciones de conjuntos:
  // difference: elementos en A que no están en B.
  final usuariosActivos = idsProcesados.difference(bloqueados);
  
  // intersection: elementos que están en ambos conjuntos.
  final coincidencia = idsProcesados.intersection(bloqueados);

  print('Activos: $usuariosActivos'); // 101, 102, 104, 105, 106
  print('Bloqueados presentes: $coincidencia'); // 103

  // SplayTreeSet mantiene los elementos ordenados según su comparación natural.
  final ordenados = SplayTreeSet<int>.from(idsProcesados);
  print('Orden natural: $ordenados'); // 101, 102, 104, 105, 106

  // removeWhere permite filtrar elementos con una condición.
  idsProcesados.removeWhere((id) => id > 104);
  print('Tras limpieza: $idsProcesados');
}

// Helper para el ejemplo
final idsRaw = [101, 102, 103, 101, 104, 102, 105];

Análisis del código

En el ejemplo, idsProcesados se inicializa mediante Set.of, lo que genera un LinkedHashSet. Por eso, al imprimirlo, los elementos mantienen el orden en que fueron insertados (primero el 101, luego 102, etc.). Observa cómo add(101) devuelve false; esto es clave para lógica de negocios donde solo queremos procesar elementos nuevos.

Al trabajar con bloqueados, hemos forzado el uso de HashSet. Si imprimiéramos este conjunto tras añadirle elementos, el orden podría no coincidir con la inserción, algo que LinkedHashSet sí garantiza. La operación difference es altamente eficiente porque el motor de Dart utiliza las propiedades del hash de los elementos para descartar rápidamente los que coinciden con el segundo conjunto, evitando comparaciones innecesarias elemento por elemento.

Finalmente, SplayTreeSet implementa un árbol de búsqueda balanceado. Esto cambia la complejidad de las operaciones de $O(1)$ a $O(\log n)$, pero te entrega la ventaja de tener los datos siempre ordenados sin necesidad de llamar a .toList().sort().

El error frecuente

Un error muy común al trabajar con literales es la ambigüedad del par de llaves {}. En Dart, {} es un mapa vacío, no un conjunto vacío.

// ERROR COMÚN
final miSet = {}; // Esto es un Map<dynamic, dynamic>
// miSet.add(1); // Error de compilación: 'Map' no tiene el método 'add'

// FORMA CORRECTA
final miSetCorrecto = <int>{}; // Esto es un Set<int>

Si intentas usar add en un objeto inicializado con {}, el compilador te dará un error de tipos porque los mapas usan [] para asignar valores a llaves, mientras que los conjuntos usan add().

34

Dejar un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Scroll al inicio