When working with key-value pair collections in C#, the non-generic Hashtable class from System.Collections was the original go-to data structure. However, since the introduction of generics in .NET 2.0, the Dictionary<TKey, TValue> class has become the preferred generic implementation. This article covers both classes, their differences, and when to use each.
The Non-Generic Hashtable
The Hashtable class was part of the original .NET Framework in the System.Collections namespace. It stores key-value pairs where both keys and values are typed as object:
using System.Collections;
Hashtable table = new Hashtable();
table.Add("Name", "John");
table.Add("Age", 30);
// Retrieval requires casting
string name = (string)table["Name"];
int age = (int)table["Age"];
The problems with Hashtable include:
- No type safety - Keys and values are stored as
object, so any type can be inserted. Type errors are only caught at runtime. - Boxing/unboxing overhead - Value types (like
int,bool,double) must be boxed when stored and unboxed when retrieved, which has a performance cost. - No IntelliSense - Since everything is
object, your IDE cannot provide meaningful code completion. - Runtime casting errors - An
InvalidCastExceptioncan occur if you cast to the wrong type.
The Generic Dictionary
Dictionary<TKey, TValue> is the generic replacement for Hashtable, found in the System.Collections.Generic namespace:
using System.Collections.Generic;
Dictionary<string, int> ages = new Dictionary<string, int>();
ages.Add("Alice", 30);
ages.Add("Bob", 25);
// No casting required - type safe at compile time
int aliceAge = ages["Alice"];
The type parameters enforce compile-time type safety. You cannot accidentally insert a value of the wrong type:
Dictionary<string, int> ages = new Dictionary<string, int>();
ages.Add("Alice", 30);
// ages.Add("Bob", "twenty-five"); // Compile error - string is not int
Key Differences Between Hashtable and Dictionary
| Feature | Hashtable | Dictionary<TKey, TValue> |
|---|---|---|
| Namespace | System.Collections | System.Collections.Generic |
| Type Safety | None (stores object) | Compile-time type checking |
| Boxing/Unboxing | Yes, for value types | No |
| Null Keys | Not allowed (throws) | Not allowed (throws) |
| Null Values | Allowed | Allowed for reference types |
| Thread Safety | Partially thread-safe for readers | Not thread-safe |
| Missing Key Behavior | Returns null | Throws KeyNotFoundException |
| Leistung | Slower due to boxing | Faster with value types |
| Enumeration Type | DictionaryEntry | KeyValuePair<TKey, TValue> |
Missing Key Behavior
One important behavioral difference is what happens when you access a key that does not exist:
// Hashtable returns null for missing keys
Hashtable ht = new Hashtable();
object value = ht["missing"]; // Returns null
// Dictionary throws an exception for missing keys
Dictionary<string, int> dict = new Dictionary<string, int>();
// int val = dict["missing"]; // Throws KeyNotFoundException
With Dictionary, the safe approach is to use TryGetValue:
Dictionary<string, int> dict = new Dictionary<string, int>();
dict["Alice"] = 30;
if (dict.TryGetValue("Alice", out int age))
{
Console.WriteLine($"Alice is {age} years old");
}
else
{
Console.WriteLine("Alice not found");
}
Leistung Characteristics
Both Hashtable and Dictionary use hash-based storage, providing similar algorithmic complexity:
| Operation | Average Case | Worst Case |
|---|---|---|
| Add | O(1) | O(n) |
| Remove | O(1) | O(n) |
| Lookup by Key | O(1) | O(n) |
| ContainsKey | O(1) | O(n) |
| ContainsValue | O(n) | O(n) |
The worst case of O(n) occurs when there are many hash collisions. In practice, with a good hash function and a reasonable load factor, operations are effectively O(1).
However, Dictionary<TKey, TValue> outperforms Hashtable when working with value types because it avoids the boxing and unboxing overhead:
// Hashtable: int is boxed to object on insert, unboxed on retrieval
Hashtable ht = new Hashtable();
ht.Add(1, 100); // Boxing occurs for both key and value
int val = (int)ht[1]; // Unboxing occurs
// Dictionary: no boxing or unboxing
Dictionary<int, int> dict = new Dictionary<int, int>();
dict.Add(1, 100); // No boxing
int val2 = dict[1]; // No unboxing
Common Dictionary Patterns
Initializing with Collection Initializer
var config = new Dictionary<string, string>
{
{ "Host", "localhost" },
{ "Port", "5432" },
{ "Database", "mydb" }
};
Using Index Initializer (C# 6+)
var config = new Dictionary<string, string>
{
["Host"] = "localhost",
["Port"] = "5432",
["Database"] = "mydb"
};
GetValueOrDefault (C# 7.1+ / .NET Core 2.0+)
var scores = new Dictionary<string, int>
{
["Alice"] = 95,
["Bob"] = 87
};
int charlieScore = scores.GetValueOrDefault("Charlie", 0); // Returns 0
Counting Occurrences
string text = "hello world";
var charCounts = new Dictionary<char, int>();
foreach (char c in text)
{
if (charCounts.ContainsKey(c))
charCounts[c]++;
else
charCounts[c] = 1;
}
Using a Custom Equality Comparer
// Case-insensitive string dictionary
var dict = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
dict["Name"] = "Alice";
Console.WriteLine(dict["name"]); // Outputs: Alice
Related Generic Collections
Beyond Dictionary<TKey, TValue>, there are other generic collections worth knowing:
HashSet<T>- A collection of unique elements with no key-value pairing. Use it when you only need to check membership (whether an item exists in the set).SortedDictionary<TKey, TValue>- Similar toDictionarybut maintains keys in sorted order using a binary search tree. Lookups are O(log n) instead of O(1).ConcurrentDictionary<TKey, TValue>- A thread-safe dictionary fromSystem.Collections.Concurrent, suitable for multi-threaded scenarios.ImmutableDictionary<TKey, TValue>- An immutable dictionary fromSystem.Collections.Immutablethat cannot be modified after creation.
Thread Safety
Neither Hashtable nor Dictionary is fully thread-safe for concurrent writes. If you need thread-safe access, use ConcurrentDictionary<TKey, TValue>:
using System.Collections.Concurrent;
var concurrentDict = new ConcurrentDictionary<string, int>();
concurrentDict.TryAdd("Alice", 30);
concurrentDict.AddOrUpdate("Alice", 31, (key, oldValue) => oldValue + 1);
Zusammenfassung
For any new .NET development, Dictionary<TKey, TValue> is the standard choice for hash-based key-value storage. It provides compile-time type safety, better performance with value types, and a rich API. The non-generic Hashtable should only be encountered in legacy codebases. When choosing between related collections, use Dictionary for key-value lookups, HashSet<T> for unique element membership testing, SortedDictionary when you need sorted keys, and ConcurrentDictionary for thread-safe scenarios.