Cara menguji kesetaraan enum Swift dengan nilai terkait


193

Saya ingin menguji kesetaraan dua nilai Swift enum. Sebagai contoh:

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

Namun, kompiler tidak akan mengkompilasi ekspresi persamaan:

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

Apakah saya harus mendefinisikan sendiri kelebihan operator kesetaraan saya? Saya berharap kompiler Swift akan menanganinya secara otomatis, seperti Scala dan Ocaml.


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

1
Dari Swift 4.1 karena SE-0185 , Swift juga mendukung sintesis Equatabledan Hashableuntuk enum dengan nilai terkait.
jedwidz

Jawaban:


245

Swift 4.1+

Seperti yang telah ditunjukkan oleh @jedwidz , dari Swift 4.1 (karena SE-0185 , Swift juga mendukung sintesa Equatabledan Hashableuntuk enum dengan nilai terkait.

Jadi jika Anda menggunakan Swift 4.1 atau yang lebih baru, yang berikut ini akan secara otomatis mensintesis metode yang diperlukan agar XCTAssert(t1 == t2)berfungsi. Kuncinya adalah menambahkan Equatableprotokol ke enum Anda.

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

Sebelum Swift 4.1

Seperti yang telah dicatat orang lain, Swift tidak mensintesis operator kesetaraan yang diperlukan secara otomatis. Biarkan saya mengusulkan implementasi pembersih (IMHO), meskipun:

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

Ini jauh dari ideal - ada banyak pengulangan - tetapi setidaknya Anda tidak perlu melakukan switch bersarang dengan if-statement di dalamnya.


39
Hal yang menyebalkan tentang hal ini adalah Anda perlu menggunakan pernyataan default di sakelar, jadi jika Anda menambahkan case enum baru, kompiler tidak memastikan Anda menambahkan klausa untuk membandingkan case baru untuk persamaan - Anda akan hanya perlu mengingat dan berhati-hati ketika Anda membuat perubahan di kemudian hari!
Air Terjun Michael

20
Anda bisa menyingkirkan masalah @MichaelWaterfall disebutkan dengan mengganti defaultdengan case (.Name, _): return false; case(.Number, _): return false.
Kazmasaurus

25
Lebih baik: case (.Name(let a), .Name(let b)) : return a == bdll.
Martin R

1
Dengan klausa di mana, bukankah setiap kasus akan terus diuji hingga mencapai standar untuk setiap kasus false? Ini mungkin sepele tetapi hal semacam itu dapat ditambahkan dalam sistem tertentu.
Christopher Swasey

1
Agar ini berfungsi baik enumdan ==fungsinya harus diimplementasikan pada cakupan global (di luar lingkup controller tampilan Anda).
Andrej

77

Menerapkan Equatableadalah IMHO berlebihan. Bayangkan Anda memiliki enum yang rumit dan besar dengan banyak case dan banyak parameter berbeda. Semua parameter ini harus sudah Equatablediimplementasikan juga. Lebih jauh, siapa bilang Anda membandingkan kasus enum dengan dasar semua atau tidak sama sekali? Bagaimana jika Anda menguji nilai dan hanya mematikan satu parameter enum tertentu? Saya akan sangat menyarankan pendekatan sederhana, seperti:

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

... atau dalam hal evaluasi parameter:

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

Temukan deskripsi terperinci di sini: https://mdcdeveloper.wordpress.com/2016/12/16/unit-testing-swift-enums/


Bisakah Anda memberikan contoh yang lebih lengkap ketika mencoba menggunakan ini bukan dalam dasar pengujian?
teradyl

Saya tidak yakin apa pertanyaannya di sini. if casedan guard casehanya konstruksi bahasa, Anda dapat menggunakannya di mana saja ketika menguji kesetaraan enum dalam kasus ini, bukan hanya dalam Tes Unit.
mbpro

3
Walaupun secara teknis jawaban ini tidak menjawab pertanyaan, saya menduga itu sebenarnya membuat banyak orang datang ke sini melalui pencarian, menyadari bahwa mereka mengajukan pertanyaan yang salah untuk memulai. Terima kasih!
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
    }
}

