Come testare l'uguaglianza di enumerazioni Swift con valori associati


193

Voglio testare l'uguaglianza di due valori di enumerazione Swift. Per esempio:

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

Tuttavia, il compilatore non compilerà l'espressione di uguaglianza:

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

Devo definire il mio sovraccarico dell'operatore di uguaglianza? Speravo che il compilatore Swift lo gestisse automaticamente, proprio come fanno Scala e Ocaml.


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

1
Da Swift 4.1 grazie a SE-0185 , Swift supporta anche la sintesi Equatablee Hashableper enumerazioni con valori associati.
jedwidz,

Risposte:


245

Swift 4.1+

Come ha sottolineato utile @jedwidz , da Swift 4.1 (grazie a SE-0185 , Swift supporta anche la sintesi Equatablee Hashableper enumerazioni con valori associati.

Quindi, se utilizzi Swift 4.1 o versioni successive, i seguenti sintetizzeranno automaticamente i metodi necessari in modo che XCTAssert(t1 == t2)funzionino. La chiave è aggiungere il Equatableprotocollo al tuo enum.

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

Prima di Swift 4.1

Come altri hanno notato, Swift non sintetizza automaticamente gli operatori di uguaglianza necessari. Vorrei proporre un'implementazione più pulita (IMHO), tuttavia:

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
    }
}

È tutt'altro che ideale - c'è molta ripetizione - ma almeno non è necessario eseguire switch nidificati con istruzioni if ​​all'interno.


39
La cosa che fa schifo al riguardo è che devi usare l'istruzione predefinita nello switch, quindi se aggiungi un nuovo caso enum, il compilatore non si assicura di aggiungere la clausola per confrontare quel nuovo caso per l'uguaglianza - non ti resta che ricordare e fare attenzione quando apporti modifiche in seguito!
Michael Waterfall,

20
È possibile eliminare il problema menzionato da @MichaelWaterfall sostituendolo defaultcon case (.Name, _): return false; case(.Number, _): return false.
Kazmasaurus,

25
Meglio: case (.Name(let a), .Name(let b)) : return a == becc.
Martin R,

1
Con la clausola where, ogni caso non continuerà a essere testato fino a quando non raggiunge il valore predefinito per ogni false? Potrebbe essere banale ma quel genere di cose può sommarsi in alcuni sistemi.
Christopher Swasey,

1
Perché questo funzioni entrambi enume la ==funzione deve essere implementata su un ambito globale (al di fuori dell'ambito del controller della vista).
Andrej,

77

L'implementazione Equatableè un IMHO eccessivo. Immagina di avere un enum complicato e di grandi dimensioni con molti casi e molti parametri diversi. Anche questi parametri dovranno essere Equatableimplementati. Inoltre, chi ti ha detto di confrontare i casi di enum su base tutto o niente? Che ne dici se stai testando il valore e hai cancellato solo un particolare parametro enum? Suggerirei fortemente un approccio semplice, come:

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

... o in caso di valutazione dei parametri:

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

Trova una descrizione più elaborata qui: https://mdcdeveloper.wordpress.com/2016/12/16/unit-testing-swift-enums/


Potresti dare un esempio più completo quando provi a usarlo non come base di prova?
teradyl,

Non sono sicuro di quale sia la domanda qui. if casee guard casesono semplicemente costrutti di linguaggio, puoi usarli ovunque quando testi l'uguaglianza degli enum in questo caso, non solo nei Test unitari.
mbpro,

3
Mentre tecnicamente questa risposta non risponde alla domanda, sospetto che in realtà molte persone che arrivano qui tramite la ricerca, si rendano conto che stavano facendo la domanda sbagliata per iniziare. Grazie!
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
    }
}

Questo può anche essere scritto con qualcosa di simile. case (.Simple(let v0), .Simple(let v1)) Anche l'operatore può trovarsi staticall'interno dell'enum. Vedi la mia risposta qui.
LShi

15

Non sembra esserci un operatore di uguaglianza generato dal compilatore per enum, né per strutture.

"Se crei la tua classe o struttura per rappresentare un modello di dati complesso, ad esempio, il significato di" uguale a "per quella classe o struttura non è qualcosa che Swift può indovinare per te." [1]

Per implementare il confronto sull'uguaglianza, si scriverà qualcosa del tipo:

@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] Vedi "Operatori di equivalenza" su https://developer.apple.com/library/content/documentation/Swift/Conceptual/Swift_Programming_Language/AdvancedOperators.html#//apple_ref/doc/uid/TP40014097-CH27-XID_43


14

Ecco un'altra opzione. È principalmente lo stesso degli altri, tranne che evita le istruzioni switch nidificate utilizzando la if casesintassi. Penso che questo lo renda leggermente più leggibile (/ sopportabile) e abbia il vantaggio di evitare del tutto il caso predefinito.

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

Sto usando questa semplice soluzione nel codice di unit test:

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

Utilizza l'interpolazione di stringhe per eseguire il confronto. Non lo consiglierei per il codice di produzione, ma è conciso e fa il lavoro per i test unitari.


2
Sono d'accordo, per i test unitari questa è una soluzione decente.
Daniel Wood,

Documenti Apple su init (stringInterpolationSegment :) dice: "Non chiamare direttamente questo inizializzatore. Viene utilizzato dal compilatore per l'interpretazione delle interpolazioni di stringa.". Basta usare "\(lhs)" == "\(rhs)".
skagedal,

Puoi anche usare String(describing:...)o l'equivalente "\(...)". Ma questo non funziona se i valori associati differiscono :(
Martin

10

Un'altra opzione sarebbe quella di confrontare le rappresentazioni di stringhe dei casi:

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

Per esempio:

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

Un altro approccio if casecon le virgole, che funziona in 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
  }
}

È così che ho scritto nel mio progetto. Ma non ricordo dove ho avuto l'idea. (Ho cercato su Google proprio ora ma non ho visto un tale utilizzo.) Ogni commento sarebbe apprezzato.


2

t1 e t2 non sono numeri, sono istanze di SimpleTokens con valori associati.

Si può dire

var t1 = SimpleToken.Number(123)

Puoi quindi dire

t1 = SimpleToken.Name(Smith) 

senza un errore del compilatore.

Per recuperare il valore da t1, utilizzare un'istruzione switch:

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

2

il "vantaggio" se confrontato con la risposta accettata è che non esiste un caso "predefinito" nell'istruzione switch "principale", quindi se estendi l'enum con altri casi, il compilatore ti costringerà ad aggiornare il resto del codice.

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

Espandendo la risposta di mbpro, ecco come ho usato questo approccio per verificare la parità di enumerazioni rapide con valori associati con alcuni casi limite.

Ovviamente puoi fare un'istruzione switch, ma a volte è bello controllare solo un valore in una riga. Puoi farlo in questo modo:

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

Se si desidera confrontare 2 condizioni nella stessa clausola if, è necessario utilizzare la virgola anziché l' &&operatore:

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

2

Da Swift 4.1 basta aggiungere il Equatableprotocollo al tuo enum e usare XCTAsserto 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

Puoi confrontare usando switch

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

Posto perfetto per un interruttore con due argomenti. Vedi sopra come questo richiede solo una riga di codice per caso. E il tuo codice fallisce per due numeri che non sono uguali.
gnasher729,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.