C# Dictionaries

A C# dictionary stores values by key, like a small lookup table inside your program. Instead of asking for item number 3, you ask for the value connected to a key such as "red", 42, or a user ID. Dictionaries matter because they make many searches fast and direct: find a price by product name, a count by word, or a setting by option name.

Overview: How Dictionaries Work

The main dictionary type is Dictionary<TKey,TValue> from System.Collections.Generic. TKey is the type of the key, and TValue is the type of the stored value. For example, Dictionary<string,int> maps strings to integers, while Dictionary<int,string> maps integers to strings.

A dictionary is useful when each item has a unique name, code, or identifier. A list is good when order and position matter. A dictionary is better when the question is, “Do I have this key, and what value belongs to it?”

Internally, Dictionary<TKey,TValue> is a hash table. When you add a key, the dictionary asks the key for a hash code by calling GetHashCode(). That hash code helps choose a bucket, which is a storage location inside the dictionary. If two different keys land in the same bucket, the dictionary also compares keys with Equals() to find the exact match. This is why dictionary lookup is usually very fast, often close to constant time, but it also means keys must have stable equality behavior.

Keys must be unique. A dictionary cannot contain two entries with the same key. Values do not have to be unique; many different keys can point to the same value. Dictionaries also do not sort entries by key. Modern .NET preserves insertion order during enumeration as an implementation behavior, but you should not use a regular dictionary as your sorting tool. If sorted order is the real requirement, use sorting or a sorted collection instead.

Dictionaries are reference type objects. The variable stores a reference to the dictionary object, and the entries live inside that object. As entries are added, the dictionary may grow its internal storage. That resizing costs work at the moment it happens, but it keeps future lookups efficient.

Syntax

Dictionary<string,int> name = new Dictionary<string,int>();
name.Add("one", 1);
int value = name["one"];
Part Meaning
TKey The type used to find entries, such as string or int.
TValue The type stored for each key, such as decimal, bool, or List<string>.
Add Adds a new key-value pair and throws an exception if the key already exists.
[] Reads or assigns a value by key. Reading a missing key throws an exception.
TryGetValue Safely checks for a key and reads its value in one lookup.

C# also supports collection initializer syntax, which is often clearer for small fixed dictionaries.

var scores = new Dictionary<string,int>
{
    ["Ava"] = 95,
    ["Noah"] = 88
};

Examples

Example 1: Store And Read Prices

using System;
using System.Collections.Generic;

class Program
{
    static void Main()
    {
        Dictionary<string, decimal> prices = new Dictionary<string, decimal>();

        prices.Add("notebook", 4.99m);
        prices.Add("pen", 1.25m);
        prices.Add("folder", 2.50m);

        Console.WriteLine($"Pen price: ${prices["pen"]}");
        Console.WriteLine($"Items in dictionary: {prices.Count}");
    }
}

Output:

Pen price: $1.25
Items in dictionary: 3

This program creates a dictionary whose keys are product names and whose values are decimal prices. The expression prices["pen"] uses the key "pen" to retrieve its value directly. Notice the m suffix on decimal literals; it tells C# these numbers are decimal, which is a good type for money-like values.

Example 2: Count Repeated Words

using System;
using System.Collections.Generic;

class Program
{
    static void Main()
    {
        string[] words = { "red", "blue", "red", "green", "blue", "red" };
        Dictionary<string, int> counts = new Dictionary<string, int>();

        foreach (string word in words)
        {
            if (counts.ContainsKey(word))
            {
                counts[word]++;
            }
            else
            {
                counts[word] = 1;
            }
        }

        foreach (KeyValuePair<string, int> item in counts)
        {
            Console.WriteLine($"{item.Key}: {item.Value}");
        }
    }
}

Output:

red: 3
blue: 2
green: 1

This is a classic dictionary use case. Each word becomes a key, and the value stores how many times the word has appeared. If the key already exists, the program increments the existing count. If not, it creates a new entry with count 1.

Example 3: Safer Lookup With TryGetValue

using System;
using System.Collections.Generic;

class Program
{
    static void Main()
    {
        var extensions = new Dictionary<string, string>
        {
            ["cs"] = "C# source file",
            ["json"] = "JSON data file",
            ["md"] = "Markdown document"
        };

        string lookup = "xml";

        if (extensions.TryGetValue(lookup, out string? description))
        {
            Console.WriteLine(description);
        }
        else
        {
            Console.WriteLine($"No description for .{lookup}");
        }
    }
}

Output:

No description for .xml

TryGetValue is the preferred way to read a value when the key might not exist. It returns true when the key is found and places the value into the out variable. It returns false when the key is missing, avoiding the exception that the indexer would throw.

Example 4: Updating And Removing Entries

using System;
using System.Collections.Generic;