Ini juga dapat ditulis dengan sesuatu seperti case (.Simple(let v0), .Simple(let v1)) Juga operator dapat berada staticdi dalam enum. Lihat jawaban saya di sini.
LShi

15

Tampaknya tidak ada compiler yang dihasilkan operator kesetaraan untuk enum, maupun untuk struct.

"Jika Anda membuat kelas atau struktur Anda sendiri untuk mewakili model data yang kompleks, misalnya, maka makna" sama dengan "untuk kelas atau struktur itu bukanlah sesuatu yang Swift bisa tebak untuk Anda." [1]

Untuk menerapkan perbandingan kesetaraan, orang akan menulis sesuatu seperti:

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


14

Ini pilihan lain. Ini terutama sama dengan yang lain kecuali itu menghindari pernyataan switch bersarang dengan menggunakan if casesintaks. Saya pikir ini membuatnya sedikit lebih mudah dibaca (/ tertahankan) dan memiliki keuntungan menghindari kasus default sama sekali.

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

Saya menggunakan solusi sederhana ini dalam kode tes unit:

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

Ini menggunakan interpolasi string untuk melakukan perbandingan. Saya tidak akan merekomendasikannya untuk kode produksi, tetapi singkat dan melakukan pekerjaan untuk pengujian unit.


2
Saya setuju, untuk pengujian unit ini adalah solusi yang layak.
Daniel Wood

Dokumen Apple pada init (stringInterpolationSegment :) mengatakan: "Jangan panggil inisialisasi ini secara langsung. Ini digunakan oleh kompiler saat menginterpretasikan string interpolasi.". Gunakan saja "\(lhs)" == "\(rhs)".
skagedal

Anda juga bisa menggunakan String(describing:...)atau yang setara "\(...)". Tapi ini tidak berfungsi jika nilai yang terkait berbeda :(
Martin

10

Pilihan lain adalah membandingkan representasi string dari kasus:

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

Sebagai contoh:

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

Pendekatan lain menggunakan if casekoma, yang bekerja di 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
  }
}

Ini adalah bagaimana saya menulis di proyek saya. Tapi saya tidak ingat dari mana saya mendapatkan ide itu. (Saya googled sekarang tetapi tidak melihat penggunaan seperti itu.) Setiap komentar akan dihargai.


2

t1 dan t2 bukan angka, mereka adalah contoh dari SimpleTokens dengan nilai yang terkait.

Bisa dibilang begitu

var t1 = SimpleToken.Number(123)

Anda bisa mengatakannya

t1 = SimpleToken.Name(Smith) 

tanpa kesalahan kompiler.

Untuk mengambil nilai dari t1, gunakan pernyataan sakelar:

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

2

'keuntungan' ketika dibandingkan dengan jawaban yang diterima adalah, bahwa tidak ada kasus 'default' dalam pernyataan peralihan 'utama', jadi jika Anda memperpanjang enum Anda dengan kasus-kasus lain, kompiler akan memaksa Anda untuk memperbarui sisa kode.

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

Memperluas jawaban mbpro, inilah cara saya menggunakan pendekatan itu untuk memeriksa kesetaraan enum cepat dengan nilai terkait dengan beberapa kasus tepi.

Tentu saja Anda bisa melakukan pergantian pernyataan, tetapi terkadang menyenangkan hanya memeriksa satu nilai dalam satu baris. Anda dapat melakukannya seperti ini:

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

Jika Anda ingin membandingkan 2 kondisi dalam klausa if yang sama, Anda harus menggunakan koma alih-alih &&operator:

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

2

Dari Swift 4.1, cukup tambahkan Equatableprotokol ke enum Anda dan gunakan XCTAssertatau 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

Anda dapat membandingkan menggunakan sakelar

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

Tempat sempurna untuk beralih dengan dua argumen. Lihat di atas bagaimana ini hanya membutuhkan satu baris kode per kasus. Dan kode Anda gagal untuk dua angka yang tidak sama.
gnasher729
Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.