Set with element of NSObject subclasses didn't work as expected


(Zhao Xin) #1

Please see the code first.

import Foundation

class Foo:Hashable {

    let value:Int

    public var hashValue: Int { return value }

    public static func ==(lhs: Foo, rhs: Foo) -> Bool {

        return lhs.value == rhs.value

    }

    init(_ value:Int) {

        self.value = value

    }

}

let fooSetA:Set = [Foo(8), Foo(9)]

let fooSetB:Set = [Foo(9), Foo(10)]

let fooResultC = fooSetA.intersection(fooSetB) // {{value 9}}

let fooResultD = fooSetA.subtracting(fooSetB) // {{value 8}}

class Bar:NSObject {

    let value:Int

    override public var hashValue: Int { return value }

    public static func ==(lhs: Bar, rhs: Bar) -> Bool {

        return lhs.value == rhs.value

    }

    init(_ value:Int) {

        self.value = value

    }

}

let barSetA:Set = [Bar(8), Bar(9)]

let barSetB:Set = [Bar(9), Bar(10)]

let barResultC = barSetA.intersection(barSetB) // Set([])

let barResultD = barSetA.subtracting(barSetB) // {{NSObject, value 9},
{NSObject, value 8}}

Behaviors of `func intersection(Set<Set.Element>)` and `func
subtracting(Set<Set.Element>)` were different between normal Swift class
and NSObject subclasses. I had thought they should be the same. It seemed
that Set<NSObject> relied on addresses of NSObject instances instead of
their hashValues. That made the Set useless.

Swift version: 3.1 (swiftlang-802.0.48 clang-802.0.48)
Xcode 8.3 (8E162)

Zhaoxin


(Zhao Xin) #2

Turns out that for `NSObject`, protocol `Equatable` wasn't used. Instead,
it used `NSObjectProtocol.isEqual(_ object: Any?)`. Also, override `func
isEqual(_ object: Any?) -> Bool` requires to override `var hash: Int { get
}` as well.

I think this behavior should be mentioned in Swift docs or manual in `Set`
section.

Below code works.

class Bar:NSObject {

    let value:Int

    override public var hashValue: Int { return value }

    public static func ==(lhs: Bar, rhs: Bar) -> Bool {

        return lhs.value == rhs.value

    }

    // required by NSObjectProtocol

    override func isEqual(_ object: Any?) -> Bool {

        if let rhs = object as? Bar {

            return self == rhs

        }

        return false

    }

    override var hash: Int { return self.hashValue }

    init(_ value:Int) {

        self.value = value

    }

}

let barSetA:Set = [Bar(8), Bar(9)]

let barSetB:Set = [Bar(9), Bar(10)]

let barResultC = barSetA.intersection(barSetB) // {{NSObject, value 9}}

let barResultD = barSetA.subtracting(barSetB) // {{NSObject, value 8}}

Gladly I find it here
<http://stackoverflow.com/questions/32726524/swift-2-0-set-not-working-as-expected-when-containing-nsobject-subclass>
.

Zhaoxin

···

On Wed, Mar 29, 2017 at 3:50 AM, Zhao Xin <owenzx@gmail.com> wrote:

Please see the code first.

import Foundation

class Foo:Hashable {

    let value:Int

    public var hashValue: Int { return value }

    public static func ==(lhs: Foo, rhs: Foo) -> Bool {

        return lhs.value == rhs.value

    }

    init(_ value:Int) {

        self.value = value

    }

}

let fooSetA:Set = [Foo(8), Foo(9)]

let fooSetB:Set = [Foo(9), Foo(10)]

let fooResultC = fooSetA.intersection(fooSetB) // {{value 9}}

let fooResultD = fooSetA.subtracting(fooSetB) // {{value 8}}

class Bar:NSObject {

    let value:Int

    override public var hashValue: Int { return value }

    public static func ==(lhs: Bar, rhs: Bar) -> Bool {

        return lhs.value == rhs.value

    }

    init(_ value:Int) {

        self.value = value

    }

}

let barSetA:Set = [Bar(8), Bar(9)]

let barSetB:Set = [Bar(9), Bar(10)]

let barResultC = barSetA.intersection(barSetB) // Set([])

let barResultD = barSetA.subtracting(barSetB) // {{NSObject, value 9},
{NSObject, value 8}}

Behaviors of `func intersection(Set<Set.Element>)` and `func
subtracting(Set<Set.Element>)` were different between normal Swift class
and NSObject subclasses. I had thought they should be the same. It seemed
that Set<NSObject> relied on addresses of NSObject instances instead of
their hashValues. That made the Set useless.

Swift version: 3.1 (swiftlang-802.0.48 clang-802.0.48)
Xcode 8.3 (8E162)

Zhaoxin


(Joe Groff) #3

This is a known issue—when you redeclare `static func ==` inside Bar, that does not override `NSObject.==`, and moreover *cannot*, since overriding `NSObject.==` requires an implementation that accepts two NSObjects. We ought to consider this an error, or at least a warning. NSObject conforms to Equatable on behalf of all its subclasses by implementing `==` in terms of `isEqual:` and `hashValue` in terms of `hash`. Even though you can override `hashValue` for Swift, it's probably still safer to override `hash` and `isEqual` in order to get common behavior between ObjC and Swift, since ObjC will not use Swift's conformances. Perhaps the NSObject implementation of `hashValue` should be final to help with this.

-Joe

···

On Mar 28, 2017, at 12:50 PM, Zhao Xin via swift-users <swift-users@swift.org> wrote:

Please see the code first.

import Foundation

class Foo:Hashable {
    let value:Int
    
    public var hashValue: Int { return value }
    public static func ==(lhs: Foo, rhs: Foo) -> Bool {
        return lhs.value == rhs.value
    }
    
    init(_ value:Int) {
        self.value = value
    }
}

let fooSetA:Set = [Foo(8), Foo(9)]
let fooSetB:Set = [Foo(9), Foo(10)]
let fooResultC = fooSetA.intersection(fooSetB) // {{value 9}}
let fooResultD = fooSetA.subtracting(fooSetB) // {{value 8}}

class Bar:NSObject {
    let value:Int
    
    override public var hashValue: Int { return value }
    public static func ==(lhs: Bar, rhs: Bar) -> Bool {
        return lhs.value == rhs.value
    }
    
    init(_ value:Int) {
        self.value = value
    }
}

let barSetA:Set = [Bar(8), Bar(9)]
let barSetB:Set = [Bar(9), Bar(10)]
let barResultC = barSetA.intersection(barSetB) // Set([])
let barResultD = barSetA.subtracting(barSetB) // {{NSObject, value 9}, {NSObject, value 8}}

Behaviors of `func intersection(Set<Set.Element>)` and `func subtracting(Set<Set.Element>)` were different between normal Swift class and NSObject subclasses. I had thought they should be the same. It seemed that Set<NSObject> relied on addresses of NSObject instances instead of their hashValues. That made the Set useless.


(Saagar Jha) #4

NSObject’s hash (which Set uses) is done via ObjectIdentifier, which IIRC uses something along the lines of the object's address. That’s why you’re getting the behavior you’re seeing.

Saagar Jha

···

On Mar 28, 2017, at 20:20, Zhao Xin via swift-users <swift-users@swift.org> wrote:

Turns out that for `NSObject`, protocol `Equatable` wasn't used. Instead, it used `NSObjectProtocol.isEqual(_ object: Any?)`. Also, override `func isEqual(_ object: Any?) -> Bool` requires to override `var hash: Int { get }` as well.

I think this behavior should be mentioned in Swift docs or manual in `Set` section.

Below code works.

class Bar:NSObject {
    let value:Int
    
    override public var hashValue: Int { return value }
    public static func ==(lhs: Bar, rhs: Bar) -> Bool {
        return lhs.value == rhs.value
    }
    // required by NSObjectProtocol
    override func isEqual(_ object: Any?) -> Bool {
        if let rhs = object as? Bar {
            return self == rhs
        }
        return false
    }
    override var hash: Int { return self.hashValue }
    
    init(_ value:Int) {
        self.value = value
    }
}

let barSetA:Set = [Bar(8), Bar(9)]
let barSetB:Set = [Bar(9), Bar(10)]
let barResultC = barSetA.intersection(barSetB) // {{NSObject, value 9}}
let barResultD = barSetA.subtracting(barSetB) // {{NSObject, value 8}}

Gladly I find it here <http://stackoverflow.com/questions/32726524/swift-2-0-set-not-working-as-expected-when-containing-nsobject-subclass>.

Zhaoxin

On Wed, Mar 29, 2017 at 3:50 AM, Zhao Xin <owenzx@gmail.com <mailto:owenzx@gmail.com>> wrote:
Please see the code first.

import Foundation

class Foo:Hashable {
    let value:Int
    
    public var hashValue: Int { return value }
    public static func ==(lhs: Foo, rhs: Foo) -> Bool {
        return lhs.value == rhs.value
    }
    
    init(_ value:Int) {
        self.value = value
    }
}

let fooSetA:Set = [Foo(8), Foo(9)]
let fooSetB:Set = [Foo(9), Foo(10)]
let fooResultC = fooSetA.intersection(fooSetB) // {{value 9}}
let fooResultD = fooSetA.subtracting(fooSetB) // {{value 8}}

class Bar:NSObject {
    let value:Int
    
    override public var hashValue: Int { return value }
    public static func ==(lhs: Bar, rhs: Bar) -> Bool {
        return lhs.value == rhs.value
    }
    
    init(_ value:Int) {
        self.value = value
    }
}

let barSetA:Set = [Bar(8), Bar(9)]
let barSetB:Set = [Bar(9), Bar(10)]
let barResultC = barSetA.intersection(barSetB) // Set([])
let barResultD = barSetA.subtracting(barSetB) // {{NSObject, value 9}, {NSObject, value 8}}

Behaviors of `func intersection(Set<Set.Element>)` and `func subtracting(Set<Set.Element>)` were different between normal Swift class and NSObject subclasses. I had thought they should be the same. It seemed that Set<NSObject> relied on addresses of NSObject instances instead of their hashValues. That made the Set useless.

Swift version: 3.1 (swiftlang-802.0.48 clang-802.0.48)
Xcode 8.3 (8E162)

Zhaoxin

_______________________________________________
swift-users mailing list
swift-users@swift.org
https://lists.swift.org/mailman/listinfo/swift-users


(Zhao Xin) #5

Now I understand it. What I don't understand is why there is nowhere metioning this. All docs are talking about Hashable, clearly that NSObject didn't rely on it.

Zhaoxin

···

Get Outlook for iOS<https://aka.ms/o0ukef>
________________________________
From: Saagar Jha <saagar@saagarjha.com>
Sent: Wednesday, March 29, 2017 11:47:51 AM
To: Zhao Xin
Cc: Swift Users List
Subject: Re: [swift-users] Set with element of NSObject subclasses didn't work as expected

NSObject’s hash (which Set uses) is done via ObjectIdentifier, which IIRC uses something along the lines of the object's address. That’s why you’re getting the behavior you’re seeing.

Saagar Jha

On Mar 28, 2017, at 20:20, Zhao Xin via swift-users <swift-users@swift.org<mailto:swift-users@swift.org>> wrote:

Turns out that for `NSObject`, protocol `Equatable` wasn't used. Instead, it used `NSObjectProtocol.isEqual(_ object: Any?)`. Also, override `func isEqual(_ object: Any?) -> Bool` requires to override `var hash: Int { get }` as well.

I think this behavior should be mentioned in Swift docs or manual in `Set` section.

Below code works.

class Bar:NSObject {
    let value:Int

    override public var hashValue: Int { return value }
    public static func ==(lhs: Bar, rhs: Bar) -> Bool {
        return lhs.value == rhs.value
    }
    // required by NSObjectProtocol
    override func isEqual(_ object: Any?) -> Bool {
        if let rhs = object as? Bar {
            return self == rhs
        }
        return false
    }
    override var hash: Int { return self.hashValue }

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

let barSetA:Set = [Bar(8), Bar(9)]
let barSetB:Set = [Bar(9), Bar(10)]
let barResultC = barSetA.intersection(barSetB) // {{NSObject, value 9}}

let barResultD = barSetA.subtracting(barSetB) // {{NSObject, value 8}}

Gladly I find it here<http://stackoverflow.com/questions/32726524/swift-2-0-set-not-working-as-expected-when-containing-nsobject-subclass>.

Zhaoxin

On Wed, Mar 29, 2017 at 3:50 AM, Zhao Xin <owenzx@gmail.com<mailto:owenzx@gmail.com>> wrote:
Please see the code first.

import Foundation

class Foo:Hashable {
    let value:Int

    public var hashValue: Int { return value }
    public static func ==(lhs: Foo, rhs: Foo) -> Bool {
        return lhs.value == rhs.value
    }

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

let fooSetA:Set = [Foo(8), Foo(9)]
let fooSetB:Set = [Foo(9), Foo(10)]
let fooResultC = fooSetA.intersection(fooSetB) // {{value 9}}
let fooResultD = fooSetA.subtracting(fooSetB) // {{value 8}}

class Bar:NSObject {
    let value:Int

    override public var hashValue: Int { return value }
    public static func ==(lhs: Bar, rhs: Bar) -> Bool {
        return lhs.value == rhs.value
    }

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

let barSetA:Set = [Bar(8), Bar(9)]
let barSetB:Set = [Bar(9), Bar(10)]
let barResultC = barSetA.intersection(barSetB) // Set([])
let barResultD = barSetA.subtracting(barSetB) // {{NSObject, value 9}, {NSObject, value 8}}

Behaviors of `func intersection(Set<Set.Element>)` and `func subtracting(Set<Set.Element>)` were different between normal Swift class and NSObject subclasses. I had thought they should be the same. It seemed that Set<NSObject> relied on addresses of NSObject instances instead of their hashValues. That made the Set useless.

Swift version: 3.1 (swiftlang-802.0.48 clang-802.0.48)
Xcode 8.3 (8E162)

Zhaoxin

_______________________________________________
swift-users mailing list
swift-users@swift.org<mailto:swift-users@swift.org>
https://lists.swift.org/mailman/listinfo/swift-users


(Hooman Mehr) #6

Yes, this is a serious shortcoming in the transition to Swift as the main application programming language for Apple platforms.

With the existing architecture (lack of stable Swift ABI) system frameworks and any binary distributed frameworks still have to be written in Objective-C. If you are already an experienced developer of Apple platforms, you already know the behavior and expectations of ObjC runtime and NSObject family. But new developers are coming to the platform everyday and now they are starting with Swift. This really needs to be better addressed as more people start developing for Apple platforms using Swift with no prior knowledge of ObjC runtime and NSObject.

I think the best way to draw some attention to this is by filing bug reports on lack of adequate documentation, at least.

Also, please note that if you want to define operators that act on class hierarchies, you should put all of the logic of the operator in a method and have the operator simply delegate to the method to preserve (some) polymorphism. Although infix operators can’t easily be fully polymorphic, given the lack of multiple (or at least dual) dispatch. The problematic part of getting Equatable right in this case is that any instance of a subclass by definition should work as an instance of its superclass and this can create bugs if you are not careful. `Equatable` protocol requires both sides of the equality test to have identical type for the mathematical definition of equality to hold. In practice, you may need to relax this in some situations, but you should be fully aware of what you are doing.

···

On Mar 29, 2017, at 2:36 AM, Zhao Xin via swift-users <swift-users@swift.org> wrote:

Now I understand it. What I don't understand is why there is nowhere metioning this. All docs are talking about Hashable, clearly that NSObject didn't rely on it.

Zhaoxin

Get Outlook for iOS <https://aka.ms/o0ukef>
From: Saagar Jha <saagar@saagarjha.com>
Sent: Wednesday, March 29, 2017 11:47:51 AM
To: Zhao Xin
Cc: Swift Users List
Subject: Re: [swift-users] Set with element of NSObject subclasses didn't work as expected

NSObject’s hash (which Set uses) is done via ObjectIdentifier, which IIRC uses something along the lines of the object's address. That’s why you’re getting the behavior you’re seeing.

Saagar Jha

On Mar 28, 2017, at 20:20, Zhao Xin via swift-users <swift-users@swift.org <mailto:swift-users@swift.org>> wrote:

Turns out that for `NSObject`, protocol `Equatable` wasn't used. Instead, it used `NSObjectProtocol.isEqual(_ object: Any?)`. Also, override `func isEqual(_ object: Any?) -> Bool` requires to override `var hash: Int { get }` as well.

I think this behavior should be mentioned in Swift docs or manual in `Set` section.

Below code works.

class Bar:NSObject {
    let value:Int
    
