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の持ってる単純性を損なっちゃうデメリットもありますね。