【问题标题】:Swift Equatable on a protocolSwift Equatable 协议
【发布时间】:2017-06-27 01:49:33
【问题描述】:

我不认为这是可以做到的,但我还是会问。我有一个协议:

protocol X {}

还有一个班级:

class Y:X {}

在我的其余代码中,我使用协议 X 引用所有内容。在该代码中,我希望能够执行以下操作:

let a:X = ...
let b:X = ...
if a == b {...}

问题是如果我尝试实现Equatable:

protocol X: Equatable {}
func ==(lhs:X, rhs:X) -> Bool {
    if let l = lhs as? Y, let r = hrs as? Y {
        return l.something == r.something
    }
    return false
} 

尝试允许使用==,同时将实现隐藏在协议后面的想法。

Swift 不喜欢这样,因为 Equatable 有 Self 引用,它不再允许我将其用作类型。仅作为通用参数。

那么有没有人找到一种方法来将运算符应用于协议而不会使协议无法作为一种类型使用?

【问题讨论】:

    标签: ios swift protocols equatable


    【解决方案1】:

    你必须为你的类类型实现一个协议扩展约束。在该扩展中,您应该实现 Equatable 运算符。

    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")
    }
    

    但我建议您将运算符实现添加到您的类中。保持简单;-)

    【讨论】:

    • 谢谢。我试图避免将协议扩展限制到一个类,因为(也许这是一个坏主意,但它适用于我的情况)我有不同的协议实现,它们实际上指的是同一件事,因此我想要他们被视为平等。
    【解决方案2】:

    也许这对你有帮助:

    protocol X:Equatable {
        var name: String {get set}
    
    }
    
    extension X {
        static func ==(lhs: Self, rhs: Self) -> Bool {
            return lhs.name == rhs.name
        }
    }
    
    struct Test : X {
        var name: String
    }
    
    let first = Test(name: "Test1")
    let second = Test(name: "Test2")
    
    print(first == second) // false
    

    【讨论】:

    • 谢谢,但是直到您尝试执行类似 `let first = Test(name: "Test1") as X' 之类的操作,然后您会收到关于不允许 'X' 作为类型的错误。我的问题是我的类在 API 的深处,并且只通过协议暴露自己。所以这些实例的使用必须是协议类型的。
    【解决方案3】:

    您应该三思而后行的协议是否符合Equatable 的原因是,在许多情况下它只是没有意义。考虑这个例子:

    protocol Pet: Equatable {
      var age: Int { get }
    }
    
    extension Pet {
      static func == (lhs: Pet, rhs: Pet) -> Bool {
        return lhs.age == rhs.age
      }
    }
    
    struct Dog: Pet {
      let age: Int
      let favoriteFood: String
    }
    
    struct Cat: Pet {
      let age: Int
      let favoriteLitter: String
    }
    
    let rover: Pet = Dog(age: "1", favoriteFood: "Pizza")
    let simba: Pet = Cat(age: "1", favoriteLitter: "Purina")
    
    if rover == simba {
      print("Should this be true??")
    }
    

    您提到在== 的实现中进行类型检查,但问题是除了Pets 之外,您没有关于任何一种类型的信息,而且您不知道所有可能是@ 的东西987654325@(也许您稍后会添加Bird 和Rabbit)。如果你真的需要这个,另一种方法可以建模像 C# 这样的语言如何实现相等,方法是:

    protocol IsEqual {
      func isEqualTo(_ object: Any) -> Bool
    }
    
    protocol Pet: IsEqual {
      var age: Int { get }
    }
    
    struct Dog: Pet {
      let age: Int
      let favoriteFood: String
    
      func isEqualTo(_ object: Any) -> Bool {
        guard let otherDog = object as? Dog else { return false }
    
        return age == otherDog.age && favoriteFood == otherDog.favoriteFood
      }
    }
    
    struct Cat: Pet {
      let age: Int
      let favoriteLitter: String
    
      func isEqualTo(_ object: Any) -> Bool {
        guard let otherCat = object as? Cat else { return false }
    
        return age == otherCat.age && favoriteLitter == otherCat.favoriteLitter
      }
    }
    
    let rover: Pet = Dog(age: "1", favoriteFood: "Pizza")
    let simba: Pet = Cat(age: "1", favoriteLitter: "Purina")
    
    if !rover.isEqualTo(simba) {
      print("That's more like it.")
    }
    

    如果你真的想要,你可以在不实现Equatable的情况下实现==:

    static func == (lhs: IsEqual, rhs: IsEqual) -> Bool { return lhs.isEqualTo(rhs) }
    

    在这种情况下,您必须注意的一件事是继承。因为您可以向下转换继承类型并删除可能使isEqualTo 没有逻辑意义的信息。

    最好的方法是只在类/结构本身上实现相等,并使用另一种机制进行类型检查。

    【讨论】:

    • 谢谢。我一直在考虑isEqual 选项(来自Java),但希望保持简单,因为我有一个案例,我有不同的类代表相同的上下文事物,因此我希望它们被视为平等,即使它们是不同的实现。
    • 在两种不同类型的对象上实现相等是一个滑坡。我会推荐另一种比较它们的选项,可能是将一种类型转换为另一种类型,然后进行比较。
    【解决方案4】:

    不确定为什么您需要协议的所有实例都符合 Equatable,但我更喜欢让类实现它们的相等方法。

    在这种情况下,我会让协议保持简单:

    protocol MyProtocol {
        func doSomething()
    }
    

    如果您要求符合MyProtocol 的对象也是Equatable,您可以使用MyProtocol & Equatable 作为类型约束:

    // Equivalent: func doSomething<T>(element1: T, element2: T) where T: MyProtocol & Equatable {
    func doSomething<T: MyProtocol & Equatable>(element1: T, element2: T) {
        if element1 == element2 {
            element1.doSomething()
        }
    }
    

    这样你可以保持你的规范清晰,让子类只在需要时实现它们的相等方法。

    【讨论】:

      【解决方案5】:

      我仍然建议不要使用多态性来实现==。有点代码味道。如果你想给框架用户一些他可以测试相等性的东西,那么你真的应该出售struct,而不是protocol。这并不是说不可能是 protocols 出售 structs:

      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
      

      我认为这更有效地传达了您的意图,基本上是“您拥有这些东西,但您不知道它们是否相同,但您确实知道它们具有相同的一组属性,您可以测试这些属性是否相同属性是一样的。”这与我实现 Money 示例的方式非常接近。

      【讨论】:

        【解决方案6】:

        如果你直接在一个协议上实现Equatable,它将不再可以作为一个类型使用,这违背了使用协议的目的。即使您只是在没有Equatable 一致性的协议上实现== 函数,结果也可能是错误的。有关这些问题的演示,请参阅我博客上的这篇文章:

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

        我发现最有效的方法是使用类型擦除。这允许对协议类型(包装在类型橡皮擦中)进行== 比较。需要注意的是,虽然我们继续在协议级别工作,但实际的 == 比较被委托给底层的具体类型以确保正确的结果。

        我使用您的简短示例构建了一个类型橡皮擦,并在最后添加了一些测试代码。我在协议中添加了一个String 类型的常量,并创建了两个一致的类型(结构最容易用于演示目的),以便能够测试各种场景。

        有关所使用的类型擦除方法的详细说明,请查看上述博文的第二部分:

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

        下面的代码应该支持您想要实现的相等比较。您只需将协议类型包装在类型橡皮擦实例中。

        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
        

        请注意,由于类型擦除器符合协议,因此您可以在任何需要协议类型实例的地方使用类型擦除器的实例。

        希望这会有所帮助。

        【讨论】:

          【解决方案7】:

          所有说你不能为协议实现Equatable 的人只是不够努力。这是您的协议X 示例的解决方案(Swift 4.1):

          protocol X: Equatable {
              var something: Int { get }
          }
          
          // Define this operator in the global scope!
          func ==<L: X, R: X>(l: L, r: R) -> Bool {
              return l.something == r.something
          }
          

          而且它有效!

          class Y: X {
              var something: Int = 14
          }
          
          struct Z: X {
              let something: Int = 9
          }
          
          let y = Y()
          let z = Z()
          print(y == z) // false
          
          y.something = z.something
          print(y == z) // true
          

          唯一的问题是你不能写let a: X = Y(),因为“Protocol can only be used as a generic constraint”错误。

          【讨论】:

          • 这应该有更多的赞成票,固体解决方案
          • 这很酷。需要指出的一个问题是,它在设计上不比较类型。所以如果你有struct Square: Shape { let sides = 4 } 和struct Rhombus: Shape { let sides = 4 } 那么Square() == Rhombus() 是真的。也可以检查类型的一个简单修复方法是将 == 运算符的返回更改为:return (r as? L)?.something == r.something
          • @LouZell,很有趣。你检查r as? L 是否有效吗?
          • 是的!它适用于我的有限情况(我需要区分协议的两个conformers,例如上面示例中的 Rhombus != Square)
          【解决方案8】:

          Swift 5.1 向语言中引入了一项称为不透明类型的新功能
          检查下面的代码
          仍然返回一个 X,它可能是一个 Y、一个 Z 或其他符合 X 协议的东西,
          但编译器确切知道返回的是什么

          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
          

          【讨论】:

            【解决方案9】:

            在符合 Swift 协议的情况下确定相等性是可能的无需类型擦除如果:

            • 您愿意放弃运算符语法(即调用isEqual(to:) 而不是==)
            • 您可以控制协议(因此您可以添加一个isEqual(to:) 函数)
            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)))
                }
            
            }
            

            任何不符合Equatable 的一致性都需要自己实现isEqual(to:)

            【讨论】:

            • 这是 Swift 5.1 的最佳答案——如果提供另一个示例会更好,例如 isEqual(to:) 将为 Shape 以外的其他协议提供默认实现
            【解决方案10】:

            我遇到了同样的问题,我认为== 运算符可以在全局范围内实现(就像以前一样),而不是在协议范围内的静态函数:

            // 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 }
            

            请注意,如果您使用诸如 SwiftLint 的 static_operator 之类的 linter,则必须将该代码包装在 // swiftlint:disable static_operator 周围以发出静默 linter 警告。

            然后这段代码将开始编译:

            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.")
            }
            

            【讨论】:

              【解决方案11】:

              从上面获取了一些代码,并提供了以下解决方案。

              它使用 IsEqual 协议而不是 Equatable 协议,并且通过一些行代码,您将能够相互比较任何两个协议对象,无论它们是否可选,在数组中,甚至添加比较日期,而我在做。

              protocol IsEqual {
                  func isEqualTo(_ object: Any) -> Bool
              }
              
              func == (lhs: IsEqual?, rhs: IsEqual?) -> Bool {
                  guard let lhs = lhs else { return rhs == nil }
                  guard let rhs = rhs else { return false }
                  return lhs.isEqualTo(rhs) }
              
              func == (lhs: [IsEqual]?, rhs: [IsEqual]?) -> Bool {
                  guard let lhs = lhs else { return rhs == nil }
                  guard let rhs = rhs else { return false }
                  
                  guard lhs.count == rhs.count else { return false }
                  for i in 0..<lhs.count {
                      if !lhs[i].isEqualTo(rhs[i]) {
                          return false
                      }
                  }
                  return true
              }
              
              func == (lhs: Date?, rhs: Date?) -> Bool {
                  guard let lhs = lhs else { return rhs == nil }
                  guard let rhs = rhs else { return false }
                  
                  return lhs.compare(rhs) == .orderedSame
              }
              
              protocol Pet: IsEqual {
                var age: Int { get }
              }
              
              struct Dog: Pet {
                let age: Int
                let favoriteFood: String
              
                func isEqualTo(_ object: Any) -> Bool {
                  guard let otherDog = object as? Dog else { return false }
              
                  return age == otherDog.age && favoriteFood == otherDog.favoriteFood
                }
              }
              

              【讨论】:

                猜你喜欢
                • 1970-01-01
                • 1970-01-01
                • 2015-11-02
                • 1970-01-01
                • 2016-09-29
                • 1970-01-01
                • 2015-10-18
                • 1970-01-01
                相关资源
                最近更新 更多