Checking Hashable conformance

前端 未结 1 1280
梦谈多话
梦谈多话 2021-01-15 15:23

I have a base protocol (Model) that some structs conform to. They also conform to Hashable

protocol Model {}
struct Contact: Model, Hashable {
    var hashVa         


        
1条回答
  •  一整个雨季
    2021-01-15 16:13

    The problem with your code is that you're talking in terms of Model, which promises nothing about Hashable conformance. As you point out, the problem with telling the compiler about this (i.e deriving Model from Hashable) is you then lose the ability to talk in terms of heterogenous types that conform to Model.

    If you don't even care about Model conformance in the first place, you can just use the standard library's AnyHashable type-erased wrapper for completely arbitrary Hashable conforming instances.

    However, assuming you do care about Model conformance, you'll have to build your own type-erased wrapper for instances that conform to both Model and Hashable. In my answer here, I demonstrate how a type eraser can be built for Equatable conforming types. The logic there can be very easily extended for Hashable – we just need to store an extra function to return the hashValue of the instance.

    For example:

    struct AnyHashableModel : Model, Hashable {
    
        static func ==(lhs: AnyHashableModel, rhs: AnyHashableModel) -> Bool {
    
            // forward to both lhs's and rhs's _isEqual in order to determine equality.
            // the reason that both must be called is to preserve symmetry for when a
            // superclass is being compared with a subclass.
            // if you know you're always working with value types, you can omit one of them.
            return lhs._isEqual(rhs) || rhs._isEqual(lhs)
        }
    
        private let base: Model
    
        private let _isEqual: (_ to: AnyHashableModel) -> Bool
        private let _hashValue: () -> Int
    
        init(_ base: T) where T : Hashable {
    
            self.base = base
    
            _isEqual = {
                // attempt to cast the passed instance to the concrete type that
                // AnyHashableModel was initialised with, returning the result of that
                // type's == implementation, or false otherwise.
                if let other = $0.base as? T {
                    return base == other
                } else {
                    return false
                }
            }
    
            // simply assign a closure that captures base and returns its hashValue
            _hashValue = { base.hashValue }
        }
    
        var hashValue: Int { return _hashValue() }
    }
    

    You would then use it like so:

    func complete(with models: [AnyHashableModel]) {
        doSomethingWithHashable(models)
    }
    
    func doSomethingWithHashable(_ objects: [T]) {
        //
    }
    
    let models = [AnyHashableModel(Contact()), AnyHashableModel(Address())]
    complete(with: models)
    

    Here I'm assuming that you'll also want to use it as a wrapper for Model's requirements (assuming there are some). Alternatively, you can expose the base property and remove the Model conformance from AnyHashableModel itself, making callers access the base for the underlying Model conforming instance:

    struct AnyHashableModel : Hashable {
        // ...
        let base: Model
        // ...
    }
    

    You will however note that the above type-erased wrapper is only applicable to types that are both Hashable and a Model. What if we want to talk about some other protocol where the conforming instances are Hashable?

    A more general solution, as I demonstrate in this Q&A, is to instead accept types that are both Hashable and conform to some other protocol – the type of which is expressed by a generic placeholder.

    As there's currently no way in Swift to express a generic placeholder that must conform to a protocol given by another generic placeholder; this relationship must be defined by the caller with a transform closure to perform the necessary upcast. However, thanks to Swift 3.1's acceptance of concrete same-type requirements in extensions, we can define a convenience initialiser to remove this boilerplate for Model (and this can be repeated for other protocol types).

    For example:

    /// Type-erased wrapper for a type that conforms to Hashable,
    /// but inherits from/conforms to a type T that doesn't necessarily require
    /// Hashable conformance. In almost all cases, T should be a protocol type.
    struct AnySpecificHashable : Hashable {
    
        static func ==(lhs: AnySpecificHashable, rhs: AnySpecificHashable) -> Bool {
            return lhs._isEqual(rhs) || rhs._isEqual(lhs)
        }
    
        let base: T
    
        private let _isEqual: (_ to: AnySpecificHashable) -> Bool
        private let _hashValue: () -> Int
    
        init(_ base: U, upcast: (U) -> T) {
    
            self.base = upcast(base)
    
            _isEqual = {
                if let other = $0.base as? U {
                    return base == other
                } else {
                    return false
                }
            }
    
            _hashValue = { base.hashValue }
        }
        var hashValue: Int { return _hashValue() }
    }
    
    // extension for convenience initialiser for when T is Model.
    extension AnySpecificHashable where T == Model {
        init(_ base: U) where U : Hashable {
            self.init(base, upcast: { $0 })
        }
    }
    

    You would now want to wrap your instances in a AnySpecificHashable:

    func complete(with models: [AnySpecificHashable]) {
        doSomethingWithHashable(models)
    }
    
    func doSomethingWithHashable(_ objects: [T]) {
        //
    }
    
    let models: [AnySpecificHashable] = [
        AnySpecificHashable(Contact()),
        AnySpecificHashable(Address())
    ]
    
    complete(with: models)
    

    0 讨论(0)
提交回复
热议问题