Як перевірити рівність перерахунків Swift із пов'язаними значеннями


193

Я хочу перевірити рівність двох значень перерахунків 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для перерахунків із пов'язаними значеннями.
jedwidz

Відповіді:


245

Швидкий 4.1+

Як корисно вказав @jedwidz , від Swift 4.1 (завдяки SE-0185 , Swift також підтримує синтез Equatableі Hashableдля перерахунків із пов'язаними значеннями.

Отже, якщо ви перебуваєте на Swift 4.1 або новіших версіях, нижче наведені нижче автоматично синтезують необхідні методи, які XCTAssert(t1 == t2)працюють. Ключ - додати Equatableпротокол до перерахунку.

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

Перед Swift 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
Справа в цьому полягає в тому, що вам потрібно використовувати оператор за замовчуванням в комутаторі, тому, якщо ви додасте новий випадок "перерахунок", компілятор не переконайтеся, що ви додали пункт для порівняння нового випадку для рівності - ви будете просто потрібно пам’ятати і бути обережними, коли ви вносите зміни пізніше вниз!
Водоспад Майкл

20
Ви можете позбутися від проблеми @MichaelWaterfall, згаданої заміною defaultна case (.Name, _): return false; case(.Number, _): return false.
Kazmasaurus

25
Краще case (.Name(let a), .Name(let b)) : return a == bі т. Д.
Мартін Р

1
За допомогою пункту де, чи не кожен випадок продовжуватиметься тестувати, поки він не встановить за замовчуванням для кожного false? Це може бути тривіально, але така річ може скластися в певних системах.
Крістофер Свайсі

1
Щоб це працювало enumі ==функція, і функція повинна бути реалізована в глобальному масштабі (поза сферою роботи вашого контролера перегляду).
Андрій

77

Реалізація Equatable- це надмірний рівень IMHO. Уявіть, у вас складний і великий перелік з багатьма випадками і безліччю різних параметрів. Ці параметри також мають бути Equatableвпроваджені. Крім того, хто сказав, що ви порівнюєте випадки перерахунків за принципом «майже все»? Як щодо того, якщо ви тестуєте значення та заглушили лише один конкретний параметр 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це просто мовні конструкції, ви можете використовувати їх будь-де під час тестування рівності перерахунків у цьому випадку, а не лише в Unit Tests.
mbpro

3
Хоча технічно ця відповідь не відповідає на питання, я підозрюю, що насправді багато людей, які приїжджають сюди за допомогою пошуку, розуміють, що вони задавали неправильне питання для початку. Дякую!
Микола Суванджієв

15

Здається, не було створено компілятором оператора рівності для перерахунків, ні для структур.

"Якщо, наприклад, ви створюєте свій власний клас чи структуру для представлення складної моделі даних, тоді значення" рівного "для цього класу чи структури - це не те, що 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

Ось ще один варіант. Це в основному те саме, що й інші, за винятком того, що він уникає вкладених операторів перемикання за допомогою 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всередині перерахунку. Дивіться мою відповідь тут.
LShi

11

Я використовую це просте вирішення в тестовому коді одиниці:

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

Для порівняння використовується стрипова інтерполяція. Я б не рекомендував його для виробничого коду, але він стислий і виконує завдання для тестування одиниць.


2
Я згоден, для тестування одиниць це гідне рішення.
Даніель Вуд

Документи Apple в init (stringInterpolationSegment :) говорить: "Не називайте цей ініціалізатор безпосередньо. Він використовується компілятором при інтерпретації рядкових інтерполяцій." Просто використовуйте "\(lhs)" == "\(rhs)".
skagedal

Ви також можете використовувати String(describing:...)або еквівалент "\(...)". Але це не працює, якщо пов’язані значення відрізняються :(
Мартін

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 t1 {
    case let .Number(numValue):
        println("Number: \(numValue)")
    case let .Name(strValue):
        println("Name: \(strValue)")
}

2

"перевага" в порівнянні з прийнятою відповіддю полягає в тому, що в операторі "main" перемикача немає жодного випадку "за замовчуванням", тому якщо розширити перерахунок на інші випадки, компілятор змусить вас оновити решту коду.

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, ось як я використовував цей підхід, щоб перевірити рівність швидких перерахунків із пов'язаними значеннями з деякими крайніми випадками.

Звичайно, ви можете зробити оператор перемикання, але іноді приємно просто перевірити наявність одного значення в одному рядку. Ви можете це зробити так:

// 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протокол до вашого перерахуванню і використанню 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
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.