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:
- Perform Fast Lookups: Retrieving a value by its key is very efficient (typically O(1) time complexity).
- Store Associations: When you need to associate data with unique identifiers.
- Eliminate Duplicate Keys: Each key in a dictionary must be unique.
- 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:
- When you add a key-value pair, the key's hash code is computed.
- This hash code determines where in the internal array the value is stored.
- 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
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
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:
- Uniqueness: Each key must be unique within the dictionary.
- Immutability: The key's hash code should not change while it's in the dictionary.
- 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");
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:
-
Lookup Performance: Dictionary lookups are very fast (O(1) on average), making them ideal for frequent access by key.
-
Initial Capacity: Setting an appropriate initial capacity can reduce the overhead of resizing when adding many elements.
-
Key Selection: Choose keys that distribute hash codes evenly to minimize collisions.
-
Memory Usage: Dictionaries use more memory than arrays or lists due to their hash table structure.
-
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:
Collection | Strengths | Weaknesses | When to Use |
---|---|---|---|
Dictionary<TKey, TValue> | Fast lookups by key, flexible key-value storage | More memory usage, unordered | When you need to quickly find values by a unique key |
List<T> | Fast iteration, ordered elements, low overhead | Slow lookups for large collections | When order matters and you access elements sequentially |
HashSet<T> | Fast lookups, unique elements | No associated values, unordered | When you only need to track unique items without values |
SortedDictionary<TKey, TValue> | Ordered by key, fast lookups | Slower than Dictionary for most operations | When 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:
- A dictionary for fast achievement lookups by ID
- A dictionary to track progress for each achievement
- 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.