Jak przetestować równość wyliczeń Swift z powiązanymi wartościami

193

Chcę przetestować równość dwóch wartości szybkiego wyliczenia. Na przykład:

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

Jednak kompilator nie skompiluje wyrażenia równości:

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

Czy mam zdefiniować własne przeciążenie operatora równości? Miałem nadzieję, że kompilator Swift poradzi sobie z tym automatycznie, podobnie jak robią to Scala i Ocaml.

Jay Lieske
źródło
1
Otwarty rdar: // 17408414 ( openradar.me/radar?id=6404186140835840 ).
Jay Lieske
1
Począwszy od Swift 4.1 ze względu na SE-0185 , Swift obsługuje również syntezę Equatablei Hashablewyliczanie z powiązanymi wartościami.
jedwidz

Odpowiedzi:

245

Swift 4.1+

Jak pomocne @jedwidz , od Swift 4.1 (dzięki SE-0185 Swift obsługuje również syntezę Equatablei Hashablewyliczanie z powiązanymi wartościami.

Jeśli więc korzystasz z wersji Swift 4.1 lub nowszej, poniższe elementy automatycznie zsyntetyzują niezbędne metody, które XCTAssert(t1 == t2)działają. Kluczem jest dodanie Equatableprotokołu do wyliczenia.

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

Przed Swift 4.1

Jak zauważyli inni, Swift nie syntetyzuje automatycznie niezbędnych operatorów równości. Pozwólcie, że zaproponuję czystszą implementację (IMHO), jednak:

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

Jest daleki od ideału - jest wiele powtórzeń - ale przynajmniej nie musisz robić zagnieżdżonych przełączników z instrukcjami if w środku.

radex
źródło
39
Chodzi o to, że musisz użyć domyślnej instrukcji w przełączniku, więc jeśli dodasz nowy przypadek wyliczenia, kompilator nie upewni się, że dodasz klauzulę, aby porównać ten nowy przypadek dla równości - będziesz musisz tylko pamiętać i zachować ostrożność, gdy wprowadzasz zmiany później!
Michael Waterfall
20
Możesz pozbyć się wspomnianego problemu @MichaelWaterfall, zastępując defaultgo case (.Name, _): return false; case(.Number, _): return false.
Kazmasaurus,
25
Lepiej: case (.Name(let a), .Name(let b)) : return a == bitd.
Martin R
1
Czy w przypadku klauzuli where każda sprawa nie będzie nadal testowana, dopóki nie osiągnie wartości domyślnej dla każdej false? Może to być trywialne, ale tego rodzaju rzeczy mogą się sumować w niektórych systemach.
Christopher Swasey,
1
Aby to działało zarówno enumi ==funkcja musi być realizowana w ramach globalnej (poza zakresem kontrolera widoku).
Andrej
77

Wdrożenie Equatableto przesada IMHO. Wyobraź sobie, że masz skomplikowane i duże wyliczenie z wieloma przypadkami i wieloma różnymi parametrami. Te parametry również będą musiały zostać Equatablezaimplementowane. Ponadto, kto powiedział, że porównujesz przypadki wyliczeniowe na zasadzie „wszystko albo nic”? Co powiesz na to, jeśli testujesz wartość i wprowadziłeś tylko jeden konkretny parametr wyliczania? Zdecydowanie zaleciłbym proste podejście, takie jak:

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

... lub w przypadku oceny parametrów:

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

Znajdź bardziej szczegółowy opis tutaj: https://mdcdeveloper.wordpress.com/2016/12/16/unit-testing-swift-enums/

mbpro
źródło
Czy możesz podać bardziej kompletny przykład, gdy próbujesz użyć tego nie na podstawie testów?
teradyl
Nie jestem pewien, o co tu chodzi. if casei guard casesą po prostu konstrukcjami językowymi, możesz ich użyć w dowolnym miejscu podczas testowania równości wyliczeń w tym przypadku, nie tylko w testach jednostkowych.
mbpro
3
Chociaż technicznie ta odpowiedź nie odpowiada na pytanie, podejrzewam, że w rzeczywistości powoduje to, że wiele osób przybywa tu poprzez wyszukiwanie, zdając sobie sprawę, że zadawali złe pytanie na początek. Dzięki!
Nikolay Suvandzhiev
15

Wydaje się, że nie ma generowanego przez kompilator operatora równości dla wyliczeń ani struktur.

„Jeśli na przykład utworzysz własną klasę lub strukturę do reprezentowania złożonego modelu danych, znaczenie„ równa się ”dla tej klasy lub struktury nie jest czymś, co Swift może dla ciebie zgadnąć.” [1]

Aby wdrożyć porównanie równości, należy napisać coś takiego:

@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] Patrz „Operatory równoważności” na stronie https://developer.apple.com/library/content/documentation/Swift/Conceptual/Swift_Programming_Language/AdvancedOperators.html#//apple_ref/doc/uid/TP40014097-CH27-XID_43

