I have a protocol that is constrained to class types only:
protocol Plugin: AnyObject {}
Now I want to use the plugin as the key of hash map. I don't want to make the Plugin
protocol extends from Hashable
, because I would have to write any Plugin
all over the places (since it would inherit "Self Requirement" from its parent protocol.
So to work around it, I want to create a generic wrapper. I don't want to use AnyHashable
, because I want a stricter type in case of mistakes.
public struct ObjectHashable<T: AnyObject>: Hashable {
public let object: T
public init(object: T) {
self.object = object
}
public static func ==(lhs: Self, rhs: Self) -> Bool {
return ObjectIdentifier(lhs.object) == ObjectIdentifier(rhs.object)
}
public func hash(into hasher: inout Hasher) {
hasher.combine(ObjectIdentifier(object))
}
}
Now I want to do something like
typealias PluginHashable = ObjectHashable<Plugin>
However, this gives me error:
requirement specified as 'T' : 'AnyObject' [with T = any Plugin]
So I changed it to
typealias PluginHashable = ObjectHashable<any Plugin>
And I got the same error:
requirement specified as 'T' : 'AnyObject' [with T = any Plugin]
My understanding is that while Plugin
protocol is constrained to be a class type, any Plugin
is not. However, I don't know what to do next.
Update:
If I do not use generic for ObjectHashable
, it works:
public struct PluginHashable: Hashable {
public let plugin: Plugin
public init(plugin: Plugin) {
self.plugin = plugin
}
public static func ==(lhs: Self, rhs: Self) -> Bool {
return ObjectIdentifier(lhs.plugin) == ObjectIdentifier(rhs.plugin)
}
public func hash(into hasher: inout Hasher) {
hasher.combine(ObjectIdentifier(plugin))
}
}
However, this solution is only usable for Plugin
protocol, so not ideal. I prefer to have a solution that works for all similar cases.
I completely see what you’re running into.
Even though Plugin: AnyObject
, when you refer to any Plugin
, it's treated as an existential — and existentials in Swift are not class types, even if the protocol they come from is AnyObject
.
That's why ObjectHashable<T: AnyObject>
refuses to accept any Plugin
as T
— because any Plugin
isn't itself a class, even if implementations of Plugin
must be.
Why any Plugin is a Problem
any Plugin is a value type representing "any instance conforming to Plugin."
It doesn't guarantee that it's a class instance at the type level in a way generic constraints can check.
Swift treats any Plugin differently than concrete types that conform to Plugin.
So... how to solve your situation?
Here's the trick: Instead of writing ObjectHashable<any Plugin>
,
you need ObjectHashable<some Plugin>
at usage sites,
or redesign ObjectHashable
slightly to accept existential values.
A Clean Solution for Your Case
Change ObjectHashable
to accept any AnyObject
(even any Plugin
existentials).
Here's a slightly updated version of ObjectHashable
:
public struct ObjectHashable: Hashable {
public let object: AnyObject
public init(_ object: AnyObject) {
self.object = object
}
public static func ==(lhs: Self, rhs: Self) -> Bool {
return ObjectIdentifier(lhs.object) == ObjectIdentifier(rhs.object)
}
public func hash(into hasher: inout Hasher) {
hasher.combine(ObjectIdentifier(object))
}
}
Now you don't need to worry about generics.
You can store any class object — including any Plugin
— wrapped properly.
Usage:
var dict: [ObjectHashable: String] = [:]
let pluginInstance: any Plugin = SomePlugin()
dict[ObjectHashable(pluginInstance)] = "some value"
But you asked for strictness (not too open like AnyObject
)...
If you still want it to be restricted only to Plugin (not any class), here's how you can do it more strictly:
public struct PluginHashable: Hashable {
public let plugin: any Plugin
public init(_ plugin: any Plugin) {
self.plugin = plugin
}
public static func ==(lhs: Self, rhs: Self) -> Bool {
return ObjectIdentifier(lhs.plugin as AnyObject) == ObjectIdentifier(rhs.plugin as AnyObject)
}
public func hash(into hasher: inout Hasher) {
hasher.combine(ObjectIdentifier(plugin as AnyObject))
}
}
- This ensures you can only wrap any Plugin
, not any AnyObject
.
- And you still hash based on the object identity.
Usage:
var pluginDict: [PluginHashable: String] = [:]
let p1: any Plugin = MyPlugin()
let p2: any Plugin = MyOtherPlugin()
pluginDict[PluginHashable(p1)] = "First Plugin"
pluginDict[PluginHashable(p2)] = "Second Plugin"
Why can't you use ObjectHashable<Plugin>
?
Because ObjectHashable<T: AnyObject>
expects a class type T
,
but any Plugin
is not a class type — it’s an existential value.
You have to either:
make ObjectHashable
non-generic, store AnyObject
, OR
specialize it for your protocol (like PluginHashable
above).
I hope this helps. Let me know if your issue is solved.
EDIT:
Since you mentioned you this previous solution does not work for you, I will go with my suggestion in the comment: Specialize ObjectHashable for existentials that are class-only.
Here is how you could do it:
public struct ObjectHashable<T>: Hashable {
public let object: T
public init(_ object: T) {
self.object = object
}
public static func ==(lhs: ObjectHashable<T>, rhs: ObjectHashable<T>) -> Bool {
ObjectIdentifier(lhs.object as AnyObject) == ObjectIdentifier(rhs.object as AnyObject)
}
public func hash(into hasher: inout Hasher) {
hasher.combine(ObjectIdentifier(object as AnyObject))
}
}
Notice carefully:
T
is generic.
At runtime, we cast to AnyObject
manually inside ==
and hash(into:)
.
Compile-time, you can still type-check strictly for ObjectHashable<Plugin>
etc.
typealias PluginHashable = ObjectHashable<any Plugin>
var pluginDict: [PluginHashable: String] = [:]
let plugin1: any Plugin = MyPlugin()
let plugin2: any Plugin = AnotherPlugin()
pluginDict[PluginHashable(plugin1)] = "Plugin 1"
pluginDict[PluginHashable(plugin2)] = "Plugin 2"
With this, you get:
- Strict type enforcement at compile-time.
- Reusable for other class-only protocols too without needing a custom wrapper every time.
In case you want even stricter compile-time guard which is optional, you could add runtime assertion, this would forbid misuse (ex: someone trying to pass a struct), you would have a code like this:
public init(_ object: T) {
precondition(Mirror(reflecting: object).subjectType is AnyObject.Type, "T must be a class type")
self.object = object
}
But honestly, since you're manually choosing any Plugin
in the PluginHashable
alias, you're already fine. No need to runtime-check unless you're super paranoid loll.