64

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

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 不喜欢这个,因为它EquatableSelf引用,它不再允许我将它用作类型。仅作为通用参数。

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

4

11 回答 11

50

如果您直接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

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

希望这可以帮助。

于 2017-10-12T21:17:57.923 回答
19

您应该三思而后行的原因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??")
}

您提到在 of 的实现中进行类型检查,==但问题是您没有关于它们之外的任何一种类型的信息Pets 并且您不知道所有可能是 a 的东西Pet(也许您将添加 aBirdRabbit稍后)。如果你真的需要这个,另一种方法可以建模像 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没有逻辑意义的信息。

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

于 2017-02-09T09:48:01.267 回答
15

如果满足以下条件,则无需类型擦除即可确定是否符合 Swift 协议的相等性:

  • 您愿意放弃运算符语法(即 callisEqual(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:)自己实现

于 2020-01-20T06:36:46.303 回答
8

也许这对你有帮助:

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
于 2017-02-09T08:15:10.487 回答
5

所有说你不能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”错误而无法编写。

于 2018-09-08T13:30:54.337 回答
5

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

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

protocol MyProtocol {
    func doSomething()
}

如果您要求符合的对象MyProtocolEquatable可以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()
    }
}

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

于 2017-02-09T11:28:58.660 回答
3

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

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示例的方式非常接近。

于 2017-02-11T04:16:03.867 回答
2

Swift 5.1 在语言中引入了一个名为 opaque types 的新特性
检查下面
的代码仍然返回一个 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
于 2019-11-27T10:35:46.667 回答
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 之类的 linter static_operator,则必须将该代码包装// 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.")
}
于 2021-01-18T15:50:53.907 回答
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")
}

但是让我建议您将运算符实现添加到您的类中。把事情简单化 ;-)

于 2017-02-09T07:15:01.593 回答
0

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

它使用 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
  }
}
于 2021-11-10T10:29:44.327 回答