Using NSObject subclass instance as key in Dictionary


(Etan Kissling) #1

I want to use instances of a custom class as Dictionary key.
This requires the class to conform to Hashable.

func ==(lhs: KeyType1, rhs: KeyType1) -> Bool { return lhs.id == rhs.id }

final class KeyType1: Hashable, CustomStringConvertible {
    let id: String
    init(id: String) { self.id = id }
    var hashValue: Int { return id.hashValue }
    var description: String { return id }
}

Now I can use KeyType1 instances as key in Dictionary.

var collection1 = [KeyType1(id: "foo") : NSObject()]

Testing works fine:

    let key = collection1.first!.0
    print(" Key stored in collection: \(unsafeAddressOf(key)) -- \(key)")

    let keyCopy = KeyType1(id: key.id)
    print(" Key copy: \(unsafeAddressOf(keyCopy)) -- \(keyCopy)")

    print(" Keys equal: \(key == keyCopy)")
    print(" Hash values equal: \(key.hashValue == keyCopy.hashValue)")
    print(" Collection has item for key: \(collection1[key] != nil)")
    print("Collection has item for key copy: \(collection1[keyCopy] != nil)")

        Key stored in collection: 0x0000608000043d80 -- foo
                        Key copy: 0x00006080000440b0 -- foo
                      Keys equal: true
               Hash values equal: true
     Collection has item for key: true
Collection has item for key copy: true

Next, I repeat the same set up -- but this time KeyType is a descendant of NSObject.

func ==(lhs: KeyType2, rhs: KeyType2) -> Bool { return lhs.id == rhs.id }

final class KeyType2: NSObject { // NSObject conforms to Hashable and CustomStringConvertible.
    let id: String
    init(id: String) { self.id = id; super.init() }
    override var hashValue: Int { return id.hashValue }
    override var description: String { return id }
}

Again, I create a Dictionary based on this key class.

var collection2 = [KeyType2(id: "foo") : NSObject()]

Using the same tests, they fail now.

    let key = collection2.first!.0
    print(" Key stored in collection: \(unsafeAddressOf(key)) -- \(key)")

    let keyCopy = KeyType2(id: key.id)
    print(" Key copy: \(unsafeAddressOf(keyCopy)) -- \(keyCopy)")

    print(" Keys equal: \(key == keyCopy)")
    print(" Hash values equal: \(key.hashValue == keyCopy.hashValue)")
    print(" Collection has item for key: \(collection2[key] != nil)")
    print("Collection has item for key copy: \(collection2[keyCopy] != nil)")

        Key stored in collection: 0x0000608000044080 -- foo
                        Key copy: 0x00006080000440e0 -- foo
                      Keys equal: true
               Hash values equal: true
     Collection has item for key: true
Collection has item for key copy: false

What am I missing here?

Thanks

Etan


(Dmitri Gribenko) #2

The == overload in the second case is not the one that gets put into the
protocol witness table. When you call == on two instances of your type,
upcast to NSObject, the isEqual() method is called.

We are aware of this issue and it will be fixed when we move operators into
types.

Dmitri

···

On Thu, Dec 17, 2015 at 2:17 PM, Etan Kissling via swift-users < swift-users@swift.org> wrote:

I want to use instances of a custom class as Dictionary key.
This requires the class to conform to Hashable.

func ==(lhs: KeyType1, rhs: KeyType1) -> Bool { return lhs.id == rhs.id }

final class KeyType1: Hashable, CustomStringConvertible {
    let id: String
    init(id: String) { self.id = id }
    var hashValue: Int { return id.hashValue }
    var description: String { return id }
}

Now I can use KeyType1 instances as key in Dictionary.

var collection1 = [KeyType1(id: "foo") : NSObject()]

