Cómo probar la igualdad de enumeraciones Swift con valores asociados

193

Quiero probar la igualdad de dos valores de enumeración Swift. Por ejemplo:

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

Sin embargo, el compilador no compilará la expresión de igualdad:

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

¿Tengo que definir mi propia sobrecarga del operador de igualdad? Esperaba que el compilador Swift lo manejara automáticamente, al igual que Scala y Ocaml.

Jay Lieske
fuente
1
Rdar abierto: // 17408414 ( openradar.me/radar?id=6404186140835840 ).
Jay Lieske
1
Desde Swift 4.1 debido a SE-0185 , Swift también admite la síntesis Equatabley Hashablepara enumeraciones con valores asociados.
jedwidz

Respuestas:

245

Swift 4.1+

Como @jedwidz ha señalado útilmente, desde Swift 4.1 (debido a SE-0185 , Swift también admite la síntesis Equatabley Hashablepara enumeraciones con valores asociados.

Entonces, si está utilizando Swift 4.1 o posterior, lo siguiente sintetizará automáticamente los métodos necesarios para que XCTAssert(t1 == t2)funcione. La clave es agregar el Equatableprotocolo a su enumeración.

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

Antes de Swift 4.1

Como otros han señalado, Swift no sintetiza los operadores de igualdad necesarios automáticamente. Sin embargo, permítanme proponer una implementación más limpia (en mi humilde opinión):

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

Está lejos de ser ideal, hay mucha repetición, pero al menos no es necesario hacer cambios anidados con sentencias if dentro.

radex
fuente
39
Lo que apesta de esto es que necesita usar la declaración predeterminada en el conmutador, por lo que si agrega un nuevo caso de enumeración, el compilador no se asegura de agregar la cláusula para comparar ese nuevo caso de igualdad: usted ¡solo tiene que recordar y tener cuidado cuando realice cambios más adelante!
Michael Waterfall
20
Puede deshacerse del problema mencionado por @MichaelWaterfall al reemplazarlo defaultpor case (.Name, _): return false; case(.Number, _): return false.
Kazmasaurus
25
Mejor: case (.Name(let a), .Name(let b)) : return a == betc.
Martin R
1
Con la cláusula where, ¿no se probará cada caso hasta que alcance el valor predeterminado para todos false? Puede ser trivial, pero ese tipo de cosas pueden sumar en ciertos sistemas.
Christopher Swasey
1
Para que esto funcione tanto enumy ==función debe ser implementada en un ámbito global (fuera del alcance de su vista controlador).
Andrej
77

La implementación Equatablees una exageración en mi humilde opinión. Imagine que tiene una enumeración complicada y grande con muchos casos y muchos parámetros diferentes. Todos estos parámetros también tendrán que haberse Equatableimplementado. Además, ¿quién dijo que compara los casos de enumeración con todo o nada? ¿Qué tal si está probando valor y ha tropezado solo con un parámetro enum particular? Sugeriría un enfoque simple, como:

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

... o en caso de evaluación de parámetros:

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

Encuentre una descripción más elaborada aquí: https://mdcdeveloper.wordpress.com/2016/12/16/unit-testing-swift-enums/

mbpro
fuente
¿Podría dar un ejemplo más completo cuando intente usar esto no como prueba?
teradyl
No estoy seguro de cuál es la pregunta aquí. if casey guard caseson simplemente construcciones de lenguaje, puede usarlas en cualquier lugar al probar la igualdad de enumeraciones en este caso, no solo en las Pruebas unitarias.
mbpro
3
Si bien técnicamente esta respuesta no responde a la pregunta, sospecho que en realidad hace que muchas personas que lleguen aquí a través de la búsqueda, se den cuenta de que estaban comenzando con la pregunta incorrecta. ¡Gracias!
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
    }
}
neoneye
fuente
Esto también se puede escribir con algo como case (.Simple(let v0), .Simple(let v1)) También el operador puede estar staticdentro de la enumeración. Mira mi respuesta aquí.
LShi
15

Parece que no hay un operador de igualdad generado por el compilador para las enumeraciones, ni para las estructuras.

"Si crea su propia clase o estructura para representar un modelo de datos complejo, por ejemplo, entonces el significado de" igual a "para esa clase o estructura no es algo que Swift pueda adivinar para usted". [1]

Para implementar la comparación de igualdad, uno escribiría algo como:

@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] Consulte "Operadores de equivalencia" en https://developer.apple.com/library/content/documentation/Swift/Conceptual/Swift_Programming_Language/AdvancedOperators.html#//apple_ref/doc/uid/TP40014097-CH27-XID_43

paiv
fuente
14

Aquí hay otra opción. Es principalmente lo mismo que los demás, excepto que evita las instrucciones de cambio anidadas mediante el uso de la if casesintaxis. Creo que esto lo hace un poco más legible (/ soportable) y tiene la ventaja de evitar el caso predeterminado por completo.

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
Daniel Wood
fuente
11

Estoy usando esta solución simple en el código de prueba de unidad:

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

Utiliza la interpolación de cadenas para realizar la comparación. No lo recomendaría para el código de producción, pero es conciso y hace el trabajo para las pruebas unitarias.

Nikolai Ruhe
fuente
2
Estoy de acuerdo, para las pruebas unitarias, esta es una solución decente.
Daniel Wood
Apple docs on init (stringInterpolationSegment :) dice: "No llame a este inicializador directamente. El compilador lo utiliza al interpretar las interpolaciones de cadenas". Solo úsalo "\(lhs)" == "\(rhs)".
skagedal
También puede usar String(describing:...)o el equivalente "\(...)". Pero esto no funciona si los valores asociados difieren :(
Martin
10

Otra opción sería comparar las representaciones de cadena de los casos:

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

Por ejemplo:

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
Daniel
fuente
3

Otro enfoque if casecon comas, que funciona en 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
  }
}

Así es como escribí en mi proyecto. Pero no recuerdo de dónde saqué la idea. (Busqué en Google ahora pero no vi tal uso.) Cualquier comentario sería apreciado

LShi
fuente
2

t1 y t2 no son números, son instancias de SimpleTokens con valores asociados.

Puedes decir

var t1 = SimpleToken.Number(123)

Entonces puedes decir

t1 = SimpleToken.Name(Smith) 

sin un error del compilador

Para recuperar el valor de t1, use una instrucción switch:

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

la 'ventaja' cuando se compara con la respuesta aceptada es que no hay un caso 'predeterminado' en la declaración del interruptor 'principal', por lo que si extiende su enumeración con otros casos, el compilador lo obligará a actualizar el resto del código.

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
usuario3441734
fuente
2

Ampliando la respuesta de mbpro, así es como utilicé ese enfoque para verificar la igualdad de enumeraciones rápidas con valores asociados con algunos casos extremos.

Por supuesto, puede hacer una declaración de cambio, pero a veces es bueno verificar solo un valor en una línea. Puedes hacerlo así:

// 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 desea comparar 2 condiciones en la misma cláusula if, debe usar la coma en lugar del &&operador:

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

Desde Swift 4.1 simplemente agregue Equatableprotocolo a su enumeración y use 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
iUrii
fuente
-1

Puedes comparar usando el interruptor

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")
}
Rachit
fuente
Lugar perfecto para un cambio con dos argumentos. Vea arriba cómo esto solo toma una línea de código por caso. Y su código falla para dos números que no son iguales.
gnasher729