swift

How to make a wrapper of protocol (not protocol itself) conform to Hashable in Swift


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.


Solution

  • 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:

    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:

    Now usage looks super clean:

    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.