Swift Equatable on a protocol

前端 未结 10 2102
时光说笑
时光说笑 2021-01-30 12:42

I don\'t think this can be done but I\'ll ask anyway. I have a protocol:

protocol X {}

And a class:

class Y:X {}
相关标签:
10条回答
  • 2021-01-30 13:13

    You have to implement a protocol extension constrained to your class type. Inside that extension you should implement the Equatable operator.

    public protocol Protocolable: class, Equatable
    {
        // Other stuff here...
    }
    
    public extension Protocolable where Self: TheClass
    {
        public static func ==(lhs: Self, rhs:Self) -> Bool 
        {
            return lhs.name == rhs.name
        } 
    }
    
    
    public class TheClass: Protocolable
    {
        public var name: String
    
        public init(named name: String)
        {
            self.name = name
        }
    }
    
    let aClass: TheClass = TheClass(named: "Cars")
    let otherClass: TheClass = TheClass(named: "Wall-E")
    
    if aClass == otherClass
    {
        print("Equals")
    }
    else
    {
        print("Non Equals")
    }
    

    But let me recommend you add the operator implementation to your class. Keep It Simple ;-)

    0 讨论(0)
  • 2021-01-30 13:15

    Determining equality across conformances to a Swift protocol is possible without type erasure if:

    • you are willing to forgo the operator syntax (i.e. call isEqual(to:) instead of ==)
    • you control the protocol (so you can add an isEqual(to:) func to it)
    import XCTest
    
    protocol Shape {
        func isEqual (to: Shape) -> Bool
    }
    
    extension Shape where Self : Equatable {
        func isEqual (to: Shape) -> Bool {
            return (to as? Self).flatMap({ $0 == self }) ?? false
        }
    }
    
    struct Circle : Shape, Equatable {
        let radius: Double
    }
    
    struct Square : Shape, Equatable {
        let edge: Double
    }
    
    class ProtocolConformanceEquality: XCTestCase {
    
        func test() {
            // Does the right thing for same type
            XCTAssertTrue(Circle(radius: 1).isEqual(to: Circle(radius: 1)))
            XCTAssertFalse(Circle(radius: 1).isEqual(to: Circle(radius: 2)))
    
            // Does the right thing for different types
            XCTAssertFalse(Square(edge: 1).isEqual(to: Circle(radius: 1)))
        }
    
    }
    

    Any conformances don't conform to Equatable will need to implement isEqual(to:) themselves

    0 讨论(0)
  • 2021-01-30 13:15

    Swift 5.1 introduces a new feature into the language called opaque types
    Check code below
    that still gets back a X, which might be an Y, a Z, or something else that conforms to the X protocol,
    but the compiler knows exactly what is being returned

    protocol X: Equatable { }
    class Y: X {
        var something = 3
        static func == (lhs: Y, rhs: Y) -> Bool {
            return lhs.something == rhs.something
        }
        static func make() -> some X {
            return Y() 
        }
    }
    class Z: X {
        var something = "5"
        static func == (lhs: Z, rhs: Z) -> Bool {
            return lhs.something == rhs.something
        }
        static func make() -> some X {
            return Z() 
        }
    }
    
    
    
    let a = Z.make()
    let b = Z.make()
    
    a == b
    
    0 讨论(0)
  • 2021-01-30 13:17

    I would still advise against implementing == using polymorphism. It's a bit of a code smell. If you want to give the framework user something he can test equality with then you should really be vending a struct, not a protocol. That's not to say that it can't be the protocols that are vending the structs though:

    struct Info: Equatable {
      let a: Int
      let b: String
    
      static func == (lhs: Info, rhs: Info) -> Bool {
        return lhs.a == rhs.a && lhs.b == rhs.b
      }
    }
    
    protocol HasInfo {
      var info: Info { get }
    }
    
    class FirstClass: HasInfo {
      /* ... */
    }
    
    class SecondClass: HasInfo {
      /* ... */
    }
    
    let x: HasInfo = FirstClass( /* ... */ )
    let y: HasInfo = SecondClass( /* ... */ )
    
    print(x == y) // nope
    print(x.info == y.info) // yep
    

    I think this more efficiently communicates your intent, which is basically "you have these things and you don't know if they are the same things, but you do know they have the same set of properties and you can test whether those properties are the same." That is pretty close to how I would implement that Money example.

    0 讨论(0)
  • 2021-01-30 13:25

    I came cross this same issue and I figured that the == operator can be implemented in the global scope (as it used to be), as opposed to a static func inside the protocol's scope:

    // This should go in the global scope
    
    public func == (lhs: MyProtocol?, rhs: MyProtocol?) -> Bool { return lhs?.id == rhs?.id }
    public func != (lhs: MyProtocol?, rhs: MyProtocol?) -> Bool { return lhs?.id != rhs?.id }
    

    Note that if you use linters such as SwiftLint's static_operator, you'll have to wrap that code around // swiftlint:disable static_operator to silent linter warnings.

    Then this code will start compiling:

    let obj1: MyProtocol = ConcreteType(id: "1")
    let obj2: MyProtocol = ConcreteType(id: "2")
    if obj1 == obj2 {
        print("They're equal.")
    } else {
        print("They're not equal.")
    }
    
    0 讨论(0)
  • 2021-01-30 13:27

    If you directly implement Equatable on a protocol, it will not longer be usable as a type, which defeats the purpose of using a protocol. Even if you just implement == functions on protocols without Equatable conformance, results can be erroneous. See this post on my blog for a demonstration of these issues:

    https://khawerkhaliq.com/blog/swift-protocols-equatable-part-one/

    The approach that I have found to work best is to use type erasure. This allows making == comparisons for protocol types (wrapped in type erasers). It is important to note that while we continue to work at the protocol level, the actual == comparisons are delegated to the underlying concrete types to ensure correct results.

    I have built a type eraser using your brief example and added some test code at the end. I have added a constant of type String to the protocol and created two conforming types (structs are the easiest for demonstration purposes) to be able to test the various scenarios.

    For a detailed explanation of the type erasure methodology used, check out part two of the above blog post:

    https://khawerkhaliq.com/blog/swift-protocols-equatable-part-two/

    The code below should support the equality comparison that you wanted to implement. You just have to wrap the protocol type in a type eraser instance.

    protocol X {
        var name: String { get }
        func isEqualTo(_ other: X) -> Bool
        func asEquatable() -> AnyEquatableX
    }
    
    extension X where Self: Equatable {
        func isEqualTo(_ other: X) -> Bool {
            guard let otherX = other as? Self else { return false }
            return self == otherX
        }
        func asEquatable() -> AnyEquatableX {
            return AnyEquatableX(self)
        }
    }
    
    struct Y: X, Equatable {
        let name: String
        static func ==(lhs: Y, rhs: Y) -> Bool {
            return lhs.name == rhs.name
        }
    }
    
    struct Z: X, Equatable {
        let name: String
        static func ==(lhs: Z, rhs: Z) -> Bool {
            return lhs.name == rhs.name
        }
    }
    
    struct AnyEquatableX: X, Equatable {
        var name: String { return value.name }
        init(_ value: X) { self.value = value }
        private let value: X
        static func ==(lhs: AnyEquatableX, rhs: AnyEquatableX) -> Bool {
            return lhs.value.isEqualTo(rhs.value)
        }
    }
    
    // instances typed as the protocol
    let y: X = Y(name: "My name")
    let z: X = Z(name: "My name")
    let equalY: X = Y(name: "My name")
    let unequalY: X = Y(name: "Your name")
    
    // equality tests
    print(y.asEquatable() == z.asEquatable())           // prints false
    print(y.asEquatable() == equalY.asEquatable())      // prints true
    print(y.asEquatable() == unequalY.asEquatable())    // prints false
    

    Note that since the type eraser conforms to the protocol, you can use instances of the type eraser anywhere an instance of the protocol type is expected.

    Hope this helps.

    0 讨论(0)
提交回复
热议问题