Skip to main content

6.4 - Dictionary<TKey, TValue>

The Dictionary<TKey, TValue> is one of the most powerful and frequently used collection types in C#. It stores key-value pairs, allowing you to quickly look up values using their associated keys. This makes dictionaries ideal for scenarios where you need fast access to data based on a unique identifier.

Why Use Dictionary<TKey, TValue>?

Dictionaries excel in situations where you need to:

  1. Perform Fast Lookups: Retrieving a value by its key is very efficient (typically O(1) time complexity).
  2. Store Associations: When you need to associate data with unique identifiers.
  3. Eliminate Duplicate Keys: Each key in a dictionary must be unique.
  4. Track Relationships: When you need to map one type of data to another.

Common game development uses include:

  • Item databases (item ID → item data)
  • Character stats (stat name → stat value)
  • Object pools (object type → list of available objects)
  • Localization systems (text key → localized string)
  • Input mapping (key code → game action)

How Dictionaries Work

Under the hood, a dictionary uses a hash table data structure:

  1. When you add a key-value pair, the key's hash code is computed.
  2. This hash code determines where in the internal array the value is stored.
  3. When you look up a value by key, the same hash function quickly identifies where to find the value.

This approach enables dictionaries to provide near-constant time performance for adding, retrieving, and removing elements, regardless of the dictionary's size.

Creating and Initializing Dictionaries

There are several ways to create and initialize a Dictionary<TKey, TValue>:

Basic Creation

// Create an empty dictionary mapping strings to integers
Dictionary<string, int> scores = new Dictionary<string, int>();

// Create an empty dictionary mapping integers to strings
Dictionary<int, string> idToName = new Dictionary<int, string>();

// Create a dictionary with an initial capacity
Dictionary<string, bool> featureFlags = new Dictionary<string, bool>(100);

Initializing with Elements

// Initialize with collection initializer syntax
Dictionary<string, int> scores = new Dictionary<string, int>
{
{ "Alice", 95 },
{ "Bob", 87 },
{ "Charlie", 92 }
};

// Alternative syntax (C# 6.0 and later)
Dictionary<string, int> scores = new Dictionary<string, int>
{
["Alice"] = 95,
["Bob"] = 87,
["Charlie"] = 92
};

// Initialize from key-value pairs
var playerData = new[]
{
new KeyValuePair<string, int>("Alice", 95),
new KeyValuePair<string, int>("Bob", 87),
new KeyValuePair<string, int>("Charlie", 92)
};
Dictionary<string, int> scores = new Dictionary<string, int>(playerData);

Adding and Updating Elements

You can add or update elements in a dictionary in several ways:

Dictionary<string, int> inventory = new Dictionary<string, int>();

// Add a new key-value pair
inventory.Add("Health Potion", 5);

// Add using indexer syntax
inventory["Mana Potion"] = 3;

// Update an existing value
inventory["Health Potion"] = 7; // Changes the value from 5 to 7

// Add or update using a single method
inventory["Antidote"] = 2; // Adds if key doesn't exist, updates if it does

// TryAdd - only adds if the key doesn't exist (C# 8.0+)
bool added = inventory.TryAdd("Health Potion", 10); // Returns false, doesn't modify the value
caution

Using Add() with a key that already exists will throw an ArgumentException. If you're not sure whether a key exists, use the indexer syntax or TryAdd() instead.

Accessing Dictionary Elements

There are multiple ways to access values in a dictionary:

Dictionary<string, int> inventory = new Dictionary<string, int>
{
{ "Health Potion", 5 },
{ "Mana Potion", 3 },
{ "Antidote", 2 }
};

// Access using indexer syntax
int healthPotions = inventory["Health Potion"]; // 5

// TryGetValue - safer way to access values
if (inventory.TryGetValue("Elixir", out int elixirCount))
{
Console.WriteLine($"You have {elixirCount} elixirs");
}
else
{
Console.WriteLine("You don't have any elixirs");
}

// Check if a key exists
bool hasAntidote = inventory.ContainsKey("Antidote"); // true
bool hasElixir = inventory.ContainsKey("Elixir"); // false

// Check if a value exists
bool hasItemWithQuantityThree = inventory.ContainsValue(3); // true
caution

Using the indexer (dictionary[key]) with a non-existent key will throw a KeyNotFoundException. If you're not sure whether a key exists, use TryGetValue() instead.

