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.