Last active
June 9, 2024 14:43
-
-
Save xtravar/7ca65b6d6cdd7ced4858074c065ac6a1 to your computer and use it in GitHub Desktop.
Use classes and objects as keys to dictionaries and sets.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
Swift AnyObject and AnyClass are not Hashable, so they cannot be used in Dictionaries as keys or Sets as members. | |
This code concisely shows how to make wrapper structs for these types and then convenience subscripts/methods to wrap/unwrap. | |
This is for the case where you wish to associate a reference, not object content, with a value or used in a set. | |
Example use case: a dependency injector that returns an object for a protocol. | |
*/ | |
// protocol for class and object struct wrappers | |
public protocol HashableObjectWrapper : Hashable { | |
associatedtype ObjectType | |
var value: ObjectType { get } | |
init(_ value: ObjectType) | |
static func identityEqual(_ lhs: ObjectType, _ rhs: ObjectType) -> Bool | |
static func createIdentifier(_ value: ObjectType) -> ObjectIdentifier | |
} | |
// default impl - not necessarily efficient unless the compiler is super smart | |
extension HashableObjectWrapper { | |
@_transparent | |
public static func identityEqual(_ lhs: ObjectType, _ rhs: ObjectType) -> Bool { | |
let id1 = self.createIdentifier(lhs) | |
let id2 = self.createIdentifier(rhs) | |
return id1 == id2 | |
} | |
} | |
// implement Equatable and Hashable | |
extension HashableObjectWrapper { | |
@_transparent | |
public static func == (lhs: Self, rhs: Self) -> Bool { | |
return Self.identityEqual(lhs.value, rhs.value) | |
} | |
@_transparent | |
public func hash(into hasher: inout Hasher) { | |
Self.createIdentifier(self.value).hash(into: &hasher) | |
} | |
} | |
// wraps a class so it can be a key to a dictionary or set | |
// note: we can't genericize this because Swift doesn't like that (yet?) | |
public struct ClassWrapper : HashableObjectWrapper { | |
@_transparent | |
public static func createIdentifier(_ value: AnyClass) -> ObjectIdentifier { | |
return ObjectIdentifier(value) | |
} | |
public let value: AnyClass | |
public init(_ value: AnyClass) { | |
self.value = value | |
} | |
} | |
// wraps an object so it can be a key to a dictionary or set | |
public struct ObjectWrapper<T: AnyObject> : HashableObjectWrapper { | |
@_transparent | |
public static func createIdentifier(_ value: T) -> ObjectIdentifier { | |
return ObjectIdentifier(value) | |
} | |
public let value: T | |
public init(_ value: T) { | |
self.value = value | |
} | |
} | |
// allows using objects/classes directly as arguments to Set | |
extension Set where Element: HashableObjectWrapper { | |
@discardableResult | |
public mutating func insert<T>(_ newMember: T) -> (inserted: Bool, memberAfterInsert: T) where T == Element.ObjectType { | |
let result = self.insert(Element(newMember)) | |
return (inserted: result.inserted, memberAfterInsert: result.memberAfterInsert.value) | |
} | |
@discardableResult | |
public mutating func remove<T>(_ member: T) -> T? where T == Element.ObjectType { | |
let removed = self.remove(Element(member)) | |
return removed?.value | |
} | |
} | |
// allows using objects directly as keys into a dictionary | |
extension Dictionary where Key: HashableObjectWrapper { | |
public subscript<K: AnyObject>(key: K) -> Value? where K == Key.ObjectType { | |
get { | |
return self[Key(key)] | |
} | |
set { | |
self[Key(key)] = newValue | |
} | |
} | |
public subscript<K: AnyObject>(key: K, default defaultValue: @autoclosure () -> Value) -> Value where K == Key.ObjectType { | |
get { | |
return self[Key(key), default: defaultValue] | |
} | |
set { | |
self[Key(key), default: defaultValue] = newValue | |
} | |
} | |
} | |
// allows using classes directly as a key into a dictionary | |
extension Dictionary where Key == ClassWrapper { | |
public subscript<K: AnyObject>(key: K.Type) -> Value? { | |
get { | |
return self[Key(key)] | |
} | |
set { | |
self[Key(key)] = newValue | |
} | |
} | |
public subscript<K: AnyObject>(key: K.Type, default defaultValue: @autoclosure () -> Value) -> Value { | |
get { | |
return self[Key(key), default: defaultValue] | |
} | |
set { | |
self[Key(key), default: defaultValue] = newValue | |
} | |
} | |
} | |
// turn an array of objects into wrapped objects | |
extension Array where Element: HashableObjectWrapper { | |
public init<O: Sequence>(_ unwrappedSequence: O) where O.Element == Element.ObjectType { | |
self = unwrappedSequence.map { Element($0) } | |
} | |
} | |
// turn an array of wrapped objects into objects | |
extension Array { | |
public init<O: Sequence>(_ wrappedSequence: O) where O.Element: HashableObjectWrapper, O.Element.ObjectType == Element { | |
self = wrappedSequence.map { $0.value } | |
} | |
} | |
// convenience typealiases | |
public typealias ObjectSet<T: AnyObject> = Set<ObjectWrapper<T>> | |
public typealias ObjectKeyedDictionary<K: AnyObject, V> = Dictionary<ObjectWrapper<K>, V> | |
public typealias ClassSet = Set<ClassWrapper> | |
public typealias ClassKeyedDictionary<V> = Dictionary<ClassWrapper, V> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment