Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[SR-1729] Wrong == operator is called for subclasses of a class that implements Equatable #44338

Open
swift-ci opened this issue Jun 10, 2016 · 7 comments
Labels
bug A deviation from expected or documented behavior. Also: expected but undesirable behavior.

Comments

@swift-ci
Copy link
Collaborator

Previous ID SR-1729
Radar None
Original Reporter jhntrifork (JIRA User)
Type Bug
Environment

Swift 2.2

Additional Detail from JIRA
Votes 3
Component/s
Labels Bug
Assignee None
Priority Medium

md5: d6f63b5ec7206d796d6d0269a6b932b1

Issue Description:

The small Playground example below shows the problem.

{{import UIKit

class A: Equatable {
let value1: Int
init(value1: Int) {
self.value1 = value1
}
}

func ==(lhs: A, rhs: A) -> Bool {
return lhs.value1 == rhs.value1
}

class B: A {
let value2: Int
init(value1: Int, value2: Int) {
self.value2 = value2
super.init(value1: value1)
}
}

func ==(lhs: B, rhs: B) -> Bool {
return lhs.value1 == rhs.value1 && lhs.value2 == rhs.value2
}

let b1 = B(value1: 1, value2: 2)
let b2 = B(value1: 1, value2: 2)
let b3 = B(value1: 1, value2: 3)

if b1 == b2 {
"The b1 and b2 are indeed equal"
} else {
"Failed: The b1 and b2 should be equal"
}

// Using != operator fails
if b1 != b3 {
"The b1 and b2 are indeed different"
} else {
"Failed: The b1 and b3 should be different"
}

// Explicitly using == operator works fine
if !(b1 == b3) {
"The b1 and b2 are indeed different"
} else {
"Failed: The b1 and b3 should be different"
}
}}

@belkadan
Copy link
Contributor

This is correct behavior in today's Swift: overload resolution is done at compile time, and operators are never dynamically dispatched even when used in protocols. If you want dynamic dispatch behavior, you'll have to use a helper function like NSObject's isEqual(_:).

There are a few open discussions about changing this behavior in some way; if we decide not to, we should see if we can make it more obvious that this is happening.

@swift-ci
Copy link
Collaborator Author

Comment by Jon Nørrelykke (JIRA)

The result of that design decision is that
{{ x ![](= y }} isn't the same as {{ )(x == y) }}.
which is very misguiding, since both x and y implement Equatable, and Equatable states that != is automatically computed from ==.

@hamishknight
Copy link
Collaborator

@belkadan Are you sure that operators are never dynamically dispatched to? The following example dispatches to Foo's == overload (I don't see how this could be achieved with static dispatch without specialising the generic function):

struct Foo : Equatable {
    static func ==(lhs: Foo, rhs: Foo) -> Bool {
        print("foo's ==")
        return true
    }
}

func areEqual<T : Equatable>(lhs: T, rhs: T) -> Bool {
    return lhs == rhs // dynamically dispatch
}

let f = Foo()
_ = areEqual(lhs: f, rhs: f) // foo's overload is called

I think the problem is that an == overload for a subclass isn't added to the protocol witness table when its superclass already conforms to Equatable (or the subclass doesn't even get it's own protocol witness table for Equatable conformance in the first place?).

@belkadan
Copy link
Contributor

Sorry, I meant == isn't dispatched like a class method; you are correct that it is dispatched dynamically like a protocol requirement. But yes, the subclass does not get to provide new members to satisfy the conformance. This is important because a protocol can be added to a base class in one module and a subclass created in another module. (It's not the only design decision, but it is the design we have today, and any change would have to go through the Swift Evolution Process.)

@slavapestov
Copy link
Member

Note that we now allow protocols to be defined inside types, but class operators still need to be final. However at least now the syntax makes sense for allowing overridable operators to be defined; I think at this point it's just a matter of implementation.

@swift-ci
Copy link
Collaborator Author

Comment by Ryan Lovelett (JIRA)

For my team, another point of consideration is a class that extends NSManagedObject it is illegal to override NSObject's isEqual. AFAICT it is used internal to CoreData and cannot safely be changed.

class SubClass : NSManagedObject {

  ...

  static func == (lhs: SubClass, rhs: SubClass) -> Bool {
    ...
  }

  // ❌ Crashes at runtime with: ❌
  // "Class 'SubClass' for entity 'SubClass' has an illegal override of NSManagedObject -isEqual:"
  override func isEqual(_ object: Any?) -> Bool {
    guard let rhs = object as? SubClass else {
      return false
    }
    return self == rhs
  }

}

This basically means that any custom sub-class of NSManagedObject cannot really be Equatable. There are just too many edge cases where it will fall into dynamic dispatch; like if it is in an Sequence, Optional, or ImplicitlyUnwrappedOptional.

To me, it certainly is not obvious at the call site which path the code will take. I think this violates the principle of least astonishment.

I suppose it is worth noting that in Swift 3.1 SubClass! would have called the == override but in Swift 3.2 it now behaves like SubClass? does.

@swift-ci
Copy link
Collaborator Author

swift-ci commented Aug 6, 2018

Comment by Grigory Entin (JIRA)

I wonder if some kind of diagnostics, or anything at compile time would be possible in this case. At quick glance, it looks like it makes sense either to make/treat such methods (utilizing default protocol implementation) as "final" in base class (because effectively you can not override them at all, even though they're "present" in the base class) or at least provide a warning on attempt to define the matching method in the subclass (I don't know how such a warning could be suppressed though).

I believe this thing (mixing subclasses and protocols/different kinds of "dynamic dispatch" for protocols and classes) should be covered somehow in the documentation, to avoid the confusion.

@swift-ci swift-ci transferred this issue from apple/swift-issues Apr 25, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug A deviation from expected or documented behavior. Also: expected but undesirable behavior.
Projects
None yet
Development

No branches or pull requests

4 participants