Swift列挙型と関連する値の同等性をテストする方法


192

2つの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)
    ^~~~~~~~~~~~~~~~~~~

等価演算子の独自のオーバーロードを定義する必要がありますか?ScalaやOcamlと同じように、Swiftコンパイラーが自動的に処理することを望んでいました。


1
開かれたrdar:// 17408414(openradar.me/radar?id=6404186140835840)。
Jay Lieske 14

1
スウィフトのために4.1からSE-0185、スウィフトはまた、合成をサポートEquatableし、Hashable関連する値を持つ列挙型のために。
jedwidz

回答:


244

Swift 4.1以降

以下のよう@jedwidzが親切によるスウィフト4.1(から、指摘しているSE-0185、スウィフトはまた、合成をサポート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を置き換えることにより、前述の問題を取り除くことができ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実装する必要があります。さらに、列挙型のケースをオールオアナッシング基準で比較すると誰が言ったのですか。値をテストしていて、特定の列挙型パラメーターを1つだけスタブした場合はどうでしょうか。次のような簡単なアプローチを強くお勧めします。

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 2017

ここで何が問題なのかわかりません。if caseそして、guard caseだけではなく、ユニットテストでは、この場合には列挙型の平等をテストするとき、あなたはどこでもそれらを使用することができ、単に言語構造です。
mbpro 2018年

3
技術的にはこの回答は質問に答えるものではありませんが、実際に検索を介してここに到着する多くの人々を誤解しているのではないかと思います。ありがとう!
Nikolay Suvandzhiev

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ステートメントを回避することを除いて、主に他のものと同じです。これは少し読みやすく(/ 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

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
私は同意します。ユニットテストの場合、これは適切なソリューションです。
Daniel Wood

init(stringInterpolationSegment :)に関するAppleのドキュメントには、「この初期化子を直接呼び出さないでください。文字列補間を解釈するときにコンパイラーによって使用されます。」とあります。だけを使用してください"\(lhs)" == "\(rhs)"
skagedal 2017年

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 caseSwift 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 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列挙型が等しいかどうかを確認する方法を次に示します。

もちろん、switchステートメントを実行することもできますが、1行の1つの値を確認するだけでよい場合もあります。あなたはそうすることができます:

// 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から、列挙型にプロトコルを追加し、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")
}

2つの引数を持つスイッチに最適な場所。上の例では、ケースごとに1行のコードしか必要としません。そして、等しくない2つの数値に対してコードが失敗します。
gnasher729 2015
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.