    override public var hashValue: Int { return value }
    public static func ==(lhs: Bar, rhs: Bar) -> Bool {
        return lhs.value == rhs.value
    }
    // required by NSObjectProtocol
    override func isEqual(_ object: Any?) -> Bool {
        if let rhs = object as? Bar {
            return self == rhs
        }
        return false
    }
    override var hash: Int { return self.hashValue }
    
    init(_ value:Int) {
        self.value = value
    }
}

let barSetA:Set = [Bar(8), Bar(9)]
let barSetB:Set = [Bar(9), Bar(10)]
let barResultC = barSetA.intersection(barSetB) // {{NSObject, value 9}}
let barResultD = barSetA.subtracting(barSetB) // {{NSObject, value 8}}

Gladly I find it here <http://stackoverflow.com/questions/32726524/swift-2-0-set-not-working-as-expected-when-containing-nsobject-subclass>.

Zhaoxin

On Wed, Mar 29, 2017 at 3:50 AM, Zhao Xin <owenzx@gmail.com <mailto:owenzx@gmail.com>> wrote:
Please see the code first.

import Foundation

class Foo:Hashable {
    let value:Int
    
    public var hashValue: Int { return value }
    public static func ==(lhs: Foo, rhs: Foo) -> Bool {
        return lhs.value == rhs.value
    }
    
    init(_ value:Int) {
        self.value = value
    }
}

let fooSetA:Set = [Foo(8), Foo(9)]
let fooSetB:Set = [Foo(9), Foo(10)]
let fooResultC = fooSetA.intersection(fooSetB) // {{value 9}}
let fooResultD = fooSetA.subtracting(fooSetB) // {{value 8}}

class Bar:NSObject {
    let value:Int
    
    override public var hashValue: Int { return value }
    public static func ==(lhs: Bar, rhs: Bar) -> Bool {
        return lhs.value == rhs.value
    }
    
    init(_ value:Int) {
        self.value = value
    }
}

let barSetA:Set = [Bar(8), Bar(9)]
let barSetB:Set = [Bar(9), Bar(10)]
let barResultC = barSetA.intersection(barSetB) // Set([])
let barResultD = barSetA.subtracting(barSetB) // {{NSObject, value 9}, {NSObject, value 8}}

Behaviors of `func intersection(Set<Set.Element>)` and `func subtracting(Set<Set.Element>)` were different between normal Swift class and NSObject subclasses. I had thought they should be the same. It seemed that Set<NSObject> relied on addresses of NSObject instances instead of their hashValues. That made the Set useless.

Swift version: 3.1 (swiftlang-802.0.48 clang-802.0.48)
Xcode 8.3 (8E162)

Zhaoxin

_______________________________________________
swift-users mailing list
swift-users@swift.org <mailto:swift-users@swift.org>
https://lists.swift.org/mailman/listinfo/swift-users

_______________________________________________
swift-users mailing list
swift-users@swift.org
https://lists.swift.org/mailman/listinfo/swift-users


(Hooman Mehr) #7

Good idea. And as I mentioned in my other reply, we need further emphasis and clarification in the documentation who are starting Apple development with Swift with no prior knowledge of ObjC/NSObject stuff.

···

On Mar 29, 2017, at 11:36 AM, Joe Groff via swift-users <swift-users@swift.org> wrote:

Perhaps the NSObject implementation of `hashValue` should be final to help with this.


(Hooman Mehr) #8

I dropped two words in my initial sentence. The complete sentence is:

We need further emphasis and clarification in the documentation for people who are starting Apple development with Swift with no prior knowledge of ObjC/NSObject stuff.

···

On Mar 29, 2017, at 11:40 AM, Hooman Mehr via swift-users <swift-users@swift.org> wrote:

On Mar 29, 2017, at 11:36 AM, Joe Groff via swift-users <swift-users@swift.org <mailto:swift-users@swift.org>> wrote:

Perhaps the NSObject implementation of `hashValue` should be final to help with this.

Good idea. And as I mentioned in my other reply, we need further emphasis and clarification in the documentation who are starting Apple development with Swift with no prior knowledge of ObjC/NSObject stuff.

_______________________________________________
swift-users mailing list
swift-users@swift.org
https://lists.swift.org/mailman/listinfo/swift-users