paiv
źródło
14

Oto kolejna opcja. Jest to głównie to samo, co inne, z tym wyjątkiem, że unika zagnieżdżonych instrukcji przełączania za pomocą if caseskładni. Myślę, że dzięki temu jest nieco bardziej czytelny (/ znośny) i ma tę zaletę, że całkowicie pomija domyślny przypadek.

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
źródło
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
źródło
Można to również napisać za pomocą czegoś takiego: case (.Simple(let v0), .Simple(let v1)) Operator może również znajdować się staticw wyliczeniu. Zobacz moją odpowiedź tutaj.
LShi
11

Używam tego prostego obejścia w kodzie testu jednostkowego:

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

Używa interpolacji łańcuchów w celu wykonania porównania. Nie polecałbym go do kodu produkcyjnego, ale jest zwięzły i sprawdza się w testowaniu jednostkowym.

Nikolai Ruhe
źródło
2
Zgadzam się, w przypadku testów jednostkowych jest to przyzwoite rozwiązanie.
Daniel Wood
Dokumenty Apple dotyczące init (stringInterpolationSegment :) mówią: „Nie wywołuj bezpośrednio tego inicjatora. Jest on używany przez kompilator podczas interpretacji interpolacji łańcucha.”. Po prostu użyj "\(lhs)" == "\(rhs)".
skagedal
Możesz także użyć String(describing:...)lub ekwiwalentu "\(...)". Ale to nie działa, jeśli powiązane wartości różnią się :(
Martin
10

Inną opcją byłoby porównanie reprezentacji ciągów przypadków:

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

Na przykład:

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
źródło
3

Inne podejście if casez użyciem przecinków, które działa w 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
  }
}

Tak napisałem w moim projekcie. Ale nie pamiętam, skąd pomysł. (Właśnie przejrzałem Google, ale nie widziałem takiego użycia). Wszelkie komentarze będą mile widziane.

LShi
źródło
2

t1 it2 nie są liczbami, są instancjami SimpleTokens z powiązanymi wartościami.

Możesz powiedzieć

var t1 = SimpleToken.Number(123)

Możesz wtedy powiedzieć

t1 = SimpleToken.Name(Smith) 

bez błędu kompilatora.

Aby pobrać wartość z t1, użyj instrukcji switch:

switch t1 {
    case let .Number(numValue):
        println("Number: \(numValue)")
    case let .Name(strValue):
        println("Name: \(strValue)")
}
Karolina
źródło
2

„zaletą” w porównaniu z zaakceptowaną odpowiedzią jest to, że w instrukcji przełącznika „głównego” nie ma przypadku „domyślnego”, więc jeśli rozszerzysz wyliczenie o inne przypadki, kompilator wymusi aktualizację reszty kodu.

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
użytkownik3441734
źródło
2

Rozwijając odpowiedź mbpro, oto jak wykorzystałem to podejście do sprawdzenia równości szybkich wyliczeń z powiązanymi wartościami z niektórymi przypadkami krawędzi.

Oczywiście możesz wykonać instrukcję switch, ale czasem miło jest po prostu sprawdzić jedną wartość w jednym wierszu. Możesz to zrobić w następujący sposób:

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

Jeśli chcesz porównać 2 warunki w tej samej klauzuli if, musisz użyć przecinka zamiast &&operatora:

if someOtherCondition, case .yourEnumCase = yourEnumVariable {
  // success
}
teradyl
źródło
2

W Swift 4.1 po prostu dodaj Equatableprotokół do swojego wyliczenia i użyj XCTAssertlub 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
źródło
-1

Możesz porównać za pomocą przełącznika

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
źródło
Idealne miejsce na przełącznik z dwoma argumentami. Zobacz powyżej, jak zajmuje to tylko jeden wiersz kodu na przypadek. A twój kod zawiedzie dla dwóch liczb, które nie są równe.
gnasher729,