class Program
{
    static void Main()
    {
        var inventory = new Dictionary<string, int>
        {
            ["apples"] = 10,
            ["bananas"] = 6,
            ["oranges"] = 8
        };

        inventory["bananas"] = 9;
        inventory.Remove("oranges");
        inventory.TryAdd("pears", 4);

        foreach (var pair in inventory)
        {
            Console.WriteLine($"{pair.Key}: {pair.Value}");
        }
    }
}

Output:

apples: 10
bananas: 9
pears: 4

Assigning through the indexer updates an existing key, or creates it if it does not already exist. Remove deletes an entry by key and returns whether anything was removed. TryAdd adds only when the key is absent, which is useful when overwriting would be a bug.

How It Works Step By Step

  1. When you create a dictionary, it starts with internal arrays that hold buckets and entries.
  2. When you add a key, the dictionary computes the key’s hash code and maps it to a bucket.
  3. If the bucket is empty, the entry can be stored quickly. If the bucket already has entries, the dictionary checks equality until it finds the matching key or confirms the key is new.
  4. When you read by key, the dictionary repeats the hash-and-compare process to locate the entry.
  5. When the dictionary grows beyond its current capacity, it allocates larger storage and redistributes entries into new buckets.

This design is why key type matters. Built-in types like string, int, and Guid already have sensible hash and equality behavior. If you use your own class as a key, you must think carefully about Equals and GetHashCode. Records are often convenient keys because C# gives them value-based equality by default.

String keys are case-sensitive by default. The keys "Admin" and "admin" are different. If you want case-insensitive string keys, pass a comparer when creating the dictionary, such as StringComparer.OrdinalIgnoreCase.

Common Mistakes

Reading A Missing Key With The Indexer

var ages = new Dictionary<string, int>();
Console.WriteLine(ages["Mina"]);

This compiles, but it fails at runtime with KeyNotFoundException because "Mina" is not in the dictionary. Use TryGetValue when missing keys are normal.

using System;
using System.Collections.Generic;

class Program
{
    static void Main()
    {
        var ages = new Dictionary<string, int>();

        if (ages.TryGetValue("Mina", out int age))
        {
            Console.WriteLine(age);
        }
        else
        {
            Console.WriteLine("Age is not available.");
        }
    }
}

Output:

Age is not available.

Adding The Same Key Twice

var codes = new Dictionary<string, string>();
codes.Add("US", "United States");
codes.Add("US", "United States of America");

Add is strict: it throws an exception if the key already exists. If replacing is intended, assign with the indexer. If replacement is not intended, use TryAdd and handle the false result.

using System;
using System.Collections.Generic;

class Program
{
    static void Main()
    {
        var codes = new Dictionary<string, string>();
        codes["US"] = "United States";
        codes["US"] = "United States of America";

        Console.WriteLine(codes["US"]);
    }
}

Output:

United States of America

Changing A Dictionary While Looping Over It

foreach (var pair in inventory)
{
    if (pair.Value == 0)
    {
        inventory.Remove(pair.Key);
    }
}

Changing the dictionary during a foreach enumeration can throw an exception because the enumerator expects the collection shape to remain stable. Collect the keys first, then remove them.

using System;
using System.Collections.Generic;

class Program
{
    static void Main()
    {
        var inventory = new Dictionary<string, int>
        {
            ["apples"] = 3,
            ["bananas"] = 0,
            ["pears"] = 0
        };

        var emptyItems = new List<string>();

        foreach (var pair in inventory)
        {
            if (pair.Value == 0)
            {
                emptyItems.Add(pair.Key);
            }
        }

        foreach (string key in emptyItems)
        {
            inventory.Remove(key);
        }

        Console.WriteLine(inventory.Count);
    }
}

Output:

1

Best Practices

  • Use TryGetValue when a missing key is expected or acceptable.
  • Use the indexer for intentional add-or-replace behavior.
  • Use Add or TryAdd when duplicate keys should be treated carefully.
  • Choose key types with stable equality. Avoid mutable objects as keys unless you fully control their equality behavior.
  • For case-insensitive text keys, create the dictionary with StringComparer.OrdinalIgnoreCase.
  • Do not rely on a regular dictionary for sorted output; sort the keys or use a sorted collection.
  • If you know the approximate number of entries, pass an initial capacity to reduce resizing.
  • Do not modify a dictionary while enumerating it. Gather changes first, then apply them.

Practice Exercises

  1. Create a Dictionary<string,int> that stores three course names and the number of lessons in each. Print each course and count.
  2. Write a program that counts how many times each character appears in the string "committee". Hint: use the character as the key.
  3. Create a case-insensitive dictionary of file extensions. Make "CS" and "cs" find the same value.

Summary

  • A dictionary stores key-value pairs and finds values by unique key.
  • Dictionary<TKey,TValue> is a generic hash table, so lookups are usually very fast.
  • Keys must be unique, but values may repeat.
  • The indexer reads, adds, or replaces values, but reading a missing key throws an exception.
  • TryGetValue, TryAdd, ContainsKey, Remove, Keys, and Values are core dictionary tools.
  • Good key equality and clear duplicate-key handling are the difference between reliable dictionary code and subtle bugs.