Posted at

Swift の enum で複数の RawValue を扱う

More than 1 year has passed since last update.


モチベーション

RawValue を持つ列挙(raw value style enum)には2つの特徴があります。



  • RawValue の値から初期化


  • RawValue の値の取得

enum Enumeration: Int {

case a = 1
case b
case c
}

Enumeration(rawValue: 1)! // 1 で初期化
Enumeration.a.rawValue // 1 を取得

今回の記事では、上記の特徴を複数扱うことを考えます。

enum Enumeration {

// Int の RawValue
init?(rawValue1: Int)
var rawValue1: Int { get }

// String の RawValue
init?(rawValue2: String)
var rawValue2: String { get }
}

もう少し具体的な例を考えます。

たとえば、日本の都道府県を enum で定義して、

都道府県コードと都道府県名の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 }
}

この場合は raw value style enum ではないので、独自の実装が必要です。

正攻法だと、各 case と都道府県コード、都道府県名を switch などで対応させることになります。

あるいは Int または String の raw value style enum として定義しておき、

残りの値を独自で実装する方法です。

/* 都道府県コードの raw value style enum で定義する */

enum Prefecture: Int {

case hokkaido = 1
case aomori
...
case okinawa

/* 都道府県名は自前で実装する必要がある */
// 都道府県名
init?(name: String) {
// 各都道府県に分岐して初期化
switch name {
case "北海道":
self = .hokkaido
case "青森県":
self = .aomori
...
case "沖縄県":
self = .okayama
default:
return nil
}
}

var name: String {
// 各都道府県に分岐して都道府県名を返す
switch self {
case .hokkaido:
return "北海道"
case .aomori:
return "青森県"
...
case .okinawa:
return "沖縄県"
}
}
}

いずれにしても、分かりきっていることを switch していくのは面倒です。

もう少しだけ実装をシンプルにしてみます。


enum の unsafeBitCast

unsafeBitCast を使うと、同一メモリレイアウトのオブジェクト間でキャストができます。

raw value style enum に対して使った場合は、

case の位置を対応させて、異なる enum へキャストすることができます。

enum 方位: String {

case , , , 西
}

enum CompassPoint: String {
case north, south, east, west
}

// Swift の型システムを無視して enum 間でキャストできる
let orientation = 方位. // 東
unsafeBitCast(orientation, to: CompassPoint.self) // east

これを利用すれば、異なる enum の RawValue を使った初期化や、値の取得ができます。

// 文字列 "north" から 方位.北 を作る

let rawValue = "north"
unsafeBitCast(CompassPoint(rawValue: rawValue)!, to: 方位.self) // 方位.北

// CompassPoint.south から文字列 "南" を取得
let point = CompassPoint.south
unsafeBitCast(point, to: 方位.self).rawValue // "南"

1つ注意する点として、C で定義した enum と Swift で定義した enum でのキャストはできません。

メモリレイアウトが異なるためです。

たとえば、UIKit で定義されている UIAlertActionStyle は C の enum のため、

上述の CompassPointunsafeBitCast しようとすると実行時にエラーになります。

unsafeBitCast(UIApplicationState.active, to: CompassPoint.self) // 実行時エラー

unsafeBitCast(UIApplicationState.active, to: UIAlertActionStyle.self) // OK


都道府県の例

unsafeBitCast を使えば、都道府県コードを持つ enum と、

都道府県名を持つ enum との間で変換ができるので、

複数の RawValue を持っているように見せることができます。


PrefectureCode.swift

/// 都道府県コード

internal enum PrefectureCode: Int {
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
}


PrefectureName.swift

/// 都道府県名

internal enum PrefectureName: String {
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 沖縄県
}

最後に、変換処理を隠蔽した enum を作れば出来上がりです。


Prefecture.swift

/// 都道府県

public enum Prefecture {
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: -
extension Prefecture {

/// 都道府県コードで初期化
public init?(code: Int) {
self.init(PrefectureCode(rawValue: code))
}

/// 都道府県名で初期化
public init?(name: String) {
self.init(PrefectureName(rawValue: name))
}

/// 都道府県コード
public var code: Int {
return converted(PrefectureCode.self).rawValue
}

/// 都道府県名
public var name: String {
return converted(PrefectureName.self).rawValue
}

private init?<T>(_ t: T?) {
guard let t = t else { return nil }
self = unsafeBitCast(t, to: Prefecture.self)
}

private func converted<T>(_ t: T.Type) -> T {
return unsafeBitCast(self, to: t)
}
}


これで switch を使わずに、複数の RawValue を1つの enum で扱うことができるようになりました。

Prefecture(code: 1)!.name // 北海道

Prefecture(name: "東京都")!.code // 13