Removing Elements

You can remove elements from a dictionary in several ways:

Dictionary<string, int> inventory = new Dictionary<string, int>
{
{ "Health Potion", 5 },
{ "Mana Potion", 3 },
{ "Antidote", 2 },
{ "Elixir", 1 }
};

// Remove a specific key-value pair
bool removed = inventory.Remove("Elixir"); // Returns true if the key was found and removed

// Try to remove and get the value at the same time (C# 8.0+)
if (inventory.Remove("Antidote", out int antidoteCount))
{
Console.WriteLine($"Removed {antidoteCount} antidotes");
}

// Clear all elements
inventory.Clear();

Iterating Through a Dictionary

There are several ways to iterate through the elements of a dictionary:

Dictionary<string, int> inventory = new Dictionary<string, int>
{
{ "Health Potion", 5 },
{ "Mana Potion", 3 },
{ "Antidote", 2 }
};

// Iterate through key-value pairs
foreach (KeyValuePair<string, int> item in inventory)
{
Console.WriteLine($"{item.Key}: {item.Value}");
}

// Using C# 7.0 deconstruction
foreach (var (itemName, quantity) in inventory)
{
Console.WriteLine($"{itemName}: {quantity}");
}

// Iterate through keys only
foreach (string itemName in inventory.Keys)
{
Console.WriteLine(itemName);
}

// Iterate through values only
foreach (int quantity in inventory.Values)
{
Console.WriteLine(quantity);
}

Dictionary Properties and Methods

Dictionary<TKey, TValue> provides several useful properties and methods:

Dictionary<string, int> inventory = new Dictionary<string, int>
{
{ "Health Potion", 5 },
{ "Mana Potion", 3 },
{ "Antidote", 2 }
};

// Count - number of key-value pairs
int itemCount = inventory.Count; // 3

// Keys and Values collections
ICollection<string> itemNames = inventory.Keys;
ICollection<int> quantities = inventory.Values;

// Convert to arrays
string[] itemNamesArray = inventory.Keys.ToArray();
int[] quantitiesArray = inventory.Values.ToArray();

// Check if empty
bool isEmpty = inventory.Count == 0;

Key Requirements

The key type in a dictionary must meet certain requirements:

  1. Uniqueness: Each key must be unique within the dictionary.
  2. Immutability: The key's hash code should not change while it's in the dictionary.
  3. Equality Implementation: The key type should properly implement equality comparison.

Built-in types like string, int, and Guid work well as keys. For custom types, you should override Equals() and GetHashCode() methods:

public class ItemId
{
public string Category { get; }
public int Id { get; }

public ItemId(string category, int id)
{
Category = category;
Id = id;
}

// Override Equals for proper equality comparison
public override bool Equals(object obj)
{
if (obj is ItemId other)
{
return Category == other.Category && Id == other.Id;
}
return false;
}

// Override GetHashCode for efficient hashing
public override int GetHashCode()
{
return HashCode.Combine(Category, Id);
}
}

// Using the custom type as a dictionary key
Dictionary<ItemId, string> itemNames = new Dictionary<ItemId, string>();
itemNames.Add(new ItemId("Weapon", 1), "Iron Sword");
itemNames.Add(new ItemId("Potion", 1), "Health Potion");
caution

Be careful when using mutable objects as dictionary keys. If an object's hash code changes after it's added to a dictionary, the dictionary may not be able to find it again.

Dictionary<TKey, TValue> in Game Development

Dictionaries are extensively used in game development. Here are some practical examples:

Example: Item Database

[System.Serializable]
public class Item
{
public int Id;
public string Name;
public string Description;
public Sprite Icon;
public int Value;
public ItemType Type;

public enum ItemType
{
Weapon,
Armor,
Potion,
Material,
Quest
}
}

