Understanding Swift's ObjectIdentifier

Understanding Swift's ObjectIdentifier

ObjectIdentifier is one of these obscure Swift types that you only see when looking at the answers for very specific problems in StackOverflow, and chances are that you have never really needed to use it. However, ObjectIdentifier plays an important part in Swift and is actually pretty useful as a solution to problems that involve metatypes. Let's take a look at why this type exists and what you can use it for.

Where you might have seen it

If you're like me, your first contact with ObjectIdentifier might have involved trying to use metatypes as the key for a dictionary:

let routes = [Route.Type: String]
// Error: Route.Type does not conform to Hashable

Metatypes cannot be used as the key to dictionaries because metatypes do not conform Hashable, but you can't make them conform because you also can't extend metatypes! If you search for problems like this on StackOverflow, ObjectIdentifier will be revealed as one of the solutions.

What's ObjectIdentifier?

ObjectIdentifier is a class defined in the Swift Standard Library that is able to provide an unique identifier to reference types and metatypes. This distinction is important -- value types like structs and enums cannot have unique identifiers because they do no have the concept of "identity" inside the language as we'll understand better shortly.

Its usage is very simple: By initializing ObjectIdentifier with an AnyObject instance (any class instance) or a metatype, you'll have access to an object that represents an unique identifier to that reference.

class Foo: Equatable {
    let val: Int
    static func == (lhs: Foo, rhs: Foo) -> Bool { lhs.val == rhs.val }
    init(_ val: Int) { self.val = val }
}

let fooA = Foo(1)

let identifierA = ObjectIdentifier(fooA)

ObjectIdentifier has no public properties or methods, but it conforms to both Equatable and Hashable, which allows you to pinpoint and separate different instances of the same object. Here's an example: Can you guess the results of the following equality operations?

let fooA = Foo(1)
let fooB = Foo(1)

fooA == fooB // ?
ObjectIdentifier(fooA) == ObjectIdentifier(fooB) // ?

If you guessed true and false, you are correct. fooA is equal to fooB because they have the same value, but their ObjectIdentifier counterparts are not equal because they are not the same instance -- they are different representations of Foo. What about this one?

let fooC = fooA
ObjectIdentifier(fooA) == ObjectIdentifier(fooC) // ?

On the other hand, the previous comparison is true because fooA and fooC are representations of the same instance. This is why ObjectIdentifier only works for reference types and metatypes -- although it is possible to create references of value types if you try hard enough (for example, with inout), they do not have this specific assignment behavior.

You might have noticed that the behavior of ObjectIdentifier is similar to the pointer equality operator === -- you might be interested to know that === is just a wrapper for ObjectIdentifier equality:

public func === (lhs: AnyObject?, rhs: AnyObject?) -> Bool {
  switch (lhs, rhs) {
  case let (l?, r?):
    return ObjectIdentifier(l) == ObjectIdentifier(r)
  case (nil, nil):
    return true
  default:
    return false
  }
}

As mentioned before but not shown, you can also use ObjectIdentifier with metatypes to give them Hashable and Equatable capabilities. This is because metatypes are treated as single global instances of an object's type, and thus hold that concept of "identity".

let dict = [ObjectIdentifier: String]()
let metaObject = ObjectIdentifier(MyViewController.self)
dict[metaObject] = "SwiftRocks!"

How ObjectIdentifier works internally

Internally, ObjectIdentifier is just a wrapper for an object's memory address.

public struct ObjectIdentifier {

  internal let _value: Builtin.RawPointer

  public init(_ x: AnyObject) {
    self._value = Builtin.bridgeToRawPointer(x)
  }

  public init(_ x: Any.Type) {
    self._value = unsafeBitCast(x, to: Builtin.RawPointer.self)
  }
}

Because the memory address is in its core a mere number, we can use it to give ObjectIdentifier Equatable and Hashable capabilities by simply comparing this number. Clever, isn't it?

extension ObjectIdentifier: Equatable {
  public static func == (x: ObjectIdentifier, y: ObjectIdentifier) -> Bool {
    return Bool(Builtin.cmp_eq_RawPointer(x._value, y._value))
  }
}

extension ObjectIdentifier: Hashable {
  public func hash(into hasher: inout Hasher) {
    hasher.combine(Int(Builtin.ptrtoint_Word(_value)))
  }
}

ObjectIdentifier is used internally in the compiler whenever identifying specific instances is needed, but one interesting use is that ObjectIdentifier is the default "identification method" for reference types in the Identifiable protocol:

public protocol Identifiable {
  associatedtype ID: Hashable
  var id: ID { get }
}

extension Identifiable where Self: AnyObject {
  public var id: ObjectIdentifier {
    return ObjectIdentifier(self)
  }
}

Example: Using ObjectIdentifier to increase performance

ObjectIdentifier can be used a way to bypass the limitation of metatypes in order to give them Hashable capabilities. If you're working on something that behaves like a plugin architecture, you'll find that being able to create Sets and Dictionaries based on metatypes is useful.

Here's an example from one of my first apps. In Rapiddo, features were completely modularized. They were developed as mini programs that were registered into the main app and exposed some capabilities. One of these capabilities was called Widget, which represented views that could be displayed in the app's home's feed.

final class MainMiniProgram: MiniProgram {

    override class var supportedWidgets: [WidgetView.Type] {
        return [
            CircleCarrouselWidgetView.self,
            NewsFeedWidgetView.self,
            OngoingOrdersWidgetView.self,
            SimpleMessageWidgetView.self
        ]
    }

    ...

Sometimes the main app wanted to know if a mini program supported a specific type of widget. Because supportedWidgets is an array, the solution was to iterate all of the widgets:

func miniProgram<T: Widget>(framework: RapiddoCore.MiniProgram.Type, supportsWidget widgetMetaType: T.Type) -> Bool {
    return framework.supportedWidgets.first { $0 == widgetMetaType }
}

Unfortunately, since it was possible for some mini programs to contain hundreds of widgets, this method was a big performance problem. The best solution is for supportedWidgets to be a Set to allow us to answer this question in constant time, but we can't have a Set of metatypes because they are not Hashable.

The solution? We can extend MiniProgram to pre-process our widget array into Set<ObjectIdentifier> and search from there.

extension MiniProgram {
    private static let _widgetSet: Set<ObjectIdentifier> = Set(supportedWidgets.map(ObjectIdentifier.init))

    static func supports
   
    (widgetType: T.Type) -> Bool {
   
        return _widgetSet.contains(ObjectIdentifier(widgetType))
    }
}

With this code, the array of widgets will only be iterated the first time that _widgetSet is accessed. On subsequent calls, the set will be provided instantly due to the lazy behavior or static properties.

References and Good Reads

ObjectIdentifier.swift