So testen Sie die Gleichheit von Swift-Aufzählungen mit zugehörigen Werten


192

Ich möchte die Gleichheit zweier Swift-Enum-Werte testen. Beispielsweise:

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

Der Compiler kompiliert den Gleichheitsausdruck jedoch nicht:

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

Muss ich meine eigene Überladung des Gleichheitsoperators definieren? Ich hatte gehofft, der Swift-Compiler würde das automatisch erledigen, ähnlich wie Scala und Ocaml.


1
Geöffnet rdar: // 17408414 ( openradar.me/radar?id=6404186140835840 ).
Jay Lieske

1
Ab Swift 4.1 aufgrund von SE-0185 unterstützt Swift auch die Synthese Equatableund HashableAufzählungen mit zugehörigen Werten.
jedwidz

Antworten:


244

Swift 4.1+

Wie @jedwidz hilfreich hervorgehoben hat, unterstützt Swift ab Swift 4.1 (aufgrund von SE-0185 auch die Synthese von EquatableundHashable für Aufzählungen mit zugehörigen Werten.

Wenn Sie also Swift 4.1 oder höher verwenden, werden im Folgenden die erforderlichen Methoden automatisch so synthetisiert, dass sie XCTAssert(t1 == t2)funktionieren. Der Schlüssel ist, das EquatableProtokoll zu Ihrer Aufzählung hinzuzufügen .

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

Vor Swift 4.1

Wie andere angemerkt haben, synthetisiert Swift die erforderlichen Gleichheitsoperatoren nicht automatisch. Lassen Sie mich jedoch eine sauberere Implementierung (IMHO) vorschlagen:

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

Es ist alles andere als ideal - es gibt viele Wiederholungen - aber zumindest müssen Sie keine verschachtelten Schalter mit darin enthaltenen if-Anweisungen ausführen.


39
Das Problem daran ist, dass Sie die Standardanweisung im Switch verwenden müssen. Wenn Sie also einen neuen Aufzählungsfall hinzufügen, stellt der Compiler nicht sicher, dass Sie die Klausel hinzufügen, um diesen neuen Fall auf Gleichheit zu vergleichen Denken Sie nur daran und seien Sie vorsichtig, wenn Sie später Änderungen vornehmen!
Michael Wasserfall

20
Sie könnten das Problem loszuwerden @MichaelWaterfall durch Ersetzen erwähnt defaultmit case (.Name, _): return false; case(.Number, _): return false.
Kazmasaurus

25
Besser: case (.Name(let a), .Name(let b)) : return a == busw.
Martin R

1
Wird mit der where-Klausel nicht jeder Fall weiter getestet, bis er für jeden die Standardeinstellung erreicht false? Es mag trivial sein, aber so etwas kann sich in bestimmten Systemen summieren.
Christopher Swasey

1
Dazu beide arbeiten enumund ==Funktion auf einem globalen Umfang umgesetzt werden müssen (außerhalb des Bereichs des View - Controller).
Andrej

75

Die Implementierung Equatableist meiner Meinung nach ein Overkill. Stellen Sie sich vor, Sie haben eine komplizierte und große Aufzählung mit vielen Fällen und vielen verschiedenen Parametern. Diese Parameter müssen ebenfalls Equatableimplementiert sein. Und wer hat gesagt, dass Sie Enum-Fälle auf Alles-oder-Nichts-Basis vergleichen? Wie wäre es, wenn Sie den Wert testen und nur einen bestimmten Enum-Parameter stubben? Ich würde dringend einen einfachen Ansatz empfehlen, wie:

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

... oder bei Parameterauswertung:

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

Eine ausführlichere Beschreibung finden Sie hier: https://mdcdeveloper.wordpress.com/2016/12/16/unit-testing-swift-enums/


Könnten Sie ein vollständigeres Beispiel geben, wenn Sie versuchen, dies nicht auf Testbasis zu verwenden?
Teradyl

Ich bin mir nicht sicher, was hier die Frage ist. if caseund guard casesind einfach Sprachkonstrukte. Sie können sie in diesem Fall überall verwenden, wenn Sie die Gleichheit von Aufzählungen testen, nicht nur in Unit-Tests.
mbpro

3
Obwohl diese Antwort die Frage technisch nicht beantwortet, vermute ich, dass viele Leute, die über die Suche hierher kommen, feststellen, dass sie zunächst die falsche Frage gestellt haben. Vielen Dank!
Nikolay Suvandzhiev

15

Es scheint keinen vom Compiler generierten Gleichheitsoperator für Aufzählungen oder Strukturen zu geben.

"Wenn Sie beispielsweise Ihre eigene Klasse oder Struktur erstellen, um ein komplexes Datenmodell darzustellen, kann Swift die Bedeutung von" gleich "für diese Klasse oder Struktur nicht für Sie erraten." [1]

Um einen Gleichheitsvergleich durchzuführen, würde man so etwas schreiben wie:

@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] Siehe "Äquivalenzoperatoren" unter https://developer.apple.com/library/content/documentation/Swift/Conceptual/Swift_Programming_Language/AdvancedOperators.html#//apple_ref/doc/uid/TP40014097-CH27-XID_43