public class ItemDatabase : MonoBehaviour
{
[SerializeField] private List<Item> allItems = new List<Item>();

// Dictionary for fast item lookups by ID
private Dictionary<int, Item> itemsById = new Dictionary<int, Item>();

// Dictionary for grouping items by type
private Dictionary<Item.ItemType, List<Item>> itemsByType = new Dictionary<Item.ItemType, List<Item>>();

void Awake()
{
// Initialize dictionaries
foreach (Item item in allItems)
{
// Add to ID lookup dictionary
if (!itemsById.ContainsKey(item.Id))
{
itemsById.Add(item.Id, item);
}
else
{
Debug.LogError($"Duplicate item ID found: {item.Id} for item {item.Name}");
}

// Add to type grouping dictionary
if (!itemsByType.ContainsKey(item.Type))
{
itemsByType[item.Type] = new List<Item>();
}
itemsByType[item.Type].Add(item);
}

Debug.Log($"Item database initialized with {itemsById.Count} items");
}

// Get an item by its ID
public Item GetItem(int id)
{
if (itemsById.TryGetValue(id, out Item item))
{
return item;
}

Debug.LogWarning($"Item with ID {id} not found");
return null;
}

// Get all items of a specific type
public List<Item> GetItemsByType(Item.ItemType type)
{
if (itemsByType.TryGetValue(type, out List<Item> items))
{
return new List<Item>(items); // Return a copy to prevent external modification
}

return new List<Item>(); // Return empty list if no items of this type
}
}

Example: Dialogue System

[System.Serializable]
public class DialogueLine
{
public string Id;
public string CharacterName;
public string Text;
public string[] Responses;
public string[] NextDialogueIds;
}

public class DialogueManager : MonoBehaviour
{
[SerializeField] private TextAsset dialogueJson;

// Dictionary to store all dialogue lines by ID
private Dictionary<string, DialogueLine> dialogueLines = new Dictionary<string, DialogueLine>();

// Dictionary to track which dialogues have been seen
private Dictionary<string, bool> seenDialogues = new Dictionary<string, bool>();

void Start()
{
// Load dialogue from JSON
DialogueLine[] lines = JsonUtility.FromJson<DialogueLine[]>(dialogueJson.text);

// Populate the dictionary
foreach (DialogueLine line in lines)
{
dialogueLines.Add(line.Id, line);
seenDialogues.Add(line.Id, false);
}

Debug.Log($"Loaded {dialogueLines.Count} dialogue lines");
}

// Start a dialogue by ID
public DialogueLine StartDialogue(string dialogueId)
{
if (dialogueLines.TryGetValue(dialogueId, out DialogueLine line))
{
seenDialogues[dialogueId] = true;
return line;
}

Debug.LogError($"Dialogue with ID {dialogueId} not found");
return null;
}

// Get the next dialogue based on response index
public DialogueLine GetNextDialogue(string currentDialogueId, int responseIndex)
{
if (dialogueLines.TryGetValue(currentDialogueId, out DialogueLine currentLine))
{
if (responseIndex >= 0 && responseIndex < currentLine.NextDialogueIds.Length)
{
string nextId = currentLine.NextDialogueIds[responseIndex];
return StartDialogue(nextId);
}
}

return null;
}

// Check if a dialogue has been seen
public bool HasSeenDialogue(string dialogueId)
{
if (seenDialogues.TryGetValue(dialogueId, out bool seen))
{
return seen;
}

return false;
}
}

Example: Input Mapping System

public enum GameAction
{
Move,
Jump,
Attack,
Interact,
Pause,
Inventory
}

