Updated on
ContainsKey() tells us whether a key is in a dictionary: it takes the key, returns true or false, and costs one hash lookup.
If we also want the value, TryGetValue() is the call to reach for. It answers the same question and hands the value back through an out parameter, in one lookup instead of two. If we want to add the key when it is missing, TryAdd() does the check and the insert together.
Which of the three we pick depends on what we do next, and the last section on this page covers the part that catches people out: what “the key exists” actually means once the key is a class of our own rather than a string. If we need the ground floor first, our guide covers what a Dictionary<TKey, TValue> is and how to build one.
What Does ContainsKey() Do in C#?
ContainsKey() takes a key and returns true when the dictionary holds it and false when it does not. It never throws for a missing key and it never changes the dictionary.
The check costs one hash lookup, so it takes the same time on a dictionary of four entries as on one of four million. That is the whole reason to reach for a dictionary rather than a list.
Keys are compared the way the key type defines equality. For string keys that means an exact, case-sensitive match by default, so "Banana" and "banana" are two different keys.
ContainsKey() answers one question and only that question: is the key there. It does not hand back the value, so code that needs the value calls the indexer straight afterwards and pays for a second lookup on the same key.
Removing that second lookup is what TryGetValue() is for, which makes ContainsKey() the right call only when the answer really is just yes or no.
Let’s start with a simple dictionary to check against:
Dictionary<string, int> MyDictionary = new Dictionary<string, int>()
{
{ "Apple", 3 },
{ "Banana", -2 },
{ "Orange", 5 },
{ "Pear", 2 }
};
bool DictionaryHasBanana = MyDictionary.ContainsKey("Banana"); // true
bool DictionaryHasKiwi = MyDictionary.ContainsKey("Kiwi"); // false
bool DictionaryHasLowercaseBanana = MyDictionary.ContainsKey("banana"); // false
Calling ContainsKey() on a dictionary returns true if the given key is present or false otherwise; this particular dictionary contains four unique keys and their corresponding values, so Kiwi is not a key and Banana is a key (but not banana, as strings are case sensitive).
When Should We Use ContainsKey()?
Use ContainsKey() when the bool is the point: branching on whether a key is present, validating input, or reporting what a lookup would find without fetching anything.
Do not use it as a guard before reading the value. The indexer throws KeyNotFoundException for a key that is not there, so wrapping it in an if is correct, but it hashes the same key twice to answer one question.
Do not use it as a guard before adding, either. TryAdd() does the check and the insert in one call, and the next section covers it.
Assigning through the indexer needs no guard at all. Writing dictionary[key] = value adds the key when it is absent and overwrites the value when it is present, which is not what Add() does: Add() throws when the key already exists.
So the rule is narrower than it looks. ContainsKey() when we want a bool and nothing else, and one of the Try methods every other time.
If what we want is a fallback value rather than a bool, our guide shows how to return a default value instead of branching on a missing key. Here is the check-then-assign pattern the section is really about:
Dictionary<string, int> SetOnceDictionary = new Dictionary<string, int>()
{
{ "Apple", 2 },
{ "Banana", 3 }
};
if (!SetOnceDictionary.ContainsKey("Apple"))
{
SetOnceDictionary["Apple"] = 4;
}
if (!SetOnceDictionary.ContainsKey("Kiwi"))
{
SetOnceDictionary["Kiwi"] = 4;
}
It’s not strictly necessary to check if the dictionary already contains a key before assigning a value to it, unless we want to update the value stored against a key depending on whether the key exists or not. Since Apple exists but Kiwi does not, the resulting dictionary will keep its Apple as-is and have one new key-value pair:
{ "Apple", 2 },
{ "Banana", 3 },
{ "Kiwi", 4 }
However, if we want to use a dictionary key we must check for its existence, lest we get a KeyNotFoundException in case it doesn’t:
Dictionary<string, int> MyDictionary = new Dictionary<string, int>()
{
{ "Apple", 3 },
{ "Banana", -2 },
{ "Orange", 5 },
{ "Pear", 2 }
};
// Works, but risky!
int apples = MyDictionary["Apple"]; // 3
// Throws an exception!
int kiwis = MyDictionary["Kiwi"];
int oranges = 0;
if (MyDictionary.ContainsKey("Orange"))
{
oranges = MyDictionary["Orange"]; // 5
}
Even though it’s clear in the example that the dictionary contains the Apple key, it’s good practice to always wrap our code with if (...ContainsKey(...)) if we want to use the dictionary on the right-hand side of the equality sign.
That’s because, in the generic expression lhs = rhs, we are assigning the value of rhs to an element named lhs, and so rhs must be well-defined for the expression to make sense.
How Do We Add a Key Only if It Does Not Exist?
TryAdd() adds the pair when the key is absent and does nothing when it is present. It returns true on the insert and false when the key was already there, so one call both tests and acts.
That replaces the ContainsKey()-then-assign pattern above, which reads the same way but hashes the key twice and leaves a gap between the test and the write.
The gap matters as soon as the dictionary is shared. Two threads can both see a missing key and both write it, and a Dictionary<TKey, TValue> is not safe for concurrent writes, so the answer there is a ConcurrentDictionary<TKey, TValue> rather than a tighter check.
Add() is the one to avoid for this job. It throws an ArgumentException when the key already exists, which turns an ordinary “it was already there” into an exception the caller has to catch, and the message is the one people paste into a search box: an item with the same key has already been added.
Microsoft says the same thing about the shared case, and names the type to reach for: if several threads write to the same instance, use a ConcurrentDictionary for a dictionary shared across threads rather than trying to make the check itself tighter. On a single thread, TryAdd() is the whole answer:
var inventory = new Dictionary<string, int>()
{
{ "Apple", 2 },
{ "Banana", 3 }
};
var appleAdded = inventory.TryAdd("Apple", 4); // false, "Apple" is already there and keeps its 2
var kiwiAdded = inventory.TryAdd("Kiwi", 4); // true, "Kiwi" is inserted
TryAdd() has been available since .NET Core 2.0 and .NET Standard 2.1, so it is on every runtime worth targeting today.
How Does TryGetValue() Check and Fetch in One Step?
TryGetValue() asks whether the key exists and hands back its value in the same call. It returns a bool and writes the value into an out parameter, so a single hash lookup answers both questions.
When the key is missing it returns false and sets the out parameter to the default for its type: 0 for an int, null for a reference type. That default is not a signal on its own, because a stored value of 0 looks exactly the same, which is why the return value is the thing to branch on and the out value is not.
Declaring the variable inline keeps the call to one line. dictionary.TryGetValue("Apple", out int apples) declares apples and fills it in the same statement.
It is not faster than ContainsKey() on its own. Both are one lookup. What it is faster than is ContainsKey() followed by the indexer, which is the two-lookup pattern it exists to replace.
Let’s see it against the same dictionary:
Dictionary<string, int> MyDictionary = new Dictionary<string, int>()
{
{ "Apple", 3 },
{ "Banana", -2 },
{ "Orange", 5 },
{ "Pear", 2 }
};
int apples = 0;
bool ApplesSuccess = MyDictionary.TryGetValue("Apple", out apples); // ApplesSuccess == true, apples == 3
int kiwi = 0;
bool KiwisSuccess = MyDictionary.TryGetValue("Kiwi", out kiwi); // KiwisSuccess == false, kiwi == 0
TryGetValue(key, out value) has everything that ContainsKey(key) has. Its first parameter represents the key we want to look for and it similarly returns true or false depending on whether the key exists or not.
However, it also has another feature: it has a second parameter with the out keyword. When we pass an object to it, the function will modify it. In the example, apples is 3 because that’s the value mapped to the Apple key.
If we use TryGetValue() on a key that does not exist, the out value will be set to the default value for its type. In the kiwi example, kiwi is 0 because that’s the default value for its type, which is int.
TryGetValue() is the one to reach for when we need the value, because it replaces a ContainsKey() call plus an indexer read, which hash the same key twice.
Declare the Value in TryGetValue()
We can make the code even more succinct if we declare the out variable right in the function call:
// Same process as before, but more succinct:
bool ApplesSuccess = MyDictionary.TryGetValue("Apple", out int apples);
bool KiwisSuccess = MyDictionary.TryGetValue("Kiwi", out var kiwi);
By using out int (or out var) we are simultaneously declaring a new variable and assigning a value to it.
What Makes Two Dictionary Keys Equal in C#?
A dictionary decides a key exists by hashing first and comparing second. For a candidate key it computes GetHashCode(), uses that to pick one bucket, and then calls Equals() against each key already in that bucket. Both have to agree before the key counts as present.
For string, int and the other built-in types this is invisible, because they already define equality on their contents.
A class of our own is where it bites. Two instances holding identical data are still two separate objects, and a class that overrides neither method is compared by reference, so the second instance is simply a different key and the lookup misses.
Overriding both fixes it, and they have to move together. Equal objects must return equal hash codes, or the lookup goes to the wrong bucket and never gets as far as calling Equals().
A record gets both for free, which is why records make good dictionary keys and plain classes usually do not.
Both steps have to succeed, and a class that overrides only one of the two methods fails at whichever step it skipped.
Dictionary<string, int> MyDictionary = new Dictionary<string, int>()
{
{ "Apple", 3 },
{ "Banana", -2 },
{ "Orange", 5 },
{ "Pear", 2 }
};
bool DictionaryHasBanana = MyDictionary.ContainsKey("Banana"); // true
The dictionary contains Banana because the string Banana is equal to one key whose value is Banana.
This is not as obvious if our key is a class type:
public class MyClass
{
public int MyNumber { get; set; }
public MyClass(int num)
{
MyNumber = num;
}
}
Dictionary<MyClass, int> MyDictionary = new Dictionary<MyClass, int>();
MyClass One = new MyClass(1);
MyClass AnotherOne = new MyClass(1);
MyDictionary.Add(One, 1);
bool DictionaryHas1 = MyDictionary.ContainsKey(AnotherOne); // false!
MyDictionary does not contain AnotherOne, even though One and AnotherOne appear to be one and the same. That’s because the two variables are references to two different objects in memory. When detecting if a key K exists, for each key X a dictionary checks:
- Is
X.GetHashCode() == K.GetHashCode()?If so, - Is
X.Equals(K) == true?
If both answers are positive for any existing key X, then K is already part of the dictionary.
The default GetHashCode() on a class is based on the object’s identity rather than its contents, so two instances holding the same data get different hash codes, and Equals() compares references. This explains why there’s no key in MyDictionary that satisfies both conditions for AnotherOne.
GetHashCode() and Equals() are two functions that every class defines, so overriding Equals() and GetHashCode() together is how we obtain our desired result:
public class MyClassWithEquality : IEquatable<MyClassWithEquality>
{
public int MyNumber { get; set; }
public MyClassWithEquality(int num)
{
MyNumber = num;
}
public bool Equals(MyClassWithEquality? other) => other is not null && MyNumber == other.MyNumber;
public override bool Equals(object? obj)
{
if (obj is not MyClassWithEquality) return false;
return Equals(obj as MyClassWithEquality);
}
public override int GetHashCode()
{
return MyNumber;
}
}
Implementing IEquatable<T> is what makes the strongly typed Equals() the one the dictionary actually calls: EqualityComparer<T>.Default picks a comparer that goes straight to it, instead of routing every comparison through Equals(object).
Now we can try our example again:
Dictionary<MyClassWithEquality, int> MyDictionary = new Dictionary<MyClassWithEquality, int>(); MyClassWithEquality One = new MyClassWithEquality(1); MyClassWithEquality AnotherOne = new MyClassWithEquality(1); MyDictionary.Add(One, 1); bool DictionaryHas1 = MyDictionary.ContainsKey(AnotherOne); // true!
This time, ContainsKey returns true because there’s at least one key X so that X.GetHashCode() == AnotherOne.GetHashCode() and X.Equals(AnotherOne), and that key is One.
Which Dictionary Check Should We Use?
Four calls cover almost everything, and the right one is decided by what happens next rather than by preference.
If we only need a bool, ContainsKey() is the whole answer and nothing else is simpler.
If we need the value, TryGetValue() is the default. One lookup, a bool to branch on, and the value in an out parameter.
If we need the value but a missing key is ordinary rather than exceptional, GetValueOrDefault() hands back a fallback with no branch at all.
If we are inserting, TryAdd() tests and writes in one call and tells us which of the two happened.
The indexer is the one to be careful with, because it behaves differently on each side of an assignment. Reading dictionary[key] throws when the key is missing; writing dictionary[key] = value never does, and quietly overwrites whatever was there.
ContainsValue() answers a different question entirely, searching values instead of keys, and it scans the whole dictionary to do it.
| We want to know | Call | Hash lookups | Returns |
|---|---|---|---|
| Is the key there? | ContainsKey(key) | 1 | bool |
| Is it there, and what is the value? | TryGetValue(key, out value) | 1 | bool, value through out |
| Add it, but only if it is missing | TryAdd(key, value) | 1 | bool, false if the key was already there |
| Just give me the value | dictionary[key] | 1 | the value, or throws KeyNotFoundException |
| Give me the value, or a fallback | GetValueOrDefault(key) | 1 | the value, or default(TValue) |
| Is the key there, then the value | ContainsKey(key) then dictionary[key] | 2 | what TryGetValue() returns in one |
| Is this value anywhere in it? | ContainsValue(value) | none, it scans | bool, and it is O(n) |
| Is the dictionary empty? | dictionary.Count == 0 | none | bool |
Conclusion
ContainsKey() when a bool is all we need, TryGetValue() when we need the value, and TryAdd() when we are inserting: three calls, one lookup each. Finally, we’ve detailed what it means for a key to exist in order to avoid one of the most common pitfalls when handling dictionary keys.
Tested with .NET 10.

Simply, short and Practical
This is not really good practice to wrap in an “if contains” for retrieval. It will cause the code to traverse two times the dictionary to get a value. Using TryGetValue() don’t have this caveat, is the best practice, and practice the pattern for using dictionary in lamda and LINQ.
Hello Marc Olivier. Thanks for the comment. It is a good point. That’s why we’ve provided the TryGetValue section in the article.
However, complexity of ContainsKey method is equal to O(1) so the use of this approach shouldn’t essentially impair performance. But I totally agree that using TryGetValue() is better and more sophisticated option.
Thanks a lot.
The fun Learning is a very simple is easy to Understand