どうも。赤ワインが一番美味しい季節は秋だと思うのですが、この初春という時期もなかなか乙だと感じる tobi462 です。
今日は Swift 4.2 で追加される SE-0194 について軽くメモです。
ちなみに執筆時点で Xcode 9.4 beta 1 がリリースされていますが、こちらには Swift 4.2 は含まれていないので、現時点では自前でビルドする必要があります。
追記(2018/06/03):
現在は Swift 公式サイトのDownloads の「Swift 4.2 Development」から Xcode のツールチェーンをダウンロードすることができるので、そちらを利用するのが楽でしょう。
追記(ここまで)
今回、Swiftのビルドにあたっては以下の記事を参考にさせていただきました。 https://qiita.com/rintaro/items/2047a9b88d9249459d9aqiita.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 の機能も記事にしてみたいと思います。