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 InvalidCastException can 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

FeatureHashtableDictionary<TKey, TValue>
NamespaceSystem.CollectionsSystem.Collections.Generic
Type SafetyNone (stores object)Compile-time type checking
Boxing/UnboxingYes, for value typesNo
Null KeysNot allowed (throws)Not allowed (throws)
Null ValuesAllowedAllowed for reference types
Thread SafetyPartially thread-safe for readersNot thread-safe
Missing Key BehaviorReturns nullThrows KeyNotFoundException
PerformanceSlower due to boxingFaster with value types
Enumeration TypeDictionaryEntryKeyValuePair<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");
}

Performance Characteristics

Both Hashtable and Dictionary use hash-based storage, providing similar algorithmic complexity:

OperationAverage CaseWorst Case
AddO(1)O(n)
RemoveO(1)O(n)
Lookup by KeyO(1)O(n)
ContainsKeyO(1)O(n)
ContainsValueO(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

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 to Dictionary but 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 from System.Collections.Concurrent, suitable for multi-threaded scenarios.
  • ImmutableDictionary<TKey, TValue> - An immutable dictionary from System.Collections.Immutable that 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);

Summary

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.