Swiftの列挙型 | Associated ValuesとRaw Values

列挙型(Enumerations)

列挙型はassociated valuesという機能があって、列挙型の値に付随した「関連する値」を持たせることができます。また、raw valuesとして、列挙型の値自体に特定の型を持った値を与えることもできます。

ここでは、先ずassociated valuesとは何か?から始め、具体例を用いてassociated valuesの使い方を説明します。次に、raw valuesに関して、具体例を用いたraw valuesの使い方、暗黙に割り当てられるraw values、raw valuesを使った初期化について説明していきます。

列挙型のAssociated Values

Associated valuesって何?

先ほど紹介した列挙型の例Positionですと、各caseに1つの値を定義しています。Swiftの列挙型はAssociated Valuesと呼ばれる機能を使って、各case値に「関連した値」を追加することができます。

「関連した値」というのが、まさにassociated valuesの直訳です。「Associated」というのは「関連する」とか「結びつける」等の訳が当てられています。脱線しますが、実用的そうなのは「associated professor」で、日本語で言う「准教授」でしょうか?

Associated valuesには、あらゆる型を取ることが出来ます。また、複数のassociated valuesを持つことも可能です。

Associated valuesの使用例 | 2段階認証ログインの雛形を列挙型associated valuesで実装

実際にassociated valuesを使用して、その簡単な使い方を見てみます。ここでは、以前論理演算子で使用した2段階認証の例を少し発展させてみます。

2段階認証として、先ずアカウント名とログインパスワードが必要です。これらは任意の長さの文字列が必要ですので、String型が最適です。2段階目の認証としては、ここでは符号なし整数UIntを使用します。具体的なイメージとしては、例えばGoogle Authenticatorを利用した2段階認証です。

// Associated valuesの使用例。2段階認証ログインの定義
enum User {
    case account(String, String)
    case authenticator(UInt)
}

//変数を初期化、別の値を代入
var loginInformation = User.account("guest", "HbfB{2R7&Fnz?vD4")
loginInformation     = .authenticator(418753)

順番に見ていきましょう。

Associated valuesを使った列挙型の値を定義

Associated valuesを定義する時の構文は、

// Associated valuesの定義構文
case value(type, ....)

のように、列挙型の値の直後にカッコをつけて、そのカッコ()の中に、associated valuesとして指定したい型を列挙します。複数指定したい場合はコンマ,で分けます。この時、型だけを指定すれば良くて、名前を付ける必要はありません。関数のパラメータを定義するのに少し似ていますね。

上記の列挙型を日本語にすると、

列挙型Userを定義。これはAccountという値を持ち、関連する(String, String)型が付随しています。また、Authenticatorという値も持ち、関連する値は型UIntを持っています。

となります。上記の定義は、associated valuesとしての型を指定しているだけで、具体的な値を定義しているわけではありません。具体的な値は、実際に使用する際に代入します。

Associated valuesを使って列挙型の値で変数(定数)を初期化、変数に代入

実際に定義した列挙型を使って変数を初期化してみます。

//変数を初期化、別の値を代入
var loginInformation = User.account("guest", "HbfB{2R7&Fnz?vD4")
loginInformation     = .authenticator(418753)

基本的な構文は同じですが、associated valuesを持つ場合は、列挙型の値の後ろにパラメータのような形でassociated valuesの値を指定します。

この列挙型の値をチェックするのにswitch文が使えます。

// switch文で値をチェック
switch loginInformation {
case .account(let name, let password):
    print("アカウント名: \(name), パスワード: \(password)")
case .authenticator(let number):
    print("2段階認証番号: \(number)")
}

Associated valuesも定数や変数として取り出すことが可能で、switch文の中で使用もできます。上記の例では、全て定数として取り出しています。

また、全てのassociated valuesが定数(もしくは変数)である場合は、以下のような記述も可能です。

// Associated valuesが全て定数の場合(全て変数の場合はvar)
switch loginInformation {
case let .account(name, password):
    print("アカウント名: \(name), パスワード: \(password)")
case let .authenticator(number):
    print("2段階認証番号: \(number)")
}

列挙型のRaw Values

Raw Valuesって何?

Associated valuesは、列挙型の値に対して「関連する値をあらゆる型を使って追加する」機能でした。一方で、列挙型の値自体に特定の型を持った値を与えることができます。これがRaw Valuesです。Raw valuesは、1つの列挙型に対して全て同じ型でなければいけません。C言語の列挙型は整数を持ちますが、これはまさに、整数型というraw valuesをもった列挙型、だと言えます。

