Как проверить равенство перечислений 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 ).
Джей Лиеске
1
Начиная с Swift 4.1 благодаря SE-0185 , Swift также поддерживает синтез Equatableи Hashableдля перечислений со связанными значениями.
Джедвидз

Ответы:

245

Swift 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 внутри.

Radex
источник
39
Отвратительная вещь в том, что вам нужно использовать в переключателе оператор по умолчанию, поэтому, если вы добавите новый случай перечисления, компилятор не обязательно добавит предложение для сравнения этого нового случая на равенство. просто нужно помнить и быть осторожным, когда вы вносите изменения позже!
Майкл Водопад
20
Вы можете избавиться от упомянутой проблемы @MichaelWaterfall, заменив defaultна case (.Name, _): return false; case(.Number, _): return false.
Казмазавр
25
Лучше: case (.Name(let a), .Name(let b)) : return a == bи т.д.
Мартин Р
1
С предложением where, не будет ли каждый случай продолжаться проверяться, пока он не достигнет значения по умолчанию для каждого false? Это может быть тривиально, но такого рода вещи могут складываться в определенных системах.
Кристофер Свази
1
Чтобы это работало, enumи ==функция, и функция должны быть реализованы в глобальной области видимости (за пределами области действия вашего контроллера представления).
Андрей
77

Реализация Equatableявляется излишним ИМХО. Представьте, что у вас сложное и большое перечисление со многими падежами и множеством разных параметров. Эти параметры также должны быть 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/

mbpro
источник
Не могли бы вы привести более полный пример, пытаясь использовать это не в тестовой основе?
терадил
Я не уверен, что здесь за вопрос. if caseи guard caseэто просто языковые конструкции, вы можете использовать их где угодно при тестировании равенства перечислений в этом случае, а не только в модульных тестах.
mbpro
3
Хотя технически этот ответ не отвечает на вопрос, я подозреваю, что на самом деле многие люди, прибывающие сюда через поиск, понимают, что они задают неправильный вопрос для начала. Спасибо!
Николай Суванджиев
15

Кажется, нет никакого сгенерированного компилятором оператора равенства ни для перечислений, ни для структур.

«Если вы создаете свой собственный класс или структуру для представления, например, сложной модели данных, то значение« равно »для этого класса или структуры не является тем, что Свифт может угадать для вас». [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

paiv
источник
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
    }
}
neoneye
источник
Это также может быть записано чем-то вроде case (.Simple(let v0), .Simple(let v1)) Также оператор может быть staticвнутри перечисления. Смотрите мой ответ здесь.
LShi
14

Вот еще один вариант. Он в основном такой же, как и другие, за исключением того, что он избегает вложенных операторов switch с использованием if caseсинтаксиса. Я думаю, что это делает его немного более читабельным (/ bearable) и имеет преимущество, заключающееся в том, что он полностью избегает регистров по умолчанию.

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

Я использую этот простой обходной путь в коде модульного теста:

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:...)или эквивалент "\(...)". Но это не работает, если связанные значения отличаются :(
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
  }
}

Вот как я написал в своем проекте. Но я не могу вспомнить, откуда у меня появилась идея. (Я гуглил только сейчас, но не видел такого использования.) Любой комментарий будет оценен.

LShi
источник
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

«преимущество» по сравнению с принятым ответом состоит в том, что в операторе переключателя «main» нет регистра по умолчанию, поэтому, если вы добавите 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
user3441734
источник
2

Более подробно об ответе mbpro, вот как я использовал этот подход для проверки на равенство быстрых перечислений со связанными значениями с некоторыми крайними случаями.

Конечно, вы можете сделать оператор switch, но иногда приятно просто проверить одно значение в одной строке. Вы можете сделать это так:

// 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, вам нужно использовать запятую вместо &&оператора:

if someOtherCondition, case .yourEnumCase = yourEnumVariable {
  // success
}
teradyl
источник
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
Iurii
источник
-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")
}
Rachit
источник
Идеальное место для переключения с двумя аргументами. Посмотрите выше, как это занимает только одну строку кода для каждого случая. И ваш код не подходит для двух чисел, которые не равны.
gnasher729