Comment tester l'égalité des énumérations Swift avec les valeurs associées


193

Je veux tester l'égalité de deux valeurs d'énumération Swift. Par exemple:

enum SimpleToken {
    case Name(String)
    case Number(Int)
}
let t1 = SimpleToken.Number(123)
let t2 = SimpleToken.Number(123)
XCTAssert(t1 == t2)

Cependant, le compilateur ne compilera pas l'expression d'égalité:

error: could not find an overload for '==' that accepts the supplied arguments
    XCTAssert(t1 == t2)
    ^~~~~~~~~~~~~~~~~~~

Dois-je définir ma propre surcharge de l'opérateur d'égalité? J'espérais que le compilateur Swift le gérerait automatiquement, un peu comme le font Scala et Ocaml.


1
Rdar ouvert: // 17408414 ( openradar.me/radar?id=6404186140835840 ).
Jay Lieske

1
De Swift 4.1 à SE-0185 , Swift prend également en charge la synthèse Equatableet les Hashableénumérations avec les valeurs associées.
jedwidz

Réponses:


245

Swift 4.1+

Comme @jedwidz l' a utilement souligné, à partir de Swift 4.1 (en raison de SE-0185 , Swift prend également en charge la synthèse Equatableet les Hashableénumérations avec des valeurs associées.

Donc, si vous êtes sur Swift 4.1 ou plus récent, ce qui suit synthétisera automatiquement les méthodes nécessaires pour que cela XCTAssert(t1 == t2)fonctionne. La clé est d'ajouter le Equatableprotocole à votre énumération.

enum SimpleToken: Equatable {
    case Name(String)
    case Number(Int)
}
let t1 = SimpleToken.Number(123)
let t2 = SimpleToken.Number(123)

Avant Swift 4.1

Comme d'autres l'ont noté, Swift ne synthétise pas automatiquement les opérateurs d'égalité nécessaires. Permettez-moi de proposer une mise en œuvre plus propre (IMHO), cependant:

enum SimpleToken: Equatable {
    case Name(String)
    case Number(Int)
}

public func ==(lhs: SimpleToken, rhs: SimpleToken) -> Bool {
    switch (lhs, rhs) {
    case let (.Name(a),   .Name(b)),
         let (.Number(a), .Number(b)):
      return a == b
    default:
      return false
    }
}

C'est loin d'être idéal - il y a beaucoup de répétitions - mais au moins vous n'avez pas besoin de faire des commutateurs imbriqués avec des instructions if à l'intérieur.


39
Ce qui est nul à ce sujet, c'est que vous devez utiliser l'instruction par défaut dans le commutateur, donc si vous ajoutez un nouveau cas d'énumération, le compilateur ne s'assure pas d'ajouter la clause pour comparer ce nouveau cas d'égalité - vous il suffit de se souvenir et d'être prudent lorsque vous apportez des modifications ultérieurement!
Michael Waterfall

20
Vous pouvez vous débarrasser du problème @MichaelWaterfall mentionné en le remplaçant defaultpar case (.Name, _): return false; case(.Number, _): return false.
Kazmasaurus

25
Mieux: case (.Name(let a), .Name(let b)) : return a == betc.
Martin R

1
Avec la clause where, chaque cas ne continuera-t-il pas à être testé jusqu'à ce qu'il atteigne la valeur par défaut pour tous false? Cela peut être trivial, mais ce genre de choses peut s'accumuler dans certains systèmes.
Christopher Swasey

1
Pour que cela fonctionne, enumet la ==fonction doit être implémentée sur une portée globale (en dehors de la portée de votre contrôleur de vue).
Andrej

77

La mise en œuvre Equatableest une IMHO exagérée. Imaginez que vous ayez une énumération compliquée et volumineuse avec de nombreux cas et de nombreux paramètres différents. Ces paramètres devront tous avoir été Equatableimplémentés également. De plus, qui a dit que vous compariez les cas d'énumération sur la base du tout ou rien? Que diriez-vous si vous testez la valeur et n'avez stubbed qu'un seul paramètre d'énumération particulier? Je suggérerais fortement une approche simple, comme:

if case .NotRecognized = error {
    // Success
} else {
    XCTFail("wrong error")
}

... ou en cas d'évaluation des paramètres:

if case .Unauthorized401(_, let response, _) = networkError {
    XCTAssertEqual(response.statusCode, 401)
} else {
    XCTFail("Unauthorized401 was expected")
}

Trouvez une description plus élaborée ici: https://mdcdeveloper.wordpress.com/2016/12/16/unit-testing-swift-enums/


