Avatar ·

Почему Dictionary<TKey, TValue> лучше, чем Hashtable, в C#?

Всем привет! Я недавно начал изучать C# и столкнулся с коллекциями. Вижу, что есть `Hashtable` (из `System.Collections`) и `Dictionary` (из `System.Collections.Generic`). Вроде оба хранят пары ключ-значение, но везде советуют использовать именно `Dictionary`. Я написал небольшой пример с `Hashtable`: ```csharp Hashtable table = new Hashtable(); table.Add("key1", 123); table.Add("key2", "строка"); foreach (DictionaryEntry entry in table) { Console.WriteLine($"{entry.Key}: {entry.Value}"); } ``` И с `Dictionary`: ```csharp Dictionary dict = new Dictionary(); dict.Add("key1", 123); dict.Add("key2", 456); // тут всё строго foreach (KeyValuePair kvp in dict) { Console.WriteLine($"{kvp.Key}: {kvp.Value}"); } ``` Но я не понимаю, почему `Dictionary` считается более предпочтительным. Ведь `Hashtable` проще в использовании — не нужно указывать типы, и он принимает любые объекты. В чём подвох? Есть ли причины, по которым стоит отказаться от `Hashtable`? Буду благодарен за объяснение с примерами! <<>>

Основные преимущества Dictionary перед Hashtable

Вы правы, оба класса решают одну задачу — хранение пар "ключ-значение" с быстрым доступом по ключу. Однако Dictionary<TKey, TValue> из пространства имён System.Collections.Generic значительно превосходит Hashtable из System.Collections по нескольким ключевым параметрам.

1. Типобезопасность (Type Safety)

Hashtable принимает любые объекты как ключи и значения. Это значит, что во время компиляции компилятор не может проверить, что вы кладёте правильные типы. Ошибка проявится только в рантайме:

Hashtable table = new Hashtable();
table.Add("key", 123);
int value = (int)table["key"]; // Работает

// А вот так — ошибка времени выполнения:
table.Add("anotherKey", "строка");
int anotherValue = (int)table["anotherKey"]; // InvalidCastException!

Dictionary<TKey, TValue> жёстко фиксирует типы на этапе компиляции:

Dictionary<string, int> dict = new Dictionary<string, int>();
dict.Add("key", 123);
// dict.Add("anotherKey", "строка"); // Ошибка компиляции!
int value = dict["key"]; // Без приведения типов

2. Производительность (Performance)

Hashtable работает с типами object, что вызывает упаковку (boxing) и распаковку (unboxing) для значимых типов (int, float, struct). Это создаёт лишнюю нагрузку на память и процессор. Dictionary использует generics и работает с конкретными типами без упаковки:

// Hashtable — упаковка при добавлении int
Hashtable ht = new Hashtable();
ht.Add(1, 2); // boxing ключа и значения

// Dictionary — без упаковки
Dictionary<int, int> dict = new Dictionary<int, int>();
dict.Add(1, 2); // никакого boxing

3. Отсутствие необходимости в приведении типов

При извлечении значений из Hashtable требуется явное приведение к нужному типу, что загромождает код и может привести к ошибкам. С Dictionary это не нужно:

// Hashtable
string name = (string)table["name"];

// Dictionary
string name = dict["name"]; // тип уже известен

4. Поддержка современных интерфейсов и LINQ

Dictionary реализует интерфейсы IDictionary<TKey, TValue>, IEnumerable<KeyValuePair<TKey, TValue>> и другие, что позволяет использовать LINQ и другие современные возможности C#:

Dictionary<string, int> scores = new() { {"Alice", 95}, {"Bob", 87} };
var topStudents = scores.Where(kvp => kvp.Value > 90);

// С Hashtable LINQ работает гораздо менее удобно
Hashtable htScores = new() { {"Alice", 95}, {"Bob", 87} };
var top = htScores.Cast<DictionaryEntry>().Where(de => (int)de.Value > 90);

5. Потокобезопасность

Hashtable допускает одновременное чтение несколькими потоками, но запись — только одним. Dictionary по умолчанию не потокобезопасен, но для многопоточных сценариев рекомендуется использовать ConcurrentDictionary<TKey, TValue> из System.Collections.Concurrent, который предоставляет более тонкие механизмы синхронизации.

Когда может пригодиться Hashtable?

Теоретически — в очень старом коде (legacy) или при работе с COM-объектами. На практике в современном C# (начиная с .NET Framework 2.0) Hashtable считается устаревшим. Даже в сценариях, где типы заранее неизвестны, лучше использовать Dictionary<object, object> или Dictionary<string, object> — это даст те же возможности, но с более строгим контролем типов.

Вывод: Всегда предпочитайте Dictionary<TKey, TValue> — это быстрее, безопаснее, лаконичнее и лучше интегрируется с современными возможностями языка.

<<>>

Дополнение: о производительности и хэшировании

Стоит добавить, что разница в производительности между Hashtable и Dictionary может быть существенной в высоконагруженных сценариях. Hashtable использует устаревший алгоритм хэширования и не поддерживает IEquatable<T> или оптимизированные хэш-функции для значимых типов.

Dictionary автоматически использует EqualityComparer<T>.Default, который для значимых типов (int, long, DateTime) вызывает оптимизированные методы без рефлексии. Например, для целых чисел хэш-функция вычисляется быстрее, чем в Hashtable, где каждый ключ обрабатывается через виртуальный метод GetHashCode() с упаковкой.

Кроме того, Dictionary позволяет задать собственную реализацию IEqualityComparer<TKey> через конструктор, что даёт гибкость при работе с составными ключами или нестандартными правилами сравнения. В Hashtable это тоже возможно, но через IEqualityComparer (без generic), что менее удобно и безопасно.

Ещё один важный момент: Dictionary имеет метод TryGetValue(), который позволяет безопасно проверить наличие ключа и получить значение за одну операцию, без двойного поиска (как в Hashtable, где нужно сначала вызвать ContainsKey(), а потом []).