はじめに
二年前に投稿した「Enum で複数の RawValue を扱う」を最近の Swift の機能で刷新してみます。
今回は以前の記事で使用した unsafeBitCast
は使用しません。
代わりに CaseIterable
と Key Path Member Lookup を使用することで
シンプルで安全な実装にしています。
モチベーション( 再掲 )
RawValue をもつ列挙型には2つの特徴があります。
- RawValue の値から初期化
- RawValue の値の取得
たとえば次の列挙型は Int
を Raw Values にもつ列挙型です。
enum Enumeration: Int {
case a = 1
case b
case c
}
Enumeration(rawValue: 1)! // 1 で初期化
Enumeration.a.rawValue // 1 を取得
RawValue を持つ列挙型は暗黙的に RawRepresentable
プロトコルに適合しているので、init(rawValue:)
, rawValue
を提供します。
コンパイラによって次のようなコードが自動合成されています。
enum Enumeration: Int, RawRepresentable {
typealias RawValue = Int
case a = 1
case b
case c
init?(rawValue: Int)
var rawValue: Int { get }
}
このように、列挙型は RawRepresentable プロトコルに適合することで、1つの生の値を表す型( RawValue )に自動で対応することができます。
一方、RawValue を複数もつ列挙型を自動合成で作成することはできません。
具体例としては、「都道府県」を列挙型として定義して、「都道府県コード」と「都道府県名」の2つ値を RawValue として扱いたい場合です。
enum Prefecture {
case hokkaido
case aomori
...
case okinawa
// 都道府県コード(JIS X 0401都道府県コード)
init?(code: Int)
var code: Int { get }
// 都道府県名
init?(name: String)
var name: String { get }
}
この場合、RawRepresentable
に自動で適合させることはできないので、何らかの独自実装が必要です。
この問題をできるだけシンプルに解決してみたいと思います。
列挙型の準備
まずは、「都道府県コード」と「都道府県名」をそれぞれ RawValue とする列挙型を定義します。
そして、それらを隠蔽する「都道府県」列挙型を定義します。
/// 都道府県コード
public enum PrefectureCode: Int, CaseIterable {
case hokkaido = 1
case aomori
...
case okinawa
}
/// 都道府県名
public enum PrefectureName: String, CaseIterable {
case 北海道
case 青森県
...
case 沖縄県
}
/// 都道府県
@dynamicMemberLookup
public enum Prefecture: CaseIterable {
case hokkaido
case aomori
...
case okinawa
}
RawValue から初期化
次に、次のような CaseIterable
の拡張を定義します。
こうすることで、 allCases
で取得できるリストを経由して case の位置を特定することができます。
extension CaseIterable where Self: Equatable {
var offset: AllCases.Index {
Self.allCases.firstIndex(of: self)!
}
}
Prefecture.hokkaido.offset // 0
PrefectureCode.hokkaido.offset // 0
PrefectureName.北海道.offset // 0
case の位置が特定できれば、次のようにして列挙型の変換ができます。
extension Prefecture {
/// 都道府県コードで初期化
init?(code: Int) {
self.init(PrefectureCode(rawValue: code))
}
/// 都道府県名で初期化
init?(name: String) {
self.init(PrefectureName(rawValue: name))
}
private init?<T>(_ object: T?) where T: CaseIterable, T.AllCases.Index == AllCases.Index, T: Equatable {
switch object {
case let object? where object.offset < Self.allCases.endIndex:
self = Self.allCases[object.offset]
case _:
return nil
}
}
}
// 都道府県名で Prefecture を初期化
Prefecture(name: "北海道")
// 都道府県コードで Prefecture を初期化
Prefecture(code: 1)
これで「都道府県コード」と「都道府県名」の2つの異なる Raw Values から初期化できるようになりました。
RawValue の取得
「都道府県コード」と「都道府県名」の2つの列挙型に RawValue のプロパティを定義します。
extension PrefectureCode {
/// 都道府県コード
var code: Int { rawValue }
}
extension PrefectureName {
/// 都道府県名
var name: String { rawValue }
}
Key Path Member Lookup を実装すれば、「都道府県コード」と「都道府県名」の
それぞれの列挙型の公開プロパティに対して直接アクセスすることができるようになります。
extension Prefecture {
subscript<V>(dynamicMember keyPath: KeyPath<PrefectureCode, V>) -> V? {
self[keyPath]
}
subscript<V>(dynamicMember keyPath: KeyPath<PrefectureName, V>) -> V? {
self[keyPath]
}
private subscript<T, V>(_ keyPath: KeyPath<T, V>) -> V? where T: CaseIterable, T.AllCases.Index == AllCases.Index {
(offset < T.allCases.endIndex) ? T.allCases[offset][keyPath: keyPath] : nil
}
}
Prefecture.tokyo.name // "東京都"
Prefecture.tokyo.code // 13
これで複数の RawValue を1つの enum で扱うことができるようになりました。
Prefecture(code: 1)?.name // 北海道
Prefecture(name: "東京都")?.code // 13
サンプルコード全体
最後にコピペでそのまま使えるサンプルコードを載せておきます。
extension CaseIterable where Self: Equatable {
var offset: AllCases.Index {
Self.allCases.firstIndex(of: self)!
}
}
/// 都道府県コード
enum PrefectureCode: Int, CaseIterable {
case hokkaido = 1
case aomori
case iwate
case miyagi
case akita
case yamagata
case fukushima
case ibaraki
case tochigi
case gunma
case saitama
case chiba
case tokyo
case kanagawa
case niigata
case toyama
case ishikawa
case fukui
case yamanashi
case nagano
case gifu
case shizuoka
case aichi
case mie
case shiga
case kyoto
case osaka
case hyogo
case nara
case wakayama
case tottori
case shimane
case okayama
case hiroshima
case yamaguchi
case tokushima
case kagawa
case ehime
case kochi
case fukuoka
case saga
case nagasaki
case kumamoto
case oita
case miyazaki
case kagoshima
case okinawa
}
// MARK: -
extension PrefectureCode {
/// 都道府県コード
var code: Int { rawValue }
}
/// 都道府県名
enum PrefectureName: String, CaseIterable {
case 北海道
case 青森県
case 岩手県
case 宮城県
case 秋田県
case 山形県
case 福島県
case 茨城県
case 栃木県
case 群馬県
case 埼玉県
case 千葉県
case 東京都
case 神奈川県
case 新潟県
case 富山県
case 石川県
case 福井県
case 山梨県
case 長野県
case 岐阜県
case 静岡県
case 愛知県
case 三重県
case 滋賀県
case 京都府
case 大阪府
case 兵庫県
case 奈良県
case 和歌山県
case 鳥取県
case 島根県
case 岡山県
case 広島県
case 山口県
case 徳島県
case 香川県
case 愛媛県
case 高知県
case 福岡県
case 佐賀県
case 長崎県
case 熊本県
case 大分県
case 宮崎県
case 鹿児島県
case 沖縄県
}
// MARK: -
extension PrefectureName {
/// 都道府県名
var name: String { rawValue }
}
/// 都道府県
@dynamicMemberLookup
public enum Prefecture: CaseIterable {
case hokkaido
case aomori
case iwate
case miyagi
case akita
case yamagata
case fukushima
case ibaraki
case tochigi
case gunma
case saitama
case chiba
case tokyo
case kanagawa
case niigata
case toyama
case ishikawa
case fukui
case yamanashi
case nagano
case gifu
case shizuoka
case aichi
case mie
case shiga
case kyoto
case osaka
case hyogo
case nara
case wakayama
case tottori
case shimane
case okayama
case hiroshima
case yamaguchi
case tokushima
case kagawa
case ehime
case kochi
case fukuoka
case saga
case nagasaki
case kumamoto
case oita
case miyazaki
case kagoshima
case okinawa
}
// MARK: -
public extension Prefecture {
/// 都道府県コードで初期化
init?(code: Int) {
self.init(PrefectureCode(rawValue: code))
}
/// 都道府県名で初期化
init?(name: String) {
self.init(PrefectureName(rawValue: name))
}
subscript<V>(dynamicMember keyPath: KeyPath<PrefectureCode, V>) -> V? {
self[keyPath]
}
subscript<V>(dynamicMember keyPath: KeyPath<PrefectureName, V>) -> V? {
self[keyPath]
}
}
// MARK: -
private extension Prefecture {
init?<T>(_ object: T?) where T: CaseIterable, T.AllCases.Index == AllCases.Index, T: Equatable {
switch object {
case let object? where object.offset < Self.allCases.endIndex:
self = Self.allCases[object.offset]
case _:
return nil
}
}
subscript<T, V>(_ keyPath: KeyPath<T, V>) -> V? where T: CaseIterable, T.AllCases.Index == AllCases.Index {
(offset < T.allCases.endIndex) ? T.allCases[offset][keyPath: keyPath] : nil
}
}