Pourriez-vous donner un exemple plus complet en essayant de ne pas l'utiliser dans le cadre d'un test?
teradyl

Je ne sais pas quelle est la question ici. if caseet guard casesont simplement des constructions de langage, vous pouvez les utiliser n'importe où lorsque vous testez l'égalité des énumérations dans ce cas, pas seulement dans les tests unitaires.
mbpro

3
Bien que techniquement cette réponse ne réponde pas à la question, je soupçonne que de nombreuses personnes qui arrivent ici via la recherche se rendent compte qu'elles posaient la mauvaise question pour commencer. Merci!
Nikolay Suvandzhiev

15
enum MyEnum {
    case None
    case Simple(text: String)
    case Advanced(x: Int, y: Int)
}

func ==(lhs: MyEnum, rhs: MyEnum) -> Bool {
    switch (lhs, rhs) {
    case (.None, .None):
        return true
    case let (.Simple(v0), .Simple(v1)):
        return v0 == v1
    case let (.Advanced(x0, y0), .Advanced(x1, y1)):
        return x0 == x1 && y0 == y1
    default:
        return false
    }
}

Cela peut également être écrit avec quelque chose comme case (.Simple(let v0), .Simple(let v1)) aussi l'opérateur peut être staticà l'intérieur de l'énumération. Voir ma réponse ici.
LShi

15

Il semble qu'aucun opérateur d'égalité généré par le compilateur pour les enums, ni pour les structs.

"Si vous créez votre propre classe ou structure pour représenter un modèle de données complexe, par exemple, alors la signification de" égal à "pour cette classe ou structure n'est pas quelque chose que Swift peut deviner pour vous." [1]

Pour implémenter la comparaison d'égalité, on écrirait quelque chose comme:

@infix func ==(a:SimpleToken, b:SimpleToken) -> Bool {
    switch(a) {

    case let .Name(sa):
        switch(b) {
        case let .Name(sb): return sa == sb
        default: return false
        }

    case let .Number(na):
        switch(b) {
        case let .Number(nb): return na == nb
        default: return false
        }
    }
}

[1] Voir «Opérateurs d'équivalence» sur https://developer.apple.com/library/content/documentation/Swift/Conceptual/Swift_Programming_Language/AdvancedOperators.html#//apple_ref/doc/uid/TP40014097-CH27-XID_43


14

Voici une autre option. C'est principalement le même que les autres sauf que cela évite les instructions de commutateur imbriquées en utilisant la if casesyntaxe. Je pense que cela le rend légèrement plus lisible (/ supportable) et a l'avantage d'éviter complètement le cas par défaut.

enum SimpleToken: Equatable {
    case Name(String)
    case Number(Int)
}
extension SimpleToken {
    func isEqual(st: SimpleToken)->Bool {
        switch self {
        case .Name(let v1): 
            if case .Name(let v2) = st where v1 == v2 { return true }
        case .Number(let i1): 
            if case .Number(let i2) = st where i1 == i2 { return true }
        }
        return false
    }
}

func ==(lhs: SimpleToken, rhs: SimpleToken)->Bool {
    return lhs.isEqual(rhs)
}

let t1 = SimpleToken.Number(1)
let t2 = SimpleToken.Number(2)
let t3 = SimpleToken.Name("a")
let t4 = SimpleToken.Name("b")

t1 == t1  // true
t1 == t2  // false
t3 == t3  // true
t3 == t4  // false
t1 == t3  // false

11

J'utilise cette solution de contournement simple dans le code de test unitaire:

extension SimpleToken: Equatable {}
func ==(lhs: SimpleToken, rhs: SimpleToken) -> Bool {
    return String(stringInterpolationSegment: lhs) == String(stringInterpolationSegment: rhs)
}

Il utilise une interpolation de chaîne pour effectuer la comparaison. Je ne le recommanderais pas pour le code de production, mais il est concis et fait le travail pour les tests unitaires.


2
Je suis d'accord, pour les tests unitaires, c'est une solution décente.
Daniel Wood le

La documentation Apple sur init (stringInterpolationSegment :) dit: "N'appelez pas cet initialiseur directement. Il est utilisé par le compilateur lors de l'interprétation des interpolations de chaînes.". Utilisez simplement "\(lhs)" == "\(rhs)".
skagedal

Vous pouvez également utiliser String(describing:...)ou l'équivalent "\(...)". Mais cela ne fonctionne pas si les valeurs associées diffèrent :(
Martin

10

Une autre option serait de comparer les représentations sous forme de chaîne des observations:

XCTAssert(String(t1) == String(t2))

Par exemple:

