วิธีการทดสอบความเท่าเทียมกันของ Swift enums ด้วยค่าที่เกี่ยวข้อง


193

ฉันต้องการทดสอบความเท่าเทียมกันของสองค่า Enum ของ Swift ตัวอย่างเช่น:

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

อย่างไรก็ตามคอมไพเลอร์จะไม่คอมไพล์นิพจน์ความเท่าเทียมกัน:

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

ฉันต้องกำหนดโอเวอร์โหลดของตัวดำเนินการความเท่าเทียมกันของตัวเองหรือไม่ ฉันหวังว่าคอมไพเลอร์ Swift จะจัดการกับมันโดยอัตโนมัติเหมือนกับที่ Scala และ Ocaml ทำ


1
เปิด rdar: // 17408414 ( openradar.me/radar?id=6404186140835840 )
Jay Lieske

1
จาก Swift 4.1 เนื่องจากSE-0185 Swift ยังสนับสนุนการสังเคราะห์EquatableและHashableสำหรับ enums ด้วยค่าที่เกี่ยวข้อง
jedwidz

คำตอบ:


245

Swift 4.1+

ตามที่@jedwidzได้ชี้ให้เห็นอย่างเป็นประโยชน์จาก Swift 4.1 (เนื่องจากSE-0185 , Swift ยังสนับสนุนการสังเคราะห์EquatableและHashableสำหรับ enums ด้วยค่าที่เกี่ยวข้อง

ดังนั้นถ้าคุณอยู่ในสวิฟท์ 4.1 หรือใหม่กว่าต่อไปนี้โดยอัตโนมัติจะสังเคราะห์วิธีการที่จำเป็นดังกล่าวที่XCTAssert(t1 == t2)งาน กุญแจสำคัญคือการเพิ่มEquatableโปรโตคอลใน enum ของคุณ

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

ก่อนสวิฟท์ 4.1

ดังที่คนอื่น ๆ สังเกต Swift ไม่สังเคราะห์ตัวดำเนินการความเสมอภาคที่จำเป็นโดยอัตโนมัติ ให้ฉันเสนอการใช้งานที่สะอาดกว่า (IMHO):

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

มันยังห่างไกลจากอุดมคติ - มีการทำซ้ำหลายครั้ง - แต่อย่างน้อยคุณไม่จำเป็นต้องทำสวิตช์ที่ซ้อนกันโดยมีคำสั่ง if อยู่ข้างใน


39
สิ่งที่แย่เกี่ยวกับเรื่องนี้ก็คือคุณต้องใช้คำสั่งเริ่มต้นในสวิตช์ดังนั้นหากคุณเพิ่มเคส enum ใหม่คอมไพเลอร์ไม่แน่ใจว่าคุณเพิ่มประโยคเพื่อเปรียบเทียบกรณีใหม่เพื่อความเท่าเทียมกัน - คุณจะ เพียงแค่ต้องจำและระมัดระวังเมื่อคุณทำการเปลี่ยนแปลงภายหลังบรรทัด!
น้ำตกไมเคิล

20
คุณสามารถกำจัดปัญหา @MichaelWaterfall กล่าวถึงโดยการแทนที่ด้วยdefault case (.Name, _): return false; case(.Number, _): return false
Kazmasaurus

25
ดีกว่า: case (.Name(let a), .Name(let b)) : return a == bฯลฯ
Martin R

1
ด้วยประโยคที่จะไม่แต่ละกรณียังคงได้รับการทดสอบจนฮิตเริ่มต้นสำหรับทุกfalse? อาจเป็นเรื่องเล็กน้อย แต่สิ่งต่างๆนั้นสามารถรวมอยู่ในระบบบางระบบได้
Christopher Swasey

1
สำหรับสิ่งนี้ในการทำงานทั้งสองenumและ==ฟังก์ชั่นจะต้องดำเนินการในขอบเขตทั่วโลก (นอกขอบเขตของตัวควบคุมมุมมองของคุณ)
Andrej

77

การดำเนินการEquatableเป็น IMHO มากเกินไป ลองนึกภาพคุณมีความซับซ้อนและ enum ขนาดใหญ่ที่มีหลายกรณีและพารามิเตอร์ที่แตกต่างกัน พารามิเตอร์เหล่านี้ทั้งหมดจะต้องมีEquatableการดำเนินการด้วย นอกจากนี้ใครบอกว่าคุณเปรียบเทียบกรณี enum บนพื้นฐานทั้งหมดหรือไม่มีอะไร ถ้าคุณกำลังทดสอบค่าและ stubbed พารามิเตอร์ enum เฉพาะเพียงพารามิเตอร์เดียว ฉันขอแนะนำวิธีง่าย ๆ เช่น:

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

... หรือในกรณีของการประเมินพารามิเตอร์:

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

ค้นหาคำอธิบายเพิ่มเติมได้ที่นี่: https://mdcdeveloper.wordpress.com/2016/12/16/unit-testing-swift-enums/


คุณช่วยยกตัวอย่างที่สมบูรณ์มากขึ้นเมื่อพยายามใช้สิ่งนี้ไม่ใช่แบบทดสอบได้หรือไม่?
teradyl

ฉันไม่แน่ใจว่าสิ่งที่เป็นคำถามที่นี่ if caseและguard caseเป็นเพียงโครงสร้างภาษาคุณสามารถใช้มันได้ทุกที่เมื่อทดสอบความเท่าเทียมของ enums ในกรณีนี้ไม่ใช่เฉพาะในการทดสอบหน่วย
mbpro

3
แม้ว่าในทางเทคนิคแล้วคำตอบนี้จะไม่ตอบคำถาม แต่ฉันคิดว่ามันทำให้คนจำนวนมากมาถึงที่นี่ผ่านการค้นหา แต่พวกเขากำลังถามคำถามที่ผิดเริ่มต้นด้วย ขอบคุณ!
Nikolay Suvandzhiev

15

ดูเหมือนว่าไม่มีคอมไพเลอร์สร้างตัวดำเนินการความเท่าเทียมกันสำหรับ enums หรือ structs

“ ถ้าคุณสร้างคลาสหรือโครงสร้างของคุณเองเพื่อแสดงโมเดลข้อมูลที่ซับซ้อนเช่นนั้นความหมายของ“ เท่ากับ” สำหรับคลาสหรือโครงสร้างนั้นไม่ใช่สิ่งที่ Swift สามารถเดาได้สำหรับคุณ” [1]

ในการใช้การเปรียบเทียบความเท่าเทียมกันเราจะเขียนสิ่งที่ชอบ:

@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] ดู "ผู้ดำเนินการที่เท่าเทียมกัน" ที่https://developer.apple.com/library/content/documentation/Swift/Conceptual/Swift_Programming_Language/AdvancedOperators.html#//apple_ref/doc/uid/TP40014097-CH27-XID_43


