1

Swift 3 introduced AnyHashable, which is a type-erased box around a Hashable. It solves the problem of how to store objects of different types that conform to the same protocol when the protocol requires an associated type.

When creating an AnyHashable box, you have to provide the thing you want to box in the initializer:

let someHashable: AnyHashable = AnyHashable(1)

However, you can also use AnyHashable as if you were using a protocol without actually creating anything. It's as if Apple overloaded the assignment operator to do automatic boxing:

let someHashable: AnyHashable = 1 

My question is, how is this possible? I have followed several of the guides online about how to create my own type-erased boxes. https://www.bignerdranch.com/blog/breaking-down-type-erasures-in-swift/ However it seems that there is something else going on with the way Apple is doing it.

If it was simply a type-erased box, you would still need to provide the type somewhere, like so:

class MyClass<T> where T: Hashable {
    let someHashable: AnyHashable<T>
}

Otherwise the compiler would complain: "requires arguments in <...>". Yet I can simply create a variable:

let someHashable: AnyHashable

I ask because I am implementing a delegate that uses an associated type. I created a type-erased box for it, however I'd like to hide the fact that there is type-erasure going on underneath (similar to the way AnyHashable appears to work).

Here is my implementation (ignoring the requirements for weak properties for now).

// The delegate protocol

public protocol SomeDelegate {
    associatedtype T
    func doSomething(_ t: T)
}

// Type-erasure boilerplate

fileprivate class _AnySomeDelegateBase<T>: SomeDelegate {

    init() {
        guard type(of: self) != _AnySomeDelegateBase.self else {
            fatalError("Must subclass")
        }
    }

    func doSomething(_ t: T) {
        fatalError("Must override")
    }

}

fileprivate final class _AnySomeDelegateBox<SD: SomeDelegate> : _AnySomeDelegateBase<SD.T> {

    private var concrete: SD

    init(_ concrete: SD) {
        self.concrete = concrete
    }

    override func doSomething(_ t: T) {
        self.concrete.doSomething(t)
    }

}

// Type-erasure wrapper

public final class AnySomeDelegate<T>: SomeDelegate {

    private let box: _AnySomeDelegateBase<T>

    public init<SD: SomeDelegate>(_ base: SD) where SD.T == T {
        self.box = _AnySomeDelegateBox(base)
    }

    public func doSomething(_ t: T) {
        self.box.doSomething(t)
    }

}

And here is how I'd like to use it:

class MyViewController: UIViewController {

    var delegate: AnySomeDelegate?

}

But this won't work: AnySomeDelegate requires arguments in <...>. Which means I'd also have to parameterize the UIViewController around T.

class MyViewController<T>: UIViewController {

    var delegate: AnySomeDelegate<T>?

}
MH175
  • 2,234
  • 1
  • 19
  • 35
  • 1
    The `AnyHashable` auto-conversion is special-cased by the compiler, compare https://stackoverflow.com/q/42021207/2976878 – Hamish Nov 13 '17 at 17:22
  • 1
    I'm not sure it's quite a duplicate – it doesn't really answer the second part of your question. Really the auto-conversion is orthogonal to whether a generic placeholder is required or not. In your case, you kind of really the generic placeholder `T` on your type-erasing wrapper. Why? Because what would you type erase it to? `Any`? Well then you'd have `doSomething(_: Any)` that would have to forward onto implementations that take subtype arguments, e.g `doSomething(_: Int)`; and that can't work unless you type cast and silently fail (or crash) if you pass an incorrectly typed argument. – Hamish Nov 13 '17 at 20:39
  • 1
    The type erasure for `Equatable`'s `==` works nicely because if the wrong typed arguments are passed, it can return `false`, because well they're not equal. Your protocol doesn't express that kind of semantic. – Hamish Nov 13 '17 at 20:39
  • 1
    Btw, you can often simplify these kinds of type erasers with closures – see for example https://stackoverflow.com/a/43264274/2976878, which achieves a similar goal as `AnyHashable`. [Rob Napier has also written a good article on type erasers](http://robnapier.net/erasure). – Hamish Nov 13 '17 at 20:43

0 Answers0