What are NSNotFound and the other sentinel values, and how do they show up in Swift?

4.9.4 · L2 · Interview

Predict first

What does this print?

let ns = "hello world" as NSString
let r = ns.range(of: "xyz")
print(r.location == NSNotFound, NSNotFound == Int.max)
print("hello world".range(of: "xyz") as Any)
print((["a", "b"] as NSArray).index(of: "z") == NSNotFound, ["a", "b"].firstIndex(of: "z") as Any)
  • false false / nil / false nil
  • true true / nil / true nil
  • true true / Optional(0..<0) / true nil

Mental model. NSNotFound is a parking space numbered 9 223 372 036 854 775 807 that is always empty: if your ticket says that, you were not given a space.

Foundation predates optionals, so “not found” was written as a magic number. NSNotFound is NSIntegerMax, which is Int.max on 64-bit. APIs such as NSString.range(of:) return an NSRange whose location is NSNotFound, and NSArray.index(of:) returns it directly. A lookup that is not annotated for Swift hands you the sentinel, and if you use it as an index you read far out of bounds or trap.

Swift-native counterparts return optionals: String.range(of:) gives Range<String.Index>? and Array.firstIndex(of:) gives Int?. Prefer those, and when you must call the Objective-C form compare to NSNotFound before using the value. The same family includes NSNull (a real object standing for “no value” in collections and JSON, not nil), nil/Nil/NULL for object, class and C pointers, and NSOrderedSame-style comparison results. A nil message receiver returns 0 or nil, which is why a missed lookup in Objective-C can silently carry on.

The Foundation call returns the sentinel, and the Swift-native calls return nil. On a 64-bit platform the sentinel equals Int.max.

Common traps

  • Treating the sentinel as an index. array.object(at: idx) with NSNotFound throws an out-of-range exception.
  • Writing if idx > 0 to mean found. The sentinel is huge and positive, so the test passes.

They might follow up with

  • How does an empty NSRange from a failed search print? As {9223372036854775807, 0} on 64-bit.
  • Why does JSON null arrive as NSNull? A collection cannot hold nil, so the object stands in for it.

Related questions

  • 2.9.2 Why does NSNumber confuse Bool, Int and Double, and how do you read untyped values safely?
  • 2.9.1 What is toll-free bridging, and how do you convert between Swift and Foundation types?

← What does atomic actually guarantee for an Objective-C property, and…   How do pointer and out-parameter conventions such as BOOL *stop… →