14

นี่คือตัวเลือกอื่น ส่วนใหญ่จะเหมือนกับคนอื่น ๆ ยกเว้นว่าจะหลีกเลี่ยงคำสั่ง switch แบบซ้อนโดยใช้if caseไวยากรณ์ ฉันคิดว่าสิ่งนี้ทำให้อ่านได้ง่ายขึ้น (/ สามารถรับได้) และมีข้อได้เปรียบในการหลีกเลี่ยงกรณีเริ่มต้นโดยสิ้นเชิง

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

สิ่งนี้สามารถเขียนด้วยอะไรก็ได้เช่นกันcase (.Simple(let v0), .Simple(let v1)) ตัวดำเนินการสามารถstaticอยู่ใน enum ได้ ดูคำตอบของฉันที่นี่
LShi

11

ฉันใช้วิธีแก้ปัญหาง่ายๆนี้ในรหัสทดสอบหน่วย:

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

มันใช้การแก้ไขสตริงเพื่อทำการเปรียบเทียบ ฉันจะไม่แนะนำสำหรับรหัสการผลิต แต่มันกระชับและทำงานสำหรับการทดสอบหน่วย


2
ฉันเห็นด้วยสำหรับการทดสอบหน่วยนี่เป็นทางออกที่ดี
Daniel Wood

