ペンギン村 Tech Blog

技術をこよなく愛するエンジニア集団が在住するペンギン村から、世界へ役立つ(かもしれない)技術情報を発信する技術系ブログです。某アラレちゃんが済む村とは一切関係ありません。んちゃ!

Swift 4.2 では enum の件数がとれるようになる(SE-0194)

どうも。赤ワインが一番美味しい季節は秋だと思うのですが、この初春という時期もなかなか乙だと感じる tobi462 です。

今日は Swift 4.2 で追加される SE-0194 について軽くメモです。

ちなみに執筆時点で Xcode 9.4 beta 1 がリリースされていますが、こちらには Swift 4.2 は含まれていないので、現時点では自前でビルドする必要があります。

今回、Swiftのビルドにあたっては以下の記事を参考にさせていただきました。 qiita.com

Tl;Dr

enum にCaseIterableを適合することで.allCasesでcase値の一覧が取得できます。

enum Fruits: CaseIterable {
    case apple, orange, banana
}

Fruits.allCases.count // => 3
Fruits.allCases // => [Fruits.apple, Fruits.orange, Fruits.banana]

これまで

これまでは enum の件数を取得したり、すべての値を列挙する方法はありませんでした。

そのため以下のように自前で実装する必要がありました。

enum Fruits {
    case apple, orange, banana
}

extension Fruits {
    static var count: Int {
        return all.count
    }
    static var all: [Fruits] {
        return [.apple, .orange, banana]
    }
}

Fruits.count // => 3
Fruits.all   // => [.apple, .orange, .banana]

これは実装が面倒であるという以上の問題があり、 enum に値が追加された場合に修正を忘れる可能性があります。

以下ではgrapeを追加していますが、allプロパティの修正を忘れています。しかし、Swiftコンパイラはこのミスを検出できません。

enum Fruits {
    case apple, orange, banana, grape
}

extension Fruits {
    static var count: Int {
        return all.count
    }
    static var all: [Fruits] {
        return [.apple, .orange, .banana] // .grape を追加し忘れている
    }
}

Swift 4.2 から

冒頭でも書きましたがCaseIterableに適合することで、自動的に.allCasesが使えるようになります。

enum Fruits: CaseIterable {
    case apple, orange, banana
}

Fruits.allCases.count // => 3

for fruit in Fruits.allCases {
    print("I like \(fruit).")
}
// => I like apple.
// => I like orange.
// => I like banana.

これはCodableと同じようにコンパイル時に自動的にコードを生成する仕組みのようです。

ちなみに私自身は利用したことが無いのですが、Sourceryでも同等のことが出来るようです。

ただし、Sourceryの場合はSwiftソースが生成されるのに対し、CaseIterableではコンパイラによって自動的に生成されるので、生成後のコード管理などについて意識する必要がありません。(これもCodableと同様ですね)

自前で実装する

さて、enumに関連値(Associated-Value)があった場合はどうなるのでしょうか?

結論から言うとコンパイルエラーとなります。

enum Maybe: CaseIterable {
    case nothing
    case some(Bool)
}
// => error: type 'Maybe' does not conform to protocol 'CaseIterable'

こういった自動生成されないケースでは、static var allCasesを自前で実装する必要があります。

enum Maybe: CaseIterable {
    static var allCases: [Maybe] {
        return [.nothing, .some(true), .some(false)] // 網羅的でなくてもOK
    }
    case nothing
    case some(Bool)
}

ちなみにenum in enumといったケースでは、(理論的には)コンパイラが網羅的に列挙できますが、関連値がある場合は自前で実装が必要になります。

enum Food: CaseIterable {
    static var allCases: [Food] {
        return [.drink] + Fruits.allCases.map(Food.fruit)
    }
    case drink
    case fruit(Fruits)
}

Food.allCases.forEach { print("\($0)") }
// => drink
// => fruit(Fruits.apple)
// => fruit(Fruits.orange)
// => fruit(Fruits.banana)

個人的にはこれは妥当な仕様だと感じます。

CaseIterable vs Sourcery

実行時に動的なアプローチが行えないSwiftについて、Sourceryなどは静的メタプログラミングという手段を使って冗長さを防ぐのは良いアプローチだと感じます。

しかし、静的メタプログラミングと言えば聞こえはいいですが、古来からあるコード自動生成のアプローチなので、テンプレート言語を覚えたりコード生成・管理というワンクッションの手間はかかります。

そうしたちょっとした手間を防ぐという意味で、言語自体にそうしたコード生成の仕組みが用意されるのは良い方針だと個人的には感じます。(そのブラックボックスさに微妙さを感じる人もいるかもしれませんが、言語仕様として明確化されていれば問題ないかなと思います)

おわり

Codableもそうですが、最近のSwiftではコンパイラによるコード自動生成というアプローチが増えてきたなという印象があります。

こうしたSwiftコンパイラによる自動生成で便利にする仕様は今後も増えていくのではないでしょうか?(と、Swift Evolutionも追ってないのに勝手に予測してみます)

P.S.
気が向いたら他の Swift 4.2 の機能も記事にしてみたいと思います。