let t1 = SimpleToken.Number(123) // the string representation is "Number(123)"
let t2 = SimpleToken.Number(123)
let t3 = SimpleToken.Name("bob") // the string representation is "Name(\"bob\")"

String(t1) == String(t2) //true
String(t1) == String(t3) //false

3

Une autre approche utilisant if casedes virgules, qui fonctionne dans Swift 3:

enum {
  case kindOne(String)
  case kindTwo(NSManagedObjectID)
  case kindThree(Int)

  static func ==(lhs: MyEnumType, rhs: MyEnumType) -> Bool {
    if case .kindOne(let l) = lhs,
        case .kindOne(let r) = rhs {
        return l == r
    }
    if case .kindTwo(let l) = lhs,
        case .kindTwo(let r) = rhs {
        return l == r
    }
    if case .kindThree(let l) = lhs,
        case .kindThree(let r) = rhs {
        return l == r
    }
    return false
  }
}

C'est ainsi que j'ai écrit dans mon projet. Mais je ne me souviens pas d'où j'ai eu l'idée. (Je viens de googler mais je n'ai pas vu une telle utilisation.) Tout commentaire serait apprécié.


2

t1 et t2 ne sont pas des nombres, ce sont des instances de SimpleTokens avec des valeurs associées.

Tu peux dire

var t1 = SimpleToken.Number(123)

Vous pouvez alors dire

t1 = SimpleToken.Name(Smith) 

sans erreur du compilateur.

Pour récupérer la valeur de t1, utilisez une instruction switch:

switch t1 {
    case let .Number(numValue):
        println("Number: \(numValue)")
    case let .Name(strValue):
        println("Name: \(strValue)")
}

2

«l'avantage» par rapport à la réponse acceptée est qu'il n'y a pas de cas «par défaut» dans l'instruction de commutateur «principal», donc si vous étendez votre énumération avec d'autres cas, le compilateur vous forcera à mettre à jour le reste du code.

enum SimpleToken: Equatable {
    case Name(String)
    case Number(Int)
}
extension SimpleToken {
    func isEqual(st: SimpleToken)->Bool {
        switch self {
        case .Name(let v1):
            switch st {
            case .Name(let v2): return v1 == v2
            default: return false
            }
        case .Number(let i1):
            switch st {
            case .Number(let i2): return i1 == i2
            default: return false
            }
        }
    }
}


func ==(lhs: SimpleToken, rhs: SimpleToken)->Bool {
    return lhs.isEqual(rhs)
}

let t1 = SimpleToken.Number(1)
let t2 = SimpleToken.Number(2)
let t3 = SimpleToken.Name("a")
let t4 = SimpleToken.Name("b")

t1 == t1  // true
t1 == t2  // false
t3 == t3  // true
t3 == t4  // false
t1 == t3  // false

2

En développant la réponse de mbpro, voici comment j'ai utilisé cette approche pour vérifier l'égalité des énumérations rapides avec les valeurs associées avec certains cas extrêmes.

Bien sûr, vous pouvez faire une instruction switch, mais il est parfois agréable de ne vérifier qu'une seule valeur sur une ligne. Vous pouvez le faire comme ceci:

// NOTE: there's only 1 equal (`=`) sign! Not the 2 (`==`) that you're used to for the equality operator
// 2nd NOTE: Your variable must come 2nd in the clause

if case .yourEnumCase(associatedValueIfNeeded) = yourEnumVariable {
  // success
}

Si vous souhaitez comparer 2 conditions dans la même clause if, vous devez utiliser la virgule au lieu de l' &&opérateur:

if someOtherCondition, case .yourEnumCase = yourEnumVariable {
  // success
}

2

Depuis Swift 4.1, ajoutez simplement un Equatableprotocole à votre énumération et utilisez XCTAssertou XCTAssertEqual:

enum SimpleToken : Equatable {
    case Name(String)
    case Number(Int)
}
let t1 = SimpleToken.Number(123)
let t2 = SimpleToken.Number(123)
XCTAssertEqual(t1, t2) // OK

-1

Vous pouvez comparer en utilisant le commutateur

enum SimpleToken {
    case Name(String)
    case Number(Int)
}

let t1 = SimpleToken.Number(123)
let t2 = SimpleToken.Number(123)

switch(t1) {

case let .Number(a):
    switch(t2) {
        case let . Number(b):
            if a == b
            {
                println("Equal")
        }
        default:
            println("Not equal")
    }
default:
    println("No Match")
}

Endroit parfait pour un switch avec deux arguments. Voir ci-dessus comment cela ne prend qu'une ligne de code par cas. Et votre code échoue pour deux nombres qui ne sont pas égaux.
gnasher729
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.