如何用关联值测试Swift枚举的相等性


192

我想测试两个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 2014年

1
SE-0185以来,从Swift 4.1开始,Swift还支持合成Equatable以及Hashable具有关联值的枚举。
jedwidz

Answers:


244

迅捷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
令人毛骨悚然的是,您需要在开关中使用默认语句,因此,如果添加新的枚举大小写,则编译器不会确保您添加该子句来比较该新大小写是否相等-您将只需记住并在以后进行更改时要小心!
Michael瀑布

20
你可以摆脱@MichaelWaterfall提到通过更换问题defaultcase (.Name, _): return false; case(.Number, _): return false
2015年

25
好:case (.Name(let a), .Name(let b)) : return a == b
马丁- [R

1
使用where子句,直到每个案例都达到默认值,才会继续测试每个案例false吗?这可能是微不足道的,但是在某些系统中这种事情可能会加起来。
Christopher Swasey

1
为了使其正常工作enum==必须在全局范围(视图控制器范围之外)上实现功能。
安德烈(Andrej)

75

实施Equatable是一个过大的恕我直言。想象一下,您有许多情况和许多不同参数的复杂枚举。这些参数也都必须Equatable实现。此外,谁说您在全有或全无的基础上比较枚举案例?如果您正在测试值并且仅存入一个特定的枚举参数,该怎么办?我强烈建议使用简单的方法,例如:

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它们只是语言构造,在这种情况下,您可以在测试枚举相等的任何地方使用它们,而不仅仅是在单元测试中。
mbpro

3
尽管从技术上讲这个答案不能回答问题,但我怀疑它实际上使许多人通过搜索到达此处,意识到他们从一个错误的问题开始。谢谢!
Nikolay Suvandzhiev '18年

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语法避免嵌套的switch语句外,它与其他方法基本相同。我认为这使其更具可读性(/可以忍受),并且具有完全避免默认情况的优点。

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在枚举内部。在这里查看我的答案。
石狮'16

11

我在单元测试代码中使用了这个简单的解决方法:

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

它使用字符串插值来执行比较。我不建议将它用于生产代码,但是它简洁明了,可以完成单元测试。


2
我同意,对于单元测试,这是一个不错的解决方案。
丹尼尔·伍德

Apple在init(stringInterpolationSegment :)上的文档说:“不要直接调用此初始化程序。编译器在解释字符串插值时将使用它。” 只需使用"\(lhs)" == "\(rhs)"
skagedal '17

您也可以使用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
  }
}

这就是我在项目中写的方式。但是我不记得我的想法了。(我刚刚在Google上进行了搜索,但没有看到这种用法。)任何评论将不胜感激。


2

t1和t2不是数字,它们是具有关联值的SimpleToken的实例。

你可以说

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 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的答案,这就是我使用该方法检查快速枚举与关联值和某些边缘情况是否相等的方式。

当然,您可以执行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
}

如果要在同一if子句中比较2个条件,则需要使用逗号而不是 &&运算符:

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

2

从Swift 4.1开始,只需将Equatable协议添加到您的枚举中并使用XCTAssertXCTAssertEqual

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
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.