LoginSignup
17
16

More than 3 years have passed since last update.

【Swift 5.1】enumで複数のRawValueを扱う

Last updated at Posted at 2020-04-29

はじめに

二年前に投稿した「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 とする列挙型を定義します。
そして、それらを隠蔽する「都道府県」列挙型を定義します。

PrefectureCode.swift
/// 都道府県コード
public enum PrefectureCode: Int, CaseIterable {
  case hokkaido = 1
  case aomori
  ...
  case okinawa
}
PrefectureName.swift
/// 都道府県名
public enum PrefectureName: String, CaseIterable {
  case 北海道
  case 青森県
  ...
  case 沖縄県
}
Prefecture.swift
/// 都道府県
@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

サンプルコード全体

最後にコピペでそのまま使えるサンプルコードを載せておきます。

CaseIterable.swift
extension CaseIterable where Self: Equatable {
    var offset: AllCases.Index {
        Self.allCases.firstIndex(of: self)!
    }
}
PrefectureCode.swift
/// 都道府県コード
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 }
}
PrefectureName.swift
/// 都道府県名
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 }
}
Prefecture.swift
/// 都道府県
@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
    }
}
17
16
1

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
17
16