7

Just a sanity check with the community before I file a radar:

In a .h Obj-C file:

@protocol myProto <NSObject> 
@end

In a .swift file (that has access to the above protocol definition via bridging header):

class myClass {
    // This line compiles fine
    var dictOne: [NSObject:Int]?
    // This line fails with "Type 'myProto' does not conform to protocol 'Hashable'"
    var dictTwo: [myProto:Int]?
}

Inspection of the NSObject class reveals that it (or the NSObjectProtocol that it maps to) does not implement the hashValue method required by the Hashable protocol, nor does it explicity adopt it.

So, somewhere behind the scenes the NSObject is being flagged as Hashable despite this, but does not extend to protocols that adopt NSObject/NSObjectProtocol.

Have I got a bug or am I missing something?

:) Teo

Additional info:

The documentation suggests that:

  • A dictionary's key type's only requirements is that it's Hashable and that it implements ==.
  • You can indeed use a protocol.
Hash Values for Dictionary Key Types

A type must be hashable in order to be used as a dictionary’s key type—that is, the type must provide a way to compute a hash value for itself. A hash value is an Int value that is the same for all objects that compare equal, such that if a == b, it follows that a.hashValue == b.hashValue.

All of Swift’s basic types (such as String, Int, Double, and Bool) are hashable by default, and all of these types can be used as the keys of a dictionary. Enumeration member values without associated values (as described in Enumerations) are also hashable by default.

NOTE You can use your own custom types as dictionary key types by making them conform to the Hashable protocol from Swift’s standard library. Types that conform to the Hashable protocol must provide a gettable Int property called hashValue, and must also provide an implementation of the “is equal” operator (==). The value returned by a type’s hashValue property is not required to be the same across different executions of the same program, or in different programs. For more information about conforming to protocols, see Protocols.

Teo Sartori
  • 1,082
  • 14
  • 24

3 Answers3

11

NSObjectProtocol doesn't inherit from Hashable. That's the crucial problem here.

It can't actually inherit from Hashable because Hashable requires a method called hashValue while NSObjectProtocol requires a method called hash.

On the other hand, NSObject class can implement both NSObjectProtocol and Hashable.

The same problem happens with Equatable.

Edit:

There is another more subtle problem. You cannot use a protocol somewhere where Equatable is expected, you always need to use a class type or a value type that adopts Equatable. The reason is that it's not enough for a key to adopt Equatable, all the keys in the dictionary have to be equatable with each other.

For example, if you have a class A and a class B, both conforming to Equatable, then you can compare instances of A with other instances of A and you can compare instances of B with other instances of B but you cannot compare instances of A with instances of B. That's why you cannot use instances of A and instances of B as keys in the same dictionary.

Note that every NSObject is equatable with any other NSObject, so NSObject is an allowed type for keys in a dictionary.

Sulthan
  • 128,090
  • 22
  • 218
  • 270
  • Thanks for your comments. I do see (as mentioned in my question) that NSObjectProtocol does not implement the required Hashable method, but I don't follow the "more subtle problem" argument; The equatable part simply compares hashes and doesn't care what A and B are as long as they adopt the protocol. – Teo Sartori Jul 25 '14 at 12:15
  • The problem as I see it is that the bridging is not managing to map Swift's Hashable `hashValue` and Equatable `==` to the Objective-C NSObject protocol's `isEqual:` and `hash`. – Teo Sartori Jul 25 '14 at 12:16
  • @TeoSartori It's not possible to bridge them that way. `Obj-C` has no way to represent the `==` operator and you can't just bridge a method name to another method name. An object can implement both but you can't just make them equal, that would be a dangeours precedent. Anyway, you can't use protocol types for dictionary keys, even if that's a pure Swift protocol. – Sulthan Jul 25 '14 at 12:18
0

I agree that this seems like a missing feature. For anyone interested, I made a little wrapper to deal with it.

struct HashableNSObject<T: NSObjectProtocol>: Hashable {
    let value: T

    init(_ value: T) {
        self.value = value
    }

    static func == (lhs: HashableNSObject<T>, rhs: HashableNSObject<T>) -> Bool {
        return lhs.value.isEqual(rhs.value)
    }

    func hash(into hasher: inout Hasher) {
        hasher.combine(value.hash)
    }
}

You can even make this non-generic by replacing T with NSObjectProtocol, but I think that it's cleaner this way.

It's fairly easy to use, but a bit long when you have to keep mapping with the contained value.

let foo = [MyObjectProtocol]()
let bar = Set<HashableNSObject<MyObjectProtocol>>()
fun1(foo.map { HashableNSObject($0) })
fun2(bar.map { $0.value })
Guy Kogus
  • 7,251
  • 1
  • 27
  • 32
0

Alternatively, you can use the NSObjectProtocol.hash as your key.

var dictTwo: [Int:Int]?
dictTwo[myProtoInstance.hash] = 0
Brody Robertson
  • 8,506
  • 2
  • 47
  • 42