Apple docs บน init (stringInterpolationSegment :) พูดว่า: "อย่าเรียก initializer นี้โดยตรงมันถูกใช้โดยคอมไพเลอร์เมื่อตีความการสอดแทรกสตริง" "\(lhs)" == "\(rhs)"ใช้เพียงแค่
skagedal

นอกจากนี้คุณยังสามารถใช้หรือเทียบเท่าString(describing:...) "\(...)"แต่สิ่งนี้จะไม่ทำงานหากค่าที่เกี่ยวข้องต่างกัน :(
Martin

10

อีกทางเลือกหนึ่งคือการเปรียบเทียบการแทนค่าสตริงของเคส:

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

ตัวอย่างเช่น:

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

วิธีการอื่นที่ใช้if caseกับเครื่องหมายจุลภาคซึ่งทำงานใน 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
  }
}

นี่คือวิธีที่ฉันเขียนในโครงการของฉัน แต่ฉันจำไม่ได้ว่าอยู่ที่ไหน (ฉันเพิ่งเริ่มใช้งาน แต่ไม่เห็นการใช้งานดังกล่าว) ความคิดเห็นใด ๆ จะได้รับการชื่นชม


2

t1 และ t2 ไม่ใช่ตัวเลข แต่เป็นอินสแตนซ์ของ SimpleTokens ที่มีค่าที่เชื่อมโยง

คุณสามารถพูดได้

var t1 = SimpleToken.Number(123)

จากนั้นคุณสามารถพูดได้

t1 = SimpleToken.Name(Smith) 

โดยไม่มีข้อผิดพลาดของคอมไพเลอร์

ในการดึงค่าจาก t1 ให้ใช้คำสั่ง switch:

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

2

'ประโยชน์' เมื่อเปรียบเทียบกับคำตอบที่ยอมรับคือไม่มีคำว่า 'ค่าเริ่มต้น' ในคำสั่งสลับ 'หลัก' ดังนั้นหากคุณขยาย enum ของคุณกับกรณีอื่น ๆ คอมไพเลอร์จะบังคับให้คุณอัปเดตโค้ดที่เหลือ

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

ขยายคำตอบของ mbpro ต่อไปนี้เป็นวิธีที่ฉันใช้วิธีนั้นเพื่อตรวจสอบความเท่าเทียมกันของ swift enums ด้วยค่าที่เกี่ยวข้องกับกรณีขอบบางอย่าง

แน่นอนคุณสามารถทำ switch statement ได้ แต่บางครั้งก็ดีที่จะตรวจสอบค่าหนึ่งค่าในหนึ่งบรรทัด คุณสามารถทำได้เช่น:

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

หากคุณต้องการเปรียบเทียบ 2 เงื่อนไขในเงื่อนไขเดียวกันหากคุณต้องใช้เครื่องหมายจุลภาคแทนตัว &&ดำเนินการ:

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

2

จาก Swift 4.1 เพียงเพิ่มEquatableโปรโตคอลใน enum ของคุณและใช้XCTAssertหรือ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

คุณสามารถเปรียบเทียบโดยใช้สวิตช์

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

เหมาะอย่างยิ่งสำหรับสวิตช์ที่มีอาร์กิวเมนต์สองตัว ดูข้างต้นวิธีนี้ใช้รหัสบรรทัดเดียวต่อกรณี และรหัสของคุณล้มเหลวสำหรับตัวเลขสองตัวที่ไม่เท่ากัน
gnasher729
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.