public class InputMapper : MonoBehaviour
{
// Map keyboard keys to game actions
private Dictionary<KeyCode, GameAction> keyboardMap = new Dictionary<KeyCode, GameAction>
{
{ KeyCode.W, GameAction.Move },
{ KeyCode.Space, GameAction.Jump },
{ KeyCode.Mouse0, GameAction.Attack },
{ KeyCode.E, GameAction.Interact },
{ KeyCode.Escape, GameAction.Pause },
{ KeyCode.I, GameAction.Inventory }
};

// Map gamepad buttons to game actions
private Dictionary<string, GameAction> gamepadMap = new Dictionary<string, GameAction>
{
{ "LeftStick", GameAction.Move },
{ "ButtonSouth", GameAction.Jump },
{ "ButtonWest", GameAction.Attack },
{ "ButtonEast", GameAction.Interact },
{ "Start", GameAction.Pause },
{ "Select", GameAction.Inventory }
};

// Track which actions are currently active
private Dictionary<GameAction, bool> actionStates = new Dictionary<GameAction, bool>();

void Start()
{
// Initialize all action states to false
foreach (GameAction action in Enum.GetValues(typeof(GameAction)))
{
actionStates[action] = false;
}
}

void Update()
{
// Check keyboard inputs
foreach (var mapping in keyboardMap)
{
if (Input.GetKeyDown(mapping.Key))
{
actionStates[mapping.Value] = true;
OnActionTriggered(mapping.Value);
}
else if (Input.GetKeyUp(mapping.Key))
{
actionStates[mapping.Value] = false;
OnActionReleased(mapping.Value);
}
}

// Check gamepad inputs would go here
}

// Rebind a keyboard key to an action
public void RebindKeyboardAction(KeyCode key, GameAction action)
{
// Remove any existing bindings for this key
List<KeyCode> keysToRemove = new List<KeyCode>();
foreach (var mapping in keyboardMap)
{
if (mapping.Key == key)
{
keysToRemove.Add(mapping.Key);
}
}

foreach (KeyCode keyToRemove in keysToRemove)
{
keyboardMap.Remove(keyToRemove);
}

// Add the new binding
keyboardMap[key] = action;

Debug.Log($"Rebound {key} to {action}");
}

// Check if an action is currently active
public bool IsActionActive(GameAction action)
{
return actionStates.TryGetValue(action, out bool active) && active;
}

// Event handlers
private void OnActionTriggered(GameAction action)
{
Debug.Log($"Action triggered: {action}");
// Trigger events or callbacks here
}

private void OnActionReleased(GameAction action)
{
Debug.Log($"Action released: {action}");
// Trigger events or callbacks here
}
}

Performance Considerations

When working with dictionaries, keep these performance considerations in mind:

  1. Lookup Performance: Dictionary lookups are very fast (O(1) on average), making them ideal for frequent access by key.

  2. Initial Capacity: Setting an appropriate initial capacity can reduce the overhead of resizing when adding many elements.

  3. Key Selection: Choose keys that distribute hash codes evenly to minimize collisions.

  4. Memory Usage: Dictionaries use more memory than arrays or lists due to their hash table structure.

  5. Iteration: Iterating through a dictionary is slower than iterating through a list or array, so if you frequently need to process all elements, consider maintaining a separate list.

// If you need both fast lookups and frequent iteration
Dictionary<int, GameObject> objectsById = new Dictionary<int, GameObject>();
List<GameObject> allObjects = new List<GameObject>();

// When adding an object
void AddObject(int id, GameObject obj)
{
objectsById[id] = obj;
allObjects.Add(obj);
}

// When removing an object
void RemoveObject(int id)
{
if (objectsById.TryGetValue(id, out GameObject obj))
{
objectsById.Remove(id);
allObjects.Remove(obj);
}
}

Dictionary<TKey, TValue> vs. Other Collections

Here's how dictionaries compare to other collection types:

CollectionStrengthsWeaknessesWhen to Use
Dictionary<TKey, TValue>Fast lookups by key, flexible key-value storageMore memory usage, unorderedWhen you need to quickly find values by a unique key
List<T>Fast iteration, ordered elements, low overheadSlow lookups for large collectionsWhen order matters and you access elements sequentially
HashSet<T>Fast lookups, unique elementsNo associated values, unorderedWhen you only need to track unique items without values
SortedDictionary<TKey, TValue>Ordered by key, fast lookupsSlower than Dictionary for most operationsWhen you need key-value pairs sorted by key

Practical Example: Game Achievement System

Let's implement a game achievement system using dictionaries:

using System;
using System.Collections.Generic;
using UnityEngine;

[Serializable]
public class Achievement
{
public string Id;
public string Title;
public string Description;
public Sprite Icon;
public int PointValue;
public bool IsSecret;

[NonSerialized]
public bool IsUnlocked;

[NonSerialized]
public DateTime UnlockTime;
}

