Swift: Protocol Constraint on iVar + Equatable

65 Views Asked by At

Context

Consider this protocol and class in Swift 5.9:

protocol SpinnerHosting: AnyObject
{
    var spinnerItem: SpinnerItem { get }
}
final class MyViewController: NSViewController
{
    var activeChild: (NSViewController & SpinnerHosting)? = nil

    
    func pushChild(_ incomingChild: (NSViewController & SpinnerHosting))
    {
        // #1
        if incomingChild != activeChild {
           ...
        }

        // #2
        if incomingChild != activeChild! {
           ...
        }
    }
}

Problem

At point #1, Swift throws this error:

Type 'any NSViewController & SpinnerHosting' cannot conform to 'Equatable'

At point #2 (ignore the unsafe unwrapping) it throws this one:

Binary operator '!=' cannot be applied to two 'any NSViewController & SpinnerHosting' operands 

Question:

I understand why Protocols aren't Equatable. But I do NOT understand why the compiler thinks this isn't. Here, SpinnerHosting is constrained to AnyObject, which means reference types and therefore pointer-equality. But the same errors persist if I constrain the Protocol to NSViewController itself, which definitely surprises me because NSViewController is equatable.

I'm just trying to say: "Whatever NSViewController is going to occupy activeChild must have a spinnerItem property." (I realize I can do it with a subclass; that isn't my question.)

I've just seen this pattern: var foo: ([Class] & [Protocol]) in several Apple source examples and I don't see a good reason why this can't be Equatable.

1

There are 1 best solutions below

0
Luca Angeletti On BEST ANSWER

activeChild should be Equatable

You are right, NSViewController inherits from NSObject which conforms to Equatable. So, every subclass of NSViewController is still Equatable.

As a consequence a variable of type NSViewController, regardless from additional conformance requirements, is still Equatable.

✅ Infact, this code compiles just fine.

protocol SpinnerHosting: AnyObject { }
class MyViewController: NSViewController, SpinnerHosting { }
var a: (NSViewController & SpinnerHosting) = MyViewController()
var b: (NSViewController & SpinnerHosting) = a
print(a == b)

❌ The problem arises when we make a and b optional.

protocol SpinnerHosting: AnyObject { }
class MyViewController: NSViewController, SpinnerHosting { }
var a: (NSViewController & SpinnerHosting)? = MyViewController()
var b: (NSViewController & SpinnerHosting)? = a
print(a == b) // Type 'any NSViewController & SpinnerHosting' cannot conform to 'Equatable'

Now we get your error

Wait, but Swift Conditional Conformance allows us to compare optional!

The most noticeable benefit of conditional conformance is the ability for types that store other types, like Array or Optional, to conform to the Equatable protocol.

https://www.swift.org/blog/conditional-conformance/

Right, you can equate 2 optionals if they hold 2 generic elements that can be equated

protocol SpinnerHosting: AnyObject { }
class MyViewController: NSViewController, SpinnerHosting { }
var a: NSViewController? = MyViewController()
var b: NSViewController? = a
print(a == b)

However, it seems the Swift compiler is unable to infer Equatable conformance when Existential Types and Conditional Conformance are involved.

✅ You can help the Swift compiler adding an explicit casting

protocol SpinnerHosting: AnyObject { }
class MyViewController: NSViewController, SpinnerHosting { }
var a: (NSViewController & SpinnerHosting)? = MyViewController()
var b: (NSViewController & SpinnerHosting)? = a
print(a as NSViewController? == b as NSViewController?)

Now it works.

enter image description here

Here's another example

We can do a test replacing Optional with Array (which similarly benefits of Conditional Conformance).

✅ This compiles.

let list: [NSViewController] = []
list == list

❌ But as we add existential types to the party, the Conditional Conformance breaks

let list: [NSViewController & Codable] = []
list == list // Type 'any NSViewController & Codable' (aka 'any NSViewController & Decodable & Encodable') cannot conform to 'Equatable'

Hope it helps.