Testing works fine:

    let key = collection1.first!.0
    print(" Key stored in collection: \(unsafeAddressOf(key)) -- \(
key)")

    let keyCopy = KeyType1(id: key.id)
    print(" Key copy: \(unsafeAddressOf(keyCopy))
-- \(keyCopy)")

    print(" Keys equal: \(key == keyCopy)")
    print(" Hash values equal: \(key.hashValue == keyCopy.
hashValue)")
    print(" Collection has item for key: \(collection1[key] != nil)")
    print("Collection has item for key copy: \(collection1[keyCopy] != nil
)")

* Key stored in collection: 0x0000608000043d80 -- foo*
* Key copy: 0x00006080000440b0 -- foo*
* Keys equal: true*
* Hash values equal: true*
* Collection has item for key: true*
*Collection has item for key copy: true*

Next, I repeat the same set up -- but this time KeyType is a descendant of
NSObject.

func ==(lhs: KeyType2, rhs: KeyType2) -> Bool { return lhs.id == rhs.id }

final class KeyType2: NSObject { // NSObject conforms to Hashable and
CustomStringConvertible.
    let id: String
    init(id: String) { self.id = id; super.init() }
    override var hashValue: Int { return id.hashValue }
    override var description: String { return id }
}

Again, I create a Dictionary based on this key class.

var collection2 = [KeyType2(id: "foo") : NSObject()]

Using the same tests, they fail now.

    let key = collection2.first!.0
    print(" Key stored in collection: \(unsafeAddressOf(key)) -- \(
key)")

    let keyCopy = KeyType2(id: key.id)
    print(" Key copy: \(unsafeAddressOf(keyCopy))
-- \(keyCopy)")

    print(" Keys equal: \(key == keyCopy)")
    print(" Hash values equal: \(key.hashValue == keyCopy.
hashValue)")
    print(" Collection has item for key: \(collection2[key] != nil)")
    print("Collection has item for key copy: \(collection2[keyCopy] != nil
)")

* Key stored in collection: 0x0000608000044080 -- foo*
* Key copy: 0x00006080000440e0 -- foo*
* Keys equal: true*
* Hash values equal: true*
* Collection has item for key: true*
*Collection has item for key copy: false*

What am I missing here?

--
main(i,j){for(i=2;;i++){for(j=2;j<i;j++){if(!(i%j)){j=0;break;}}if
(j){printf("%d\n",i);}}} /*Dmitri Gribenko <gribozavr@gmail.com>*/


(Etan Kissling) #3

Thanks for the fast response!

Replacing the == definition with

    override func isEqual(object: AnyObject?) -> Bool {
        guard let object = object as? KeyType2 else { return false }
        return id == object.id
    }

fixed this issue!

Etan

···

On 17 Dec 2015, at 23:48, Dmitri Gribenko <gribozavr@gmail.com<mailto:gribozavr@gmail.com>> wrote:

On Thu, Dec 17, 2015 at 2:17 PM, Etan Kissling via swift-users <swift-users@swift.org<mailto:swift-users@swift.org>> wrote:
I want to use instances of a custom class as Dictionary key.
This requires the class to conform to Hashable.

func ==(lhs: KeyType1, rhs: KeyType1) -> Bool { return lhs.id == rhs.id }

final class KeyType1: Hashable, CustomStringConvertible {
    let id: String
    init(id: String) { self.id = id }
    var hashValue: Int { return id.hashValue }
    var description: String { return id }
}

Now I can use KeyType1 instances as key in Dictionary.

var collection1 = [KeyType1(id: "foo") : NSObject()]

Testing works fine:

    let key = collection1.first!.0
    print(" Key stored in collection: \(unsafeAddressOf(key)) -- \(key)")

    let keyCopy = KeyType1(id: key.id)
    print(" Key copy: \(unsafeAddressOf(keyCopy)) -- \(keyCopy)")

    print(" Keys equal: \(key == keyCopy)")
    print(" Hash values equal: \(key.hashValue == keyCopy.hashValue)")
    print(" Collection has item for key: \(collection1[key] != nil)")
    print("Collection has item for key copy: \(collection1[keyCopy] != nil)")

        Key stored in collection: 0x0000608000043d80 -- foo
                        Key copy: 0x00006080000440b0 -- foo
                      Keys equal: true
               Hash values equal: true
     Collection has item for key: true
Collection has item for key copy: true

Next, I repeat the same set up -- but this time KeyType is a descendant of NSObject.

func ==(lhs: KeyType2, rhs: KeyType2) -> Bool { return lhs.id == rhs.id }

final class KeyType2: NSObject { // NSObject conforms to Hashable and CustomStringConvertible.
    let id: String
    init(id: String) { self.id = id; super.init() }
    override var hashValue: Int { return id.hashValue }
    override var description: String { return id }
}

Again, I create a Dictionary based on this key class.

var collection2 = [KeyType2(id: "foo") : NSObject()]

Using the same tests, they fail now.

    let key = collection2.first!.0
    print(" Key stored in collection: \(unsafeAddressOf(key)) -- \(key)")

    let keyCopy = KeyType2(id: key.id)
    print(" Key copy: \(unsafeAddressOf(keyCopy)) -- \(keyCopy)")

    print(" Keys equal: \(key == keyCopy)")
    print(" Hash values equal: \(key.hashValue == keyCopy.hashValue)")
    print(" Collection has item for key: \(collection2[key] != nil)")
    print("Collection has item for key copy: \(collection2[keyCopy] != nil)")

        Key stored in collection: 0x0000608000044080 -- foo
                        Key copy: 0x00006080000440e0 -- foo
                      Keys equal: true
               Hash values equal: true
     Collection has item for key: true
Collection has item for key copy: false

What am I missing here?

The == overload in the second case is not the one that gets put into the protocol witness table. When you call == on two instances of your type, upcast to NSObject, the isEqual() method is called.

We are aware of this issue and it will be fixed when we move operators into types.

Dmitri

--
main(i,j){for(i=2;;i++){for(j=2;j<i;j++){if(!(i%j)){j=0;break;}}if
(j){printf("%d\n",i);}}} /*Dmitri Gribenko <gribozavr@gmail.com<mailto:gribozavr@gmail.com>>*/