TL;DR — Resumo Rápido

Qual é o/a Generic Implementation of a Hashtable in C#. Guia técnico detalhado para profissionais de TI.

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

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
Desempenho 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");
}

Desempenho 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

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);

Resumo

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.