Swift Equatable по протоколу
Я не думаю, что это можно сделать, но я все равно спрошу. У меня есть протокол:
protocol X {}
И класс:
class Y:X {}
В остальной части моего кода я ссылаюсь на все, используя протокол X. В этом коде я хотел бы иметь возможность сделать что-то вроде:
let a:X = ...
let b:X = ...
if a == b {...}
Проблема в том, что если я попытаюсь реализовать Equatable
:
protocol X: Equatable {}
func ==(lhs:X, hrs:X) -> Bool {
if let l = lhs as? Y, let r = hrs as? Y {
return l.something == r.something
}
return false
}
Идея попытаться разрешить использование ==
скрывая реализации за протоколом.
Свифт не любит это, потому что Equatable
имеет Self
ссылки, и это больше не позволит мне использовать его как тип. Только в качестве общего аргумента.
Так кто-нибудь нашел способ применить оператор к протоколу, чтобы протокол не стал непригодным для использования в качестве типа?
11 ответов
Если вы непосредственно реализуете 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
Обратите внимание, что поскольку ластик типа соответствует протоколу, вы можете использовать экземпляры ластика типа везде, где ожидается экземпляр типа протокола.
Надеюсь это поможет.
Причина, почему вы должны дважды подумать о том, чтобы протокол соответствовал 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
(может быть, вы добавите 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
не имеет логического смысла.
Лучший способ сделать это - реализовать равенство только для самого класса / структуры и использовать другой механизм проверки типов.
Определение равенства между соответствиями протоколу Swift возможно без стирания типа, если:
- вы готовы отказаться от синтаксиса оператора (т.е. вызвать
isEqual(to:)
вместо того==
) - вы контролируете протокол (так что вы можете добавить
isEqual(to:)
Func к нему)
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:)
самих себя
Может быть, это будет полезно для вас:
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
Все люди, которые говорят, что вы не можете реализовать 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
pirnt(y == z) // true
Единственная проблема в том, что вы не можете написать let a: X = Y()
из-за ошибки "Протокол может использоваться только как общее ограничение".
Не уверен, почему вам нужны все экземпляры вашего протокола, чтобы соответствовать 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()
}
}
Таким образом, вы можете сохранить свою спецификацию прозрачной и позволить подклассам реализовать свой метод равенства только в случае необходимости.
Я бы все еще советовал против реализации ==
используя полиморфизм. Это немного запах кода. Если вы хотите дать пользователю фреймворка что-то, с чем он может проверить равенство, тогда вы действительно должны торговать 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
пример.
Вы должны реализовать расширение протокола, ограниченное вашим типом класса. Внутри этого расширения вы должны реализовать 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")
}
Но позвольте мне порекомендовать вам добавить реализацию оператора в ваш класс. Будь проще;-)
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
Я столкнулся с этой же проблемой и решил, что
==
Оператор может быть реализован в глобальной области видимости (как это было раньше), в отличие от статической функции внутри области действия протокола:
// 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
, вам придется обернуть этот код
// swiftlint:disable static_operator
к молчанию предупреждений линтера.
Затем этот код начнет компилироваться:
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.")
}
взял часть кода сверху и пришел со следующим решением.
он использует протокол 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
}
}