1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

【Swift】Enumのassociated Valueがあるときのパターンマッチで単純にif文使ったらエラー出た

Last updated at Posted at 2020-04-26

Enumのassociated Value、便利なことに気づいたのでよく使うようになったんですが、
この前ちょっと不便だなと思うことがあったので、ストーリー形式で書いてみます。

〜associated Value使う前〜

Twitterライクなアプリの開発に入ってたとして、下記みたいなStructがありました。

import Foundation

struct Followee {
    
    enum Status {
        case follow
        case blocked
        case mute
    }
    
    var status: Status = .follow // ホントは別のところでセットするけどわかりやすく
    
    var canAppearTimeline: Bool {
        if status == .follow {
            return true
        } else {
            return false
        }
    }
}

フォローしてるアカウントの投稿をタイムラインに表示するかどうかを、
canAppearTimelineで判定しています。
現状、フォローしてるアカウントのステータスは、フォロー中・被ブロック・ミュート中の三つで、フォロー中のときだけ表示します。
シンプルですね。

「タイムラインで表示する優先順位づけできひん?」

ここで仕様変更が入って、

「タイムラインで表示する優先順位づけできひん?」

という要望が入りました。

「なんかミュートまでは行かへんけど、この人タイムラインに出過ぎてうっとうしいわ〜ってときあるやろ?」
「嫌いまではいかへんけど、毎日食べると胃もたれする料理みたいな」(?)
「そういうとき、表示頻度を下げる機能あったら、嬉しいよな」

というわけで、仕様変更することになりました。

Statusを追加

とりあえずStatusをひとつ追加することにしました。

    enum Status {
        case follow
        case blocked
        case mute
        case showLess(priority: Int) // New!
    }

ロジック

こう考えました。

「うーん、めんどくさいな……どないしよ……」
「もうええわ。優先順位の初期値1入れて、タイムラインにあらわれるたびに1ずつ増やしてって、10になったら表示にしたろ!」
「とりあえず表示頻度減ったらええやろ」

機能としてはどうかと思いますが、ロジックは単純になりそうですね。

ところが

ところが、Statusを変更した段階で、Protocol 'Equatable' requires that 'Followee.Status' conform to 'Equatable'というエラーが出るようになりました。

    var canAppearTimeline: Bool {
        if status == .follow {
            return true
        } else {
            return false
        }
    }

「な、何もしてないのにif文が通らなくなった!」

associated Valueがあるとなぜifのパターンマッチができないのか

「Equatableに従ってないと怒られるんなら、従わせればええんか……?」
と思って、Statusをこうしてみました。

    enum Status: Equatable {
        case follow
        case blocked
        case mute
        case showLess(priority: Int)
    }

これをやると、showLess以外のケースであればコンパイル通るようになります。
しかし……

    var canAppearTimeline: Bool {
        if status == .showLess(_) { // Missing argument label 'priority:' in call
            // showLessのときのロジックを書く
        }
    }

こんな書き方はできません。

    var canAppearTimeline: Bool {
        if status == .showLess(priority: 1) {
            // showLessのときのロジックを書く
        }
    }

これなら通ります。

「えっ、これ1から10まで書かないとアカンの?!」

    var canAppearTimeline: Bool {
        if status == .showLess(priority: 1) || status == .showLess(priority: 2) ||
           status == .showLess(priority: 3) || status == .showLess(priority: 4) ||
           status == .showLess(priority: 5) || status == .showLess(priority: 6) ||
           status == .showLess(priority: 7) || status == .showLess(priority: 8) ||
           status == .showLess(priority: 9) || status == .showLess(priority: 10) {
            // showLessのときのロジックを書く
        }
    }

「こうやな!」

これなら通りますが、

「すまん!やっぱpriorityもっと細かくしてくれ!1〜100にしよう!」

と言われた瞬間に「アアアアアアアアアアアアアアア」となってしまいますね。

そう。Associated Valueは値を保存してくれるので、マジメにパターン分けすると、
Associated Valueのとりうる値分のパターンがあるんですね〜

対処法

対処法は二つあります。
まずswitch文を使う方法。

    var canAppearTimeline: Bool {
        switch status {
        case .follow:
            // 処理
        case .blocked:
            // 処理
        case .mute:
            // 処理
        case .showLess(priority: let priority):
            // 処理
        }
    }

これはEnum使うときのパターンマッチの王道ですね。

ただ状況によっては、もっとif文ぽく書きたいときもあると思います。
たとえばEnumが10ケースあるけど、ある1ケースに該当するときだけ処理を変えて、あとの9パターンは何もしない、みたいなときとか。
そんなときにSwitch文書くのはちょっと大袈裟ですよね。

そんなときのためにif caseという構文があります。

        if case .showLess(let priority) = status {
            if priority < maxPriority {
                increment()
                return false
            } else {
                reset()
                return true
            }
        }

こんな感じで書けます。
case .xxxのあとに=でつないで、Enumが来るっていうちょっと気持ち悪い構文ですが、
まあ元々Switch文を書かなきゃいけないところの省略形だと思えば多少は……いやそうでもないですか。

完成形

import Foundation

struct Followee {
    enum Status {
        case follow
        case blocked
        case mute
        case showLess(priority: Int)
        
        mutating func countUp() {
            if case .showLess(let priority) = self { self = .showLess(priority: priority + 1) }
        }
        
        mutating func resetPriority() {
            if case .showLess(_) = self { self = .showLess(priority: 1) }
        }
    }
    
    var status: Status = .follow
    let maxPriority = 10
    
    var canAppearTimeline: Bool {
        if case .follow = status {
            return true
        } else if case .showLess(let priority) = status {
            if priority < maxPriority {
                increment() // 直接countUp()呼んだら怒られたので迂回している
                return false
            } else {
                reset()
                return true
            }
        }
        return false
    }
}

func increment() {
    followee.status.countUp()
}

func reset() {
    followee.status.resetPriority()
}

var followee = Followee()
followee.status = .showLess(priority: 1)
(1...10).forEach { _ in print(followee.canAppearTimeline)}
print(followee.status)
出力
false
false
false
false
false
false
false
false
false
true
showLess(priority: 1)

余談

この記事書くために調べてたら知ったんですが、

switch productBarcode {
case .upc(let numberSystem, let manufacturer, let product, let check):
    print("UPC: \(numberSystem), \(manufacturer), \(product), \(check).")
case .qrCode(let productCode):
    print("QR code: \(productCode).")
}
// Prints "QR code: ABCDEFGHIJKLMNOP."

これを、

switch productBarcode {
case let .upc(numberSystem, manufacturer, product, check):
    print("UPC : \(numberSystem), \(manufacturer), \(product), \(check).")
case let .qrCode(productCode):
    print("QR code: \(productCode).")
}
// Prints "QR code: ABCDEFGHIJKLMNOP."

こんな書き方もできるんですね。へえ〜って感じです。

あとrawValueとassociated Valueは一緒に使えないとか、便利な分色々制約あるんだな〜と思いました。
Enumの持ってる単純性を損なっちゃうデメリットもありますね。

参考

Enumerations
SwiftのEnumをif文で比較できない(Associated Value)

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?