在协议中使用Swift的Equatable

76

我不认为这可以做到,但无论如何我会问一下。 我有一个协议:

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引用,所以它不能再被用作类型,只能用作泛型参数。有没有人找到一种方法,在不使协议成为不可用类型的情况下应用运算符于协议?
11个回答

60
如果您直接在协议上实现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

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

希望这可以帮到您。


22

你应该三思而后行,考虑是否需要将协议符合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??")
}

你提到了在==的实现中进行类型检查,但问题是你对这两种类型没有更多的信息,除了它们都是Pet,而且你不知道可能是Pet的所有东西(也许稍后会添加BirdRabbit)。如果你真的需要这个功能,另一种方法可以是模拟像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),但希望保持简单,因为我有一个情况,其中有不同的类代表相同的上下文事物,因此我希望它们被视为相等,即使它们是不同的实现。 - drekka
1
在两种不同类型的对象上实现相等性是一个很棘手的问题。我建议另一种比较它们的选项,也许是通过将一种类型转换为另一种类型,然后再进行比较。 - Scott H

19

如果:

  • 你愿意放弃操作符语法(即调用 isEqual(to:) 而不是 ==
  • 你控制协议(因此可以向其中添加一个 isEqual(to:) 函数)

则在不使用类型擦除的情况下,可以确定遵循 Swift 协议的相等性。

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:)


2
这是截至Swift 5.1的最佳答案 - 如果提供另一个示例,使isEqual(to:)为除Shape之外的其他协议提供默认实现,则会更好。 - adib

14

也许这对你有帮助:

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

7
谢谢,但是当您尝试执行类似于let first = Test(name: "Test1") as X'这样的操作时,就会出现不允许将“X”作为类型的错误。我的问题是我的类深入到API中,并且只通过协议公开自己。因此,这些实例的使用必须是协议类型。 - drekka

6

所有说你不能为协议实现Equatable的人都是没有尝试够努力。这里是一个解决方案(Swift 4.1),适用于你的协议X示例:

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" 错误。

这个应该有更多的赞,解决方案很实在。 - aeskreis
那相当酷。需要指出的一个陷阱是它不会比较类型,这是有意为之的。所以,如果你有 struct Square: Shape { let sides = 4 }struct Rhombus: Shape { let sides = 4 },那么 Square() == Rhombus() 是 true。或许一个简单的解决方法是,在 == 操作符中修改返回值,变成: return (r as? L)?.something == r.something,以便同时检查类型。 - Lou Zell
@LouZell,有趣。你检查过 r as? L 是否有效吗? - kelin
是的!对于我的有限情况它起作用了(我需要区分协议的两个构象,例如上面的菱形!=正方形)。 - Lou Zell
这是一个可靠的解决方案。它肯定应该有更多的赞同票。 - Rizwan Ahmed

5

不确定为什么需要所有协议实例符合 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()
    }
}

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

3
我建议不要使用多态实现==。这有点像一种代码坏味道。如果你想给框架用户提供一些可以测试相等性的东西,那么你应该真正提供一个struct,而不是一个protocol。这并不意味着不能是protocol提供struct
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示例的方式非常接近。


2

您需要为您的类类型实现一个有限制的协议扩展。在该扩展中,您应该实现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")
}

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


1
谢谢。我正在尝试避免将协议扩展约束到类中,因为(也许这是个不好的想法,但它适用于我的情况),我有协议的不同实现,实际上都指向同一个东西,因此我希望它们被视为相等。 - drekka

2

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

1

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

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

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接