Key Difference Between Dictionary and NSDictionary
The recent article Strings in Swift 4 by Ole Begemann talked about how Swift
String
equality is implemented as Unicode canonical equivalence. As a result, two String instances can be equal even if they contain different Unicode code units. […] TwoNSString
instances are equal if they have the same sequence of UTF-16 code units.[…]
This difference in behavior between Swift and Objective-C is troubling. Suppose that you had some old Objective-C code that saved user defaults in dictionary format with string keys, and then you wrote new Swift code to access the user defaults, naively using
dictionary(forKey:)
, because of course that’s what you’d think to use.
What happens is that you get a weird bridged Swift Dictionary
. This is supposed to be an O(1) wrapper around the NSDictionary
. Indeed, its count
is the same. But when you look up the keys it uses the Swift rules for string quality. So a lookup that would find no matches in Objective-C may find one with Swift. Removing an entry removes multiple entries with equivalent keys. When you get the description
it coalesces the entries with equivalent string keys, so that the description
doesn’t match the count
. And if you try to cast it as NSDictionary
, which should always succeed and just give you the underlying wrapped dictionary, it crashes.
Update (2017-12-11): Joe Groff:
It’s a known problem. Eventually we want to do away with the “lazy bridging”, which would allow the crash to at least happen deterministically on bridging and avoid the inconsistent view of the Dictionary.
It’s supposed to crash if it detects a key collision introduced by the difference in equality model (theory being that keys differing only by normalization form are almost always by mistake).
Another possibility is to opt out APIs from bridging when they deal with NSStrings that may significantly differ in normalization.
We already know we need an opt-out mechanism for things like Core Data NSArray/NSDictionary objects where their class-iness is part of the magic.
Bit of clarification: the crash is definitely a bug. The code to bridge into Swift from ObjC coalesces keys and shouldn’t trap, but it assumes the count from the NSDictionary, an invalid optimization.
O(1) bridging from Swift to ObjC only happens when the types in the ObjC container are verbatim-bridged. Which String/NSString aren’t.