Como testar a igualdade de enumerações Swift com valores associados

193

Eu quero testar a igualdade de dois valores de enumeração Swift. Por exemplo:

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

No entanto, o compilador não compila a expressão de igualdade:

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

Eu tenho que definir minha própria sobrecarga do operador de igualdade? Eu esperava que o compilador Swift pudesse lidar com isso automaticamente, assim como Scala e Ocaml.

Jay Lieske
fonte
1
Rdar aberto: // 17408414 ( openradar.me/radar?id=6404186140835840 ).
Jay Lieske
1
A partir do Swift 4.1, devido ao SE-0185 , o Swift também suporta a sintetização Equatablee Hashableenumerações com valores associados.
jedwidz

Respostas:

245

Swift 4.1+

Como o @jedwidz apontou de maneira útil, no Swift 4.1 (devido ao SE-0185 , o Swift também suporta sintetização Equatablee Hashableenumerações com valores associados.

Portanto, se você estiver no Swift 4.1 ou mais recente, o seguinte sintetizará automaticamente os métodos necessários para que XCTAssert(t1 == t2)funcionem. A chave é adicionar o Equatableprotocolo à sua enumeração.

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

Antes do Swift 4.1

Como outros observaram, Swift não sintetiza automaticamente os operadores de igualdade necessários. Deixe-me propor uma implementação mais limpa (IMHO), no entanto:

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á longe de ser o ideal - há muita repetição - mas pelo menos você não precisa fazer comutações aninhadas com instruções if dentro.

radex
fonte
39
O importante é que você precisa usar a instrução padrão no comutador; portanto, se você adicionar um novo caso de enum, o compilador não garantirá que você adicione a cláusula para comparar esse novo caso de igualdade - você apenas lembre-se e tenha cuidado ao fazer alterações mais tarde!
Michael Waterfall
20
Você pode se livrar do problema @MichaelWaterfall mencionado substituindo defaultpor case (.Name, _): return false; case(.Number, _): return false.
Kazmasaurus
25
Melhor: case (.Name(let a), .Name(let b)) : return a == betc.
Martin R
1
Com a cláusula where, cada caso não continuará sendo testado até atingir o padrão para cada false ? Pode ser trivial, mas esse tipo de coisa pode aumentar em certos sistemas.
Christopher Swasey
1
Para que isso funcione, as funções enume ==devem ser implementadas em um escopo global (fora do escopo do seu controlador de exibição).
Andrej
77

Implementar Equatableé um IMHO em excesso. Imagine que você tenha um enum grande e complicado com muitos casos e muitos parâmetros diferentes. Todos esses parâmetros também precisam ser Equatableimplementados. Além disso, quem disse que você compara casos enum com base em tudo ou nada? E se você estiver testando valor e tiver removido apenas um parâmetro de enumeração em particular? Eu sugeriria uma abordagem simples, como:

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

... ou no caso de avaliação de parâmetros:

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

Encontre uma descrição mais elaborada aqui: https://mdcdeveloper.wordpress.com/2016/12/16/unit-testing-swift-enums/

mbpro
fonte
Você poderia dar um exemplo mais completo ao tentar usar isso não em uma base de teste?
Teradyl 17/08/19
Não tenho certeza de qual é a questão aqui. if casee guard casesão simplesmente construções de linguagem, você pode usá-las em qualquer lugar ao testar a igualdade de enumerações nesse caso, não apenas em testes de unidade.
mbpro
3
Embora tecnicamente essa resposta não responda à pergunta, desconfio que muitas pessoas que chegam aqui por meio de pesquisa percebam que estavam fazendo a pergunta errada para começar. Obrigado!
Nikolay Suvandzhiev 31/07
15

Parece que nenhum compilador gerou operador de igualdade para enumerações nem estruturas.

"Se você criar sua própria classe ou estrutura para representar um modelo de dados complexo, por exemplo, o significado de" igual a "para essa classe ou estrutura não é algo que Swift possa adivinhar para você." [1]

Para implementar a comparação de igualdade, escreveríamos 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 equivalência" em https://developer.apple.com/library/content/documentation/Swift/Conceptual/Swift_Programming_Language/AdvancedOperators.html#//apple_ref/doc/uid/TP40014097-CH27-XID_43

paiv
fonte
14

Aqui está outra opção. É principalmente o mesmo que os outros, exceto que evita as instruções de chave aninhadas usando a if casesintaxe. Eu acho que isso o torna um pouco mais legível (/ suportável) e tem a vantagem de evitar o caso padrão 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
fonte
14
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
fonte
Isso também pode ser escrito com algo como case (.Simple(let v0), .Simple(let v1)) Também o operador pode estar staticdentro da enumeração. Veja minha resposta aqui.
LShi
11

Estou usando esta solução simples no código de teste da unidade:

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

Ele usa interpolação de strings para realizar a comparação. Eu não o recomendaria para código de produção, mas é conciso e faz o trabalho para testes de unidade.

Nikolai Ruhe
fonte
2
Eu concordo, para testes de unidade, essa é uma solução decente.
22430 Daniel
Os documentos da Apple sobre init (stringInterpolationSegment :) dizem: "Não chame este inicializador diretamente. Ele é usado pelo compilador ao interpretar interpolações de string.". Apenas use "\(lhs)" == "\(rhs)".
skagedal
Você também pode usar String(describing:...)ou o equivalente "\(...)". Mas isso não funciona se os valores associados diferirem :(
Martin
10

Outra opção seria comparar as representações de string dos casos:

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

Por exemplo:

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
fonte
3

Outra abordagem usando if casevírgulas, que funciona no 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
  }
}

Foi assim que escrevi no meu projeto. Mas não me lembro de onde tirei a ideia. (Pesquisei no Google agora, mas não vi esse uso.) Qualquer comentário seria apreciado.

LShi
fonte
2

t1 e t2 não são números, são instâncias de SimpleTokens com valores associados.

Você pode dizer

var t1 = SimpleToken.Number(123)

Você pode então dizer

t1 = SimpleToken.Name(Smith) 

sem um erro do compilador.

Para recuperar o valor de t1, use uma instrução switch:

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

a 'vantagem' quando comparada à resposta aceita é que não há um caso 'padrão' na instrução de opção 'principal'; portanto, se você estender sua enumeração com outros casos, o compilador o forçará a atualizar o restante do 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
user3441734
fonte
2

Expandindo a resposta da mbpro, veja como usei essa abordagem para verificar a igualdade de enumerações rápidas com valores associados a alguns casos extremos.

É claro que você pode fazer uma declaração de troca, mas às vezes é bom apenas verificar um valor em uma linha. Você pode fazer assim:

// 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 você deseja comparar duas condições na mesma cláusula if, use a vírgula em vez do &&operador:

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

No Swift 4.1, basta adicionar o Equatableprotocolo à sua enumeração e usar 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
iUrii
fonte
-1

Você pode comparar usando o 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")
}
Rachit
fonte
Lugar perfeito para uma troca com dois argumentos. Veja acima como isso leva apenas uma linha de código por caso. E seu código falha para dois números que não são iguais.
gnasher729