Dictionary vs Hashtable: batalla de colecciones en C#
\n\nImagínese la situación: está escribiendo código en C# y necesita guardar y recuperar datos rápidamente por clave. Abre la documentación y ve dos clases similares: Dictionary y Hashtable. Ambas funcionan bajo el principio "clave-valor", ambas prometen acceso ultrarrápido a los datos. Pero, ¿cuál elegir?
Si se ha hecho esta pregunta, no está solo. En Stack Overflow, el tema "Why is Dictionary preferred over Hashtable in C#?" ha acumulado miles de visitas y cientos de votos. Y hoy resolveremos esta pregunta de una vez por todas. Spoiler: Dictionary casi siempre gana. Pero analicemos por qué.
\n\nBreve recorrido histórico: ¿de dónde vino Hashtable?
\n\nPara entender la esencia del enfrentamiento, debemos mirar la historia de C#. Cuando la plataforma .NET apareció por primera vez (versión 1.0), los desarrolladores tenían disponible la clase Hashtable del espacio de nombres System.Collections. Era una tabla hash clásica que podía almacenar cualquier objeto. Suena genial, ¿verdad? Pero había un problema: la falta de tipificación.
Con el lanzamiento de .NET 2.0 y la llegada de las genéricos (generics), Microsoft presentó una nueva colección: Dictionary<TKey, TValue> del espacio de nombres System.Collections.Generic. Y ahí comenzó la verdadera magia.
Seguridad de tipos: la principal ventaja de Dictionary
\n\nVeamos un ejemplo concreto de código. Así es como se trabaja con Hashtable:
// Forma antigua: Hashtable\nHashtable hashtable = new Hashtable();\nhashtable.Add("clave1", 123);\nhashtable.Add("clave2", "cadena"); // ¿Error? ¡No, se compilará!\nhashtable.Add(456, "otra cadena"); // La clave también puede ser cualquier objeto\n\n// Ahora intentemos obtener los datos\nint value = (int)hashtable["clave2"]; // ¡Ups! InvalidCastException en tiempo de ejecución\n\n¿Ve el problema? Hashtable acepta cualquier tipo, tanto para claves como para valores. El compilador dejará pasar cualquier error, y se enterará de él solo durante la ejecución del programa. Este es un camino directo a errores difíciles de detectar.
\n\nAhora veamos Dictionary:
\n\n// Forma nueva y correcta: Dictionary\nDictionary<string, int> dictionary = new Dictionary<string, int>();\ndictionary.Add("clave1", 123);\n// dictionary.Add("clave2", "cadena"); // ¡Error de compilación! El tipo no coincide\n// dictionary.Add(456, "otra cadena"); // También error: la clave debe ser string\n\n// Recuperamos datos sin conversiones\nint value = dictionary["clave1"]; // Limpio, seguro, elegante\n\nDictionary es seguro en cuanto a tipos. Usted especifica explícitamente qué tipos tendrán las claves y los valores. El compilador lo verifica en tiempo de compilación. Sin excepciones inesperadas durante la ejecución: el código funciona o no se compila.
\n\nRendimiento: ¿quién es más rápido?
\n\nEl segundo aspecto importante es el rendimiento. Y aquí Dictionary vuelve a tomar la delantera. ¿Por qué? Todo se debe al boxing y unboxing.
\n\nCuando trabaja con Hashtable, todos los tipos de valor (int, double, struct, etc.) se empaquetan en objetos. Esto genera carga en el recolector de basura y ralentiza el trabajo. Verifiquemos en la práctica:
\n\n// Hashtable: boxing/unboxing\nHashtable ht = new Hashtable();\nStopwatch sw = Stopwatch.StartNew();\nfor (int i = 0; i < 1000000; i++)\n{\n ht.Add(i, i); // Cada int se empaqueta en object\n}\nsw.Stop();\nConsole.WriteLine($"Hashtable inserción: {sw.ElapsedMilliseconds} ms");\n\n// Dictionary: sin boxing\nDictionary<int, int> dict = new Dictionary<int, int>();\nsw.Restart();\nfor (int i = 0; i < 1000000; i++)\n{\n dict.Add(i, i); // Sin boxing: trabajamos con int directamente\n}\nsw.Stop();\nConsole.WriteLine($"Dictionary inserción: {sw.