public class AchievementManager : MonoBehaviour
{
[SerializeField] private List<Achievement> achievementsList;

// Dictionary for fast achievement lookups by ID
private Dictionary<string, Achievement> achievements = new Dictionary<string, Achievement>();

// Dictionary to track progress for achievements that require multiple steps
private Dictionary<string, int> achievementProgress = new Dictionary<string, int>();

// Dictionary to track achievement requirements
private Dictionary<string, int> achievementRequirements = new Dictionary<string, int>
{
{ "KILL_ENEMIES", 100 },
{ "COLLECT_COINS", 1000 },
{ "COMPLETE_LEVELS", 10 }
};

void Awake()
{
// Initialize dictionaries
foreach (Achievement achievement in achievementsList)
{
achievements.Add(achievement.Id, achievement);
achievementProgress[achievement.Id] = 0;
}

// Load saved achievement data
LoadAchievements();
}

// Update progress for an achievement
public void UpdateProgress(string achievementId, int increment = 1)
{
if (!achievements.ContainsKey(achievementId))
{
Debug.LogWarning($"Achievement {achievementId} not found");
return;
}

// Update progress
achievementProgress[achievementId] += increment;

// Check if the achievement should be unlocked
if (achievementRequirements.TryGetValue(achievementId, out int requirement))
{
if (achievementProgress[achievementId] >= requirement && !achievements[achievementId].IsUnlocked)
{
UnlockAchievement(achievementId);
}
}

Debug.Log($"Achievement {achievementId} progress: {achievementProgress[achievementId]}/{requirement}");
}

// Unlock an achievement
public void UnlockAchievement(string achievementId)
{
if (!achievements.ContainsKey(achievementId))
{
Debug.LogWarning($"Achievement {achievementId} not found");
return;
}

Achievement achievement = achievements[achievementId];

if (!achievement.IsUnlocked)
{
achievement.IsUnlocked = true;
achievement.UnlockTime = DateTime.Now;

// Display notification
DisplayAchievementNotification(achievement);

// Save achievement data
SaveAchievements();

Debug.Log($"Achievement unlocked: {achievement.Title}");
}
}

// Get all unlocked achievements
public List<Achievement> GetUnlockedAchievements()
{
List<Achievement> unlockedAchievements = new List<Achievement>();

foreach (var achievement in achievements.Values)
{
if (achievement.IsUnlocked)
{
unlockedAchievements.Add(achievement);
}
}

return unlockedAchievements;
}

// Get achievement progress
public float GetAchievementProgress(string achievementId)
{
if (achievements.TryGetValue(achievementId, out Achievement achievement))
{
if (achievement.IsUnlocked)
{
return 1.0f;
}

if (achievementRequirements.TryGetValue(achievementId, out int requirement) && requirement > 0)
{
return (float)achievementProgress[achievementId] / requirement;
}
}

return 0f;
}

// Display achievement notification
private void DisplayAchievementNotification(Achievement achievement)
{
// Implementation would depend on your UI system
Debug.Log($"Achievement Unlocked: {achievement.Title} - {achievement.Description}");
}

// Save achievement data
private void SaveAchievements()
{
// Implementation would depend on your save system
// For example, using PlayerPrefs:
foreach (var achievement in achievements.Values)
{
PlayerPrefs.SetInt($"Achievement_{achievement.Id}_Unlocked", achievement.IsUnlocked ? 1 : 0);
PlayerPrefs.SetInt($"Achievement_{achievement.Id}_Progress", achievementProgress[achievement.Id]);
}
PlayerPrefs.Save();
}

// Load achievement data
private void LoadAchievements()
{
// Implementation would depend on your save system
// For example, using PlayerPrefs:
foreach (var achievement in achievements.Values)
{
achievement.IsUnlocked = PlayerPrefs.GetInt($"Achievement_{achievement.Id}_Unlocked", 0) == 1;
achievementProgress[achievement.Id] = PlayerPrefs.GetInt($"Achievement_{achievement.Id}_Progress", 0);
}
}
}

This achievement system demonstrates several uses of dictionaries:

  1. A dictionary for fast achievement lookups by ID
  2. A dictionary to track progress for each achievement
  3. A dictionary to store the requirements for each achievement

The dictionaries enable efficient updates and lookups, which is important for a system that might be accessed frequently during gameplay.

Conclusion

Dictionary<TKey, TValue> is a powerful and versatile collection type that excels at storing and retrieving key-value pairs with fast lookups. It's an essential tool in any C# developer's toolkit, particularly for game development scenarios where performance and data organization are critical.

Key points to remember:

  • Dictionaries provide fast lookups by key (typically O(1))
  • Each key must be unique within the dictionary
  • Keys should be immutable while in the dictionary
  • Dictionaries are unordered by default
  • They're ideal for mapping identifiers to data or tracking relationships between different types of data

In the next section, we'll explore Queue<T>, a collection type designed for first-in, first-out (FIFO) operations.