NSDictionary: Key-Value Storage, Lookup, and Generics
Understand how NSDictionary stores and retrieves values by key, why keys must conform to NSCopying, how lookup and enumeration work, and how lightweight generics document a dictionary's key and value types.
Arrays store values in a fixed order and let you retrieve them by position. Dictionaries solve a different problem: retrieving a value by a meaningful name rather than a numeric position. NSDictionary is Foundation's immutable key-value collection — an unordered association of unique keys to values, backed internally by a hash table, that gives you near-constant-time lookup by key regardless of how many entries it holds. Configuration data, JSON-decoded objects, and anywhere you would reach for a 'map', 'hash', or 'associative array' in another language are the natural home for NSDictionary in Objective-C.
Creating a Dictionary
The modern, idiomatic way to build an NSDictionary is the literal syntax: NSDictionary *scores = @{@"Amir": @92, @"Mina": @88, @"Zara": @95};. Each key: value pair is separated by a colon, pairs are separated by commas, and the whole literal is wrapped in @{ }. This is by far the most common way dictionaries appear in real Objective-C code, because it reads almost exactly like the resulting structure and needs no explicit class name.
Before literals existed (and still occasionally seen in older code, or when keys and values need to be built programmatically from variables rather than written as literals in the source), dictionaries were constructed with dictionaryWithObjectsAndKeys:, which takes an alternating sequence of value, key, value, key pairs terminated by nil: [NSDictionary dictionaryWithObjectsAndKeys:@92, @"Amir", @88, @"Mina", nil]. Note the order is value first, then key — the reverse of how the literal syntax reads — which is a common source of confusion when reading old code; the nil terminator is also easy to forget and, if omitted, produces undefined behavior since the method has no other way to know where the argument list ends.
Keys Must Conform to NSCopying
Every key used in an NSDictionary must conform to the NSCopying protocol. NSString, NSNumber, and NSValue — the types you will use as keys nearly all the time — all conform to NSCopying already, which is why @"Amir" and @92 work as keys with no extra ceremony. If you try to use an object whose class does not implement copyWithZone: as a dictionary key, you get a runtime exception the moment you attempt the insertion.
The reason for this requirement is subtle but important: when you insert a key-value pair, the dictionary does not simply retain the key object you handed it — it calls copy on the key and stores that copy as the actual hash-table key. This matters because a dictionary's internal hash table depends on each key's hash value staying fixed for as long as that key is stored; if the dictionary instead kept a live reference to a *mutable* key object, and the caller later mutated that object (say, appending characters to an NSMutableString used as a key), the key's hash value would silently change out from under the hash table, corrupting the dictionary's internal bucket structure and making that entry unfindable by any subsequent lookup. Copying the key at insertion time neutralizes this risk entirely: the dictionary always owns an independent, frozen snapshot of whatever the key's value was at the moment of insertion, immune to any later mutation of the original object the caller still holds.
In practice this means: pass an NSMutableString as a dictionary key and it works fine, because what actually gets stored as the key is an immutable NSString copy of it, not the mutable object itself. This is exactly why NSCopying, not merely 'being an object', is the requirement — the dictionary needs the ability to make that frozen snapshot, not just a reference.
Values, Lookup, and Subscripting
Unlike keys, values stored in an NSDictionary carry no such NSCopying requirement — a value can be any Objective-C object, mutable or immutable, and the dictionary simply retains it (strongly holds a reference to it) rather than copying it. This asymmetry is intentional: values are read back out and used as-is, so there is no hash-table integrity concern the way there is for keys.
The traditional way to look up a value is objectForKey:: NSNumber *amirScore = scores[@"Amir"]; is actually sugar for [scores objectForKeyedSubscript:@"Amir"], which both this course and most modern Objective-C code use interchangeably with the explicit method call — [scores objectForKey:@"Amir"] and scores[@"Amir"] return the identical result. Both forms return nil if the key is not present in the dictionary — this is not an exception and not a crash, just a normal nil return, which is a meaningful contrast with languages like Python or Java where an unrecognized-key lookup on a dictionary/map throws by default. That said, nil from a missing key looks identical to nil stored deliberately as a value would look — except NSDictionary, unlike NSMutableDictionary, cannot actually store nil as a value at all (attempting @{@"x": nil} is a compile error, and dictionaryWithObjectsAndKeys: silently truncates its argument list at the first nil it sees), so in practice a nil result from lookup on an NSDictionary always and unambiguously means 'key not found'.
Enumeration
The simplest way to walk every entry is fast enumeration over the dictionary itself, which iterates its keys: for (id key in scores) { NSLog(@"%@: %@", key, scores[key]); }. Each loop iteration hands you one key; you look the corresponding value up yourself inside the loop body if you need it.
When you need both the key and the value together and prefer not to write an explicit lookup each iteration, enumerateKeysAndObjectsUsingBlock: hands you both directly: [scores enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) { NSLog(@"%@: %@", key, obj); }]. The block also receives a BOOL *stop out-parameter — setting *stop = YES inside the block ends enumeration early, which is useful when you are searching for one matching entry and want to bail out the moment you find it rather than pointlessly visiting the rest.
It is worth being explicit that NSDictionary makes no promise about enumeration order. Two dictionaries built from the identical literal, or even the same dictionary enumerated twice in a row, are not guaranteed to yield keys in the same order (in practice the order is deterministic for a given hash table state, but that state depends on internal details you should not rely on). If a stable, meaningful order matters for output — say, an alphabetically sorted report — sort the keys array yourself before iterating rather than depending on dictionary enumeration order.
Lightweight Generics
Since generics were added to Objective-C, you can annotate a dictionary's declared type with the types of its keys and values: NSDictionary<NSString *, NSNumber *> *scores = @{@"Amir": @92};. This tells the compiler (and, just as importantly, anyone reading the code, and Xcode's autocomplete) that scores maps NSString keys to NSNumber values, and the compiler will flag a call site that tries to insert a mismatched type or that treats a looked-up value as the wrong class without a cast.
These generics are 'lightweight' — a term Apple uses deliberately — because they exist purely for compile-time checking and documentation; there is exactly one NSDictionary class at runtime regardless of what type parameters you write, and the parameters are erased before your code runs (much like Java generics, and unlike, say, C++ templates, which really do generate distinct code per instantiation). This means lightweight generics catch real bugs at compile time for free, with zero runtime cost, but they are not a runtime type check — a dictionary can still, in principle, end up holding a value of the wrong type if it arrives through untyped code (bridged from JSON, for instance), and generics will not catch that at runtime.
• Literal syntax: @{@"key": value, ...} — the standard, idiomatic way to build a dictionary. • Keys must conform to NSCopying; the dictionary copies each key at insertion time. • objectForKey: / subscripting returns nil for a missing key — never an exception. • enumerateKeysAndObjectsUsingBlock: gives you key and value together, with an early-stop option. • NSDictionary<KeyType, ObjectType> generics are compile-time only, erased at runtime, zero cost.
NSDictionary Creation and Lookup
@{ } literal syntax is key: value, comma-separated — the standard modern form. Missing-key lookups return nil; they never raise an exception. for-in over a dictionary iterates its keys, not its values or pairs.
NSDictionary<NSString *, NSNumber *> *scores = @{
@"Amir": @92,
@"Mina": @88,
@"Zara": @95
};
NSNumber *amirScore = scores[@"Amir"]; // subscripting
NSNumber *same = [scores objectForKey:@"Amir"]; // explicit method, identical result
NSNumber *missing = scores[@"Noah"]; // nil, no exception
for (NSString *key in scores) {
NSLog(@"%@: %@", key, scores[key]);
}
[scores enumerateKeysAndObjectsUsingBlock:^(NSString *key, NSNumber *obj, BOOL *stop) {
NSLog(@"%@ scored %@", key, obj);
}];Code Examples
The literal @{ } syntax builds an immutable NSDictionary in one expression. Subscripting with capitals[@"Japan"] returns the associated value directly; looking up a key that was never inserted (@"Atlantis") returns nil rather than crashing, which the code checks for explicitly with a plain == nil comparison.
NSString *unknown = capitals[@"Atlantis"];
enumerateKeysAndObjectsUsingBlock: hands both the key and the value to the block on each iteration, avoiding a separate lookup. The __block variable captures the result across iterations, and setting *stop = YES the moment a zero-count item is found ends enumeration early instead of needlessly visiting the remaining entries.
Because NSString conforms to NSCopying, the dictionary copies mutableKey's value at the moment of insertion into a frozen, independent NSString. Mutating the original mutableKey object afterward has no effect on what is actually stored as the dictionary's key, which is why lookup under the original string "session" still succeeds and lookup under the mutated string fails — the hash table's integrity was never at risk. This is precisely why NSCopying is required for keys in the first place: without the copy-at-insertion guarantee, this same code would silently corrupt the dictionary's hash table the moment appendString: ran.
NSDictionary<NSString *, NSString *> *capitals = @{
@"France": @"Paris",
@"Japan": @"Tokyo",
@"Kenya": @"Nairobi"
};
NSString *capital = capitals[@"Japan"];
NSLog(@"%@", capital); // Tokyo
NSString *unknown = capitals[@"Atlantis"];
if (unknown == nil) {
NSLog(@"No entry for that key.");
}NSDictionary<NSString *, NSNumber *> *inventory = @{
@"widgets": @120,
@"gadgets": @0,
@"gizmos": @45
};
__block NSString *firstOutOfStock = nil;
[inventory enumerateKeysAndObjectsUsingBlock:^(NSString *key, NSNumber *count, BOOL *stop) {
if (count.integerValue == 0) {
firstOutOfStock = key;
*stop = YES;
}
}];
NSLog(@"%@", firstOutOfStock); // "gadgets"NSMutableString *mutableKey = [NSMutableString stringWithString:@"session"];
NSDictionary *cache = @{mutableKey: @"active"};
[mutableKey appendString:@"-token"]; // mutate the original object AFTER insertion
NSLog(@"%@", cache[@"session"]); // still finds it: "active"
NSLog(@"%@", cache[@"session-token"]); // nil — the stored key never changedKey Lookup Never Throws
Contrast this with languages whose default map/dictionary access throws on an unrecognized key — Objective-C's nil-on-miss is a deliberate, consistent design choice across Foundation's collections.
scores[@"Amir"] ----> hash("Amir") ----> bucket found ----> @92
scores[@"Noah"] ----> hash("Noah") ----> bucket empty ----> nil
(no exception either way — a missing key is just an ordinary nil)Important Rules
Use the @{ } literal syntax for dictionaries built from known keys and values; reach for dictionaryWithObjectsAndKeys: only when reading or maintaining older code.
Any key type must conform to NSCopying — NSString, NSNumber, and NSValue all qualify; the dictionary copies the key at insertion time.
objectForKey: and subscripting both return nil, never an exception, for a key that is not present.
for-in over a dictionary iterates its keys; use enumerateKeysAndObjectsUsingBlock: when you need both key and value without a manual lookup.
NSDictionary<KeyType, ObjectType> generics are compile-time documentation and checking only — they are erased at runtime and carry zero performance cost.
Decoding JSON Responses
Almost every network API response an app parses with NSJSONSerialization comes back as a tree of NSDictionary and NSArray objects, with NSString keys throughout — exactly the shape this lesson covers. Reading a field means a chain of subscript lookups (response[@"user"][@"profile"][@"email"]), each of which can legitimately return nil if a field is absent from a particular response, which is why production parsing code checks for nil at each step rather than assuming every expected key is always present.
id value = [dict objectForKey:@"missing"]; NSLog(@"%@", [value description]); // NPE-style thinking id value = dict[@"missing"]; if (value) { /* use it */ } else { /* handle absence */ }
A missing key returns nil, not an exception, so code should check for nil rather than assuming the returned object is always valid — sending most messages to nil is harmless in Objective-C (it returns nil/0/NO), but relying on that silently rather than explicitly checking for absence tends to produce confusing downstream bugs instead of a clear failure at the lookup site.
NSDictionary *d = @{someCustomObjectThatDoesNotConformToNSCopying: @"value"};
// Use NSString, NSNumber, NSValue, or a custom class that implements copyWithZone: as keys
A dictionary key must conform to NSCopying so the dictionary can store a frozen copy of it at insertion time. Attempting to use an object whose class does not implement copyWithZone: as a key raises a runtime exception immediately.
for (id key in dict) { id value = key; /* treats key as if it were the value */ } for (id key in dict) { id value = dict[key]; /* look the value up explicitly */ }
Fast enumeration over an NSDictionary yields its keys, not key-value pairs and not values. Forgetting this and treating the loop variable as the value is a common source of confusion for developers coming from languages whose default dictionary iteration yields entries or values.
What must every key used in an NSDictionary conform to? A. NSCoding B. NSCopying C. NSFastEnumeration D. NSComparisonMethods
B. NSCopying — Dictionary keys must conform to NSCopying because the dictionary copies each key at insertion time, storing an independent frozen snapshot rather than a live reference to the caller's object.
Why does NSDictionary copy a key at insertion time instead of just retaining it? A. Copying is required by ARC for all object types B. To save memory by discarding the original object C. So that a later mutation of the original key object cannot change its hash value out from under the hash table and corrupt lookups D. Because NSDictionary cannot retain objects, only copy them
C. So that a later mutation of the original key object cannot change its hash value out from under the hash table and corrupt lookups — If the dictionary kept a live reference to a mutable key and the caller mutated it afterward, the key's hash value could change, corrupting the hash table's internal structure. Copying at insertion freezes the key's value, immune to later mutation of the original object.
What does scores[@"missingKey"] return when scores is an NSDictionary and "missingKey" was never inserted? A. Throws an NSRangeException B. Throws a generic NSException C. Returns nil D. Returns an empty string
C. Returns nil — Both objectForKey: and subscript lookup return nil for a key that is not present in the dictionary — this is ordinary, expected behavior, not an error condition, unlike default dictionary/map access in some other languages.
What does a for-in loop over an NSDictionary iterate? A. Its values B. Its keys C. Key-value pairs as two-element arrays D. Its keys sorted alphabetically
B. Its keys — Fast enumeration over a dictionary yields its keys, one per iteration; you look up the corresponding value yourself inside the loop body, or use enumerateKeysAndObjectsUsingBlock: to get both directly.
What is true of NSDictionary's lightweight generics, such as NSDictionary<NSString *, NSNumber *>? A. They enforce type checks at runtime and will throw if a mismatched type is inserted B. They are compile-time-only annotations, erased at runtime, that help the compiler and readers without any runtime cost C. They change which class is instantiated at runtime D. They are required by the compiler for every NSDictionary declaration
B. They are compile-time-only annotations, erased at runtime, that help the compiler and readers without any runtime cost — Lightweight generics exist purely for compile-time checking and documentation. There is exactly one NSDictionary class at runtime regardless of the declared type parameters, which are erased before the code executes, so they carry zero runtime cost but also provide no runtime type enforcement.
Given NSDictionary<NSString *, NSNumber *> *inventory, write a method that returns an NSArray of the keys (item names) whose value is exactly zero, using enumerateKeysAndObjectsUsingBlock:.
• (NSArray<NSString *> *)outOfStockItems:(NSDictionary<NSString *, NSNumber *> *)inventory { NSMutableArray<NSString *> *result = [NSMutableArray array]; [inventory enumerateKeysAndObjectsUsingBlock:^(NSString *key, NSNumber *count, BOOL *stop) { if (count.integerValue == 0) { [result addObject:key]; } }]; return [result copy]; }
Build a mutable array before enumeration starts, append matching keys inside the block, and return an immutable copy (or the mutable array itself, depending on the method's declared return type) once enumeration finishes.