14

Hier ist eine weitere Option. Es ist hauptsächlich dasselbe wie die anderen, außer dass die verschachtelten switch-Anweisungen mithilfe der if caseSyntax vermieden werden . Ich denke, dies macht es etwas lesbarer (/ erträglicher) und hat den Vorteil, dass der Standardfall insgesamt vermieden wird.

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

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

Dies kann auch mit so etwas wie geschrieben werden. case (.Simple(let v0), .Simple(let v1)) Auch der Operator kann sich staticinnerhalb der Aufzählung befinden. Siehe meine Antwort hier.
LShi

11

Ich verwende diese einfache Problemumgehung im Unit-Test-Code:

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

Der Vergleich wird mithilfe der Zeichenfolgeninterpolation durchgeführt. Ich würde es nicht für Produktionscode empfehlen, aber es ist prägnant und erledigt die Aufgabe für Unit-Tests.


2
Ich stimme zu, für Unit-Tests ist dies eine anständige Lösung.
Daniel Wood

In Apple-Dokumenten zu init (stringInterpolationSegment :) heißt es: "Rufen Sie diesen Initialisierer nicht direkt auf. Er wird vom Compiler bei der Interpretation von Zeichenfolgeninterpolationen verwendet." Einfach benutzen "\(lhs)" == "\(rhs)".
Skagedal

Sie können auch String(describing:...)oder das Äquivalent verwenden "\(...)". Dies funktioniert jedoch nicht, wenn sich die zugehörigen Werte unterscheiden :(
Martin

10

Eine andere Möglichkeit wäre, die Zeichenfolgendarstellungen der Fälle zu vergleichen:

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

Beispielsweise:

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

Ein anderer Ansatz if casemit Kommas, der in Swift 3 funktioniert:

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

So habe ich in meinem Projekt geschrieben. Aber ich kann mich nicht erinnern, woher ich die Idee hatte. (Ich habe gerade gegoogelt, aber keine solche Verwendung gesehen.) Jeder Kommentar wäre willkommen.


2

t1 und t2 sind keine Zahlen, sondern Instanzen von SimpleTokens mit zugeordneten Werten.

Sie können sagen

var t1 = SimpleToken.Number(123)

Das kannst du dann sagen

t1 = SimpleToken.Name(Smith) 

ohne Compilerfehler.

Verwenden Sie eine switch-Anweisung, um den Wert von t1 abzurufen:

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

2

Der 'Vorteil' im Vergleich zur akzeptierten Antwort besteht darin, dass die 'main'-switch-Anweisung keinen' Standard'-Fall enthält. Wenn Sie also Ihre Aufzählung mit anderen Fällen erweitern, zwingt Sie der Compiler, den Rest des Codes zu aktualisieren.

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

Um die Antwort von mbpro zu erweitern, habe ich diesen Ansatz verwendet, um die Gleichheit von schnellen Aufzählungen mit zugehörigen Werten mit einigen Randfällen zu überprüfen.

Natürlich können Sie eine switch-Anweisung ausführen, aber manchmal ist es hilfreich, nur in einer Zeile nach einem Wert zu suchen. Sie können es so machen:

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

Wenn Sie zwei Bedingungen in derselben if-Klausel vergleichen möchten, müssen Sie anstelle des &&Operators das Komma verwenden :

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

2

Fügen Sie ab Swift 4.1 einfach ein EquatableProtokoll zu Ihrer Aufzählung hinzu und verwenden Sie XCTAssertoder 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

Sie können mit Schalter vergleichen

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

Perfekter Ort für einen Wechsel mit zwei Argumenten. Sehen Sie oben, wie dies nur eine Codezeile pro Fall benötigt. Und Ihr Code schlägt für zwei Zahlen fehl, die nicht gleich sind.
Gnasher729
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.