具体的な例で見てみます。以下の列挙型は、日本語のあいさつ(文字列)を割り当てた例です。

//文字列のraw valuesを持つ列挙型
enum Greeting: String {
    case morning  = "おはよう"
    case dayTime  = "こんにちわ"
    case evening  = "こんばんわ"
}

Raw valuesを与える場合、列挙型の名前の後コロン:を付けて型typeを指定します。上記の例では、文字列型Stringを指定しました。

Raw valuesを取り出す際は、rawValueプロパティを使います。

// Raw valueは".rawValue"で取り出す
let sayMorning = Greeting.morning.rawValue
print(sayMorning)
// "おはよう"と表示

// 文字列なのでprint()に直接渡せる
print(Greeting.dayTime.rawValue)
// "こんにちわ"と表示

Raw valuesを割り当てる場合、それらには全て異なる値を定義しないとコンパイルエラーになります。例えば、先ほどのGreeting列挙型に、次のような値を作ると怒られます。

// Raw valueの値は「ユニーク」でなければならない
...
    case midNight = "こんばんわ"
}
// コンパイルエラー: error: raw value for enum case is not unique  case midNight = "こんばんわ"

暗黙に割り当てられるRaw Values

実は、列挙型のraw valuesには手動で値を割り当てる必要は(必ずしも)ありません。Swiftは型が指定されると、その値を推測して自動的に割り当ててくれます。最初に作ったPosition列挙型で試してみましょう。

// 暗黙の割り当てられるraw values、Intの例
enum Position: Int {
    case top, bottom, left, right
}
print("Top=\(Position.top.rawValue), Bottom=\(Position.bottom.rawValue), Left=\(Position.left.rawValue), Right=\(Position.right.rawValue)")
//"Top=0, Bottom=1, Left=2, Right=3"と表示

Position列挙型にIntを指定しました。出力すると分かりますが、順番に0から値が割り当てられています。

同様に、もしStringを指定すると、値として定義した文字列(ここでは、例えばTop)がそのままraw valueになります。実際に試してみると良く分かりますので、是非色々テストしてみて下さい。

Raw Valuesを使った初期化 | 初期化子はオプショナル列挙型を返す

Raw value型を指定して定義した列挙型には、自動的に初期化子(initializer)が与えられます。構造体のMemberwise initializerを思い出して頂くと分かりやすいと思います。この場合、列挙型の初期化子は、指定した型のrawValueをパラメータに取ります。

先ほどIntを割り当てたPosition列挙型で、初期化子の具体的な使用例を見てみます。

// Raw valueを持った列挙型の初期化子
let currentPosition = Position(rawValue: 6)
print("\(currentPosition)")

この例では、rawValueとして6を指定しました。今、PositionrawValueには0から3までしか割り当てられていません。したがって、上記の初期化子はnilを返します。

つまり、raw valueによる列挙型の初期化子は、実はオプショナル列挙型を返すことが分かります。これは、指定するrawValueが列挙型側で必ずしも定義されているとは限らない(上記の例の場合、6は定義されていない)、ということを想定しての仕様です。

したがって、列挙型本体にアクセスするためには、オプショナル型を外す操作(nilを回避するため)として、Optional Bindingなどを利用する必要があります。

// Optional Bindingを使った、オプショナル列挙型へのアクセス
let targetPosition = 6
if let somePosition = Position(rawValue: targetPosition) {
    switch somePosition {
        case .top:
            print("上です")
        default:
            print("下、左右のどれかです")
    }
} else {
    print("\(targetPosition)で指定される位置はありません")
}
//"6で指定される位置はありません"と表示

手抜きの例ですが、上記のように列挙型の全ての値を網羅しない場合は、defaultを使用します。このように、raw valueを使った初期化子はオプショナル列挙型を返しますので、少し注意する必要があります。

詳しくは初期化子のページで説明しますが、列挙型の初期化子は少し特殊なケースで「failable initializers」という分類になります。通常の初期化子では、必ず何かしらの値が割り当てられることが前提ですが、この初期化子を使うと「値がない状態(nil)」に成り得ます。初期化が失敗する可能性があるということから、「failable」という名称になっています。

まとめ

  • Associated valuesを使えば、列挙型の値に、関連するあらゆる型を持った値を追加できる
  • Raw valuesに基本的な型(Intなど)を指定できる。その際、特定の値でraw valuesを初期化する必要は必ずしもない(implicitly assigned raw values)
  • Raw values型を指定すると、自動的に初期化子(initializer)が出来る。ただし、この初期化子はオプショナル列挙型を返すので注意