web-dev-qa-db-fra.com

Swift Equatable sur un protocole

Je ne pense pas que cela puisse être fait, mais je demanderai quand même. J'ai un protocole:

protocol X {}

Et une classe:

class Y:X {}

Dans le reste de mon code, je fais référence à tout ce qui utilise le protocole X. Dans ce code, j'aimerais pouvoir faire quelque chose comme:

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

Le problème est que si j'essaie d'implémenter 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
} 

L'idée d'essayer de permettre l'utilisation de == tout en cachant les implémentations derrière le protocole.

Swift n'aime pas cela, car Equatable a des références Self et ne me permettra plus de l'utiliser comme type. Seulement comme argument générique.

Alors, est-ce que quelqu'un a trouvé un moyen d'appliquer un opérateur à un protocole sans que le protocole devienne inutilisable en tant que type?

27
drekka

Si vous implémentez directement Equatable sur un protocole, celui-ci ne sera plus utilisable en tant que type, ce qui irait à l'encontre de l'utilisation d'un protocole. Même si vous venez d'implémenter des fonctions == sur des protocoles sans conformité Equatable, les résultats peuvent être erronés. Voir ce post sur mon blog pour une démonstration de ces questions: 

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

L’approche que j’ai trouvée la plus efficace consiste à utiliser le type effacement. Cela permet de faire des comparaisons == pour les types de protocole (enveloppés dans des gommes à effacer). Il est important de noter que, même si nous continuons à travailler au niveau du protocole, les comparaisons réelles avec == sont déléguées aux types concrets sous-jacents pour garantir des résultats corrects.

J'ai construit une gomme à effacer à l'aide de votre bref exemple et ajouté du code de test à la fin. J'ai ajouté une constante de type String au protocole et créé deux types conformes (les structures sont les plus faciles à des fins de démonstration) pour pouvoir tester les différents scénarios. 

Pour une explication détaillée de la méthodologie d’effacement des types utilisée, consultez la deuxième partie du billet de blog ci-dessus:

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

Le code ci-dessous doit prendre en charge la comparaison d’égalité que vous souhaitez implémenter. Vous devez juste envelopper le type de protocole dans une instance de type gomme. 

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

Notez que puisque la gomme de type est conforme au protocole, vous pouvez utiliser des instances de la gomme de type partout où une instance du type de protocole est attendue. 

J'espère que cela t'aides. 

15
Khawer Khaliq

La raison pour laquelle vous devriez réfléchir à deux fois avant d’avoir un protocole conforme à Equatable est que dans de nombreux cas, cela n’a aucun sens. Considérons cet exemple:

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

Vous faites allusion à la vérification de type dans l’implémentation de ==, mais le problème est que vous n’avez aucune information sur les types au-delà d’eux étant Pets et vous ne savez pas tout ce qui pourrait être un Pet (peut-être que vous ajouterez un Bird et Rabbit plus tard). Si vous en avez vraiment besoin, une autre approche peut être de modéliser la manière dont des langages tels que C # implémentent l'égalité, en effectuant quelque chose comme:

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

Si vous le souhaitez vraiment, vous pouvez implémenter == sans implémenter Equatable:

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

Une chose à surveiller dans ce cas, toutefois, est l'héritage. Parce que vous pourriez décaler un type hérité et effacer les informations qui pourraient rendre isEqualTo non logique.

La meilleure solution consiste à implémenter l'égalité uniquement sur la classe/structure et à utiliser un autre mécanisme pour la vérification de type.

8
Scott H

Vous ne savez pas pourquoi vous avez besoin que toutes les instances de votre protocole soient conformes à Equatable, mais je préfère laisser les classes implémenter leurs méthodes d'égalité.

Dans ce cas, je laisserais le protocole simple:

protocol MyProtocol {
    func doSomething()
}

Si vous souhaitez qu'un objet conforme à MyProtocol soit également Equatable, vous pouvez utiliser MyProtocol & Equatable comme contrainte de type:

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

De cette façon, vous pouvez garder votre spécification claire et laisser les sous-classes implémenter leur méthode d'égalité uniquement si nécessaire.

6
redent84

peut-être que cela vous sera utile:

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
4

Je conseillerais toujours de ne pas implémenter == en utilisant le polymorphisme. C'est un peu une odeur de code. Si vous voulez donner quelque chose à l'utilisateur du framework avec lequel il peut tester l'égalité, alors vous devriez vraiment vendre un struct, pas un protocol. Cela ne veut pas dire que cela ne peut pas être le protocols qui vendent le 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

Je pense que cela communique plus efficacement votre intention, qui est essentiellement "vous avez ces choses et vous ne savez pas si ce sont les mêmes choses, mais vous savez qu'elles ont le même ensemble de propriétés et vous pouvez tester si ces propriétés sont les mêmes." même." C'est assez proche de la façon dont j'appliquerais cet exemple Money.

3
Scott H

Vous devez implémenter une extension de protocole contrainte à votre type de classe. Dans cette extension, vous devez implémenter l'opérateur 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")
}

Mais laissez-moi vous recommander d’ajouter l’implémentation d’opérateur à votre classe. Rester simple ;-)

1
Adolfo

Toutes les personnes qui disent que vous ne pouvez pas implémenter Equatable pour un protocole n'essayez tout simplement pas assez. Voici la solution (Swift 4.1 ) pour votre exemple de protocole 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
}

Et il fonctionne!

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

Le seul problème est que vous ne pouvez pas écrire let a: X = Y() car "Le protocole ne peut être utilisé que comme une contrainte générique" error.

0
kelin