LoginSignup
21
22

More than 5 years have passed since last update.

列挙型の恩恵と Raw 値について考えてみる

Last updated at Posted at 2016-08-06

Swift の列挙型は、表現力がとても豊かです。それ故に、扱いに悩むところもたくさんあって、さまざまな機能を使い出すと混乱したり、列挙型ならではの良さを引き出しきれなかったりしてしまうように感じます。

特に自分が強力に感じるのが Raw 値 で、これは十分に注意して使わないと、せっかくの列挙型の良さが大きく損なわれかねないような気がします。そしてそんな Raw 値の扱いどころが難しい。

このごろに幾つか、列挙型に関する面白い考察を眺めていたら、自分も Swift の列挙型というものについて、自分なりに感じることを記してみたくなりました。

列挙型の恩恵

一意性の保証

列挙型の定義は、次のように、まず enum で名前を決めて、その中に case を使って選択肢を定義するのが基本になります。

列挙型の定義
enum ExpressionForm {

    case honorific
    case polite
    case casual
}

ここで特に大事なのが この候補の中から、どれか 1 つ ということが約束されるというところです。

たとえば上の例では ExpressionStyle という名前の列挙型が、候補 honorific, politeness, casual のうちのどれか 1 つを表現する、ということを記述できるようになります。

つまり、これ以外の値になることは有り得ないため、プログラマーはそれ以外の可能性を全く考える必要なく、プログラミングをすることができます。考える必要がないということは、それだけ、コードを書くのも読むのも楽になりますし、バグを生む可能性も大幅に下がります。

一意性の活用

このようにして定義した列挙型の値を生成したいときは、次のように 型名.列挙子名 で生成します。

列挙型のインスタンス化
let form = ExpressionForm.politeness

取り得る値は定義されているものに限られるため、存在しない slang をインスタンス化することはできません。

存在しない候補ではインスタンス化できない
// error: type 'ExpressionForm' has no member 'slang'
let form = ExpressionForm.slang

コンパイルエラーが最大のメリット

そしてこのとき、このように存在しない候補を指定すると、コンパイルエラーとして検出されます。

つまり、コンパイル時に間違いに気づくことができるため、エラーを実行時まで持ち越さない、つまり、予期しない値になっているがためにアプリ強制終了してしまうみたいな事態を回避できるところが、列挙型による一意性保証の 最大のメリット なんじゃないかなって思います。

switch 構文との相性抜群

そんな、列挙型の一意性保証を最大限に活用してくれるのが switch 構文です。

この構文は、ある値に対して、さまざまな条件で処理を篩い分けるのに使います。この構文は、すべての条件を網羅する必要があるのですけど、列挙型ではどんな値を取り得るかが定義で決まっているので、条件の網羅性を 網羅しているかどうかをコンパイラーで検出 できます。

列挙型が取り得る範囲は、コンパイラーが知っている
switch form {

case .honorific:
    print("敬語")

case .polite:
    print("丁寧語")

}    // error: Switch must be exhaustive, consider adding a default case

例えば、上のように定義した場合、今回の列挙型 ExpressionForm はもうひとつの候補 casual を採る可能性があるため、すべての条件を網羅できていません。そのため、ここでビルドエラーが発生します。

このようなときに Swift では それ以外のすべての場合 を表すために default: 条件を最後に添えるのが一般的ですけど、列挙型の場合はそうしなくても、次のようにすべての候補を網羅してあげればビルドが通るようになります。

列挙型なら、それ以外の場合を明記しなくても全てを網羅できる
switch style {

case .honorific:
    print("敬語")

case .polite:
    print("丁寧語")

case .casual:
    print("通常語")
}

この性質は特に重要で、その後にもし新しい候補を追加したとき、新しく追加した候補が条件に埋もれずに、ビルドエラーになってくれるのは嬉しいポイントです。

switch で default は避けたい

このメリットを最大限に活かすためにも、列挙型の値を switch で篩い分けるときには、条件で default を使うことは、それが 完璧に適切な条件を表現しているのでない限り は使わないようにすることが、とても大事に感じます。

たとえばこの列挙型に、次のように default を使った実装でプロパティ isFormal を持たせたとします。

defaultを使ってisFormalを実装した場合
extension ExpressionForm {

    var isFormal: Bool {

        switch self {

        case .casual:
            return false

        default:
            return true
        }
    }
}

このようなとき、現状であれば、期待通りに動作してくれます。

let formA = ExpressionForm.honorific
let formB = ExpressionForm.polite
let formC = ExpressionForm.casual

print(formA.isFormal)  // true
print(formB.isFormal)  // true
print(formC.isFormal)  // false

ところが、もしある日、列挙型 ExpressionFromslanghumble を追加したくなったとします。このとき、列挙型の case にこれらを追加すれば完成、でしょうか。

列挙型の候補を追記
enum ExpressionForm {

    case honorific
    case polite
    case casual
    case slang
    case humble
}

このように書き換えて、ビルドをすると、エラーは発生せずに通ります。でもさっそく使ってみると、それからしばらくたったある日、実装してあったプロパティ isFormal が、期待したのとは違う動きをしていることに気がつくかもしれません。

let formA = ExpressionForm.honorific
let formB = ExpressionForm.polite
let formC = ExpressionForm.casual
let formD = ExpressionForm.slang
let formE = ExpressionForm.humble

print(formA.isFormal)  // true
print(formB.isFormal)  // true
print(formC.isFormal)  // false
print(formD.isFormal)  // true
print(formE.isFormal)  // true

この例の場合、列挙子 slang までもが isFormaltrue と判定されています。これが意図した結果でないとしたら、修正しないといけませんが、先ほどのコードだと『それ以外の全ては true』と記載されてしまっているため、コード全体をよほど注意して眺め直さないと、なかなか間違いを見つけることは困難です。

また、今回の仕様変更では、もうひとつ humble が追加されていて、こちらは isFormaltrue と判定されます。つまり『意図した通りなので良い』と思うかもしれないですけど、新しく追加した候補がどちらに当たるかは完全にその追加候補次第、今回はたまたま結果が一致しただけなので、根本的な問題は slang のときと変わりません。

根本的なところとしては 候補を追加した時に、処理が適切かどうかを必ず判断する ことが最も大切です。

default を使わない実装

適切な条件判定を徹底するためには、実装で default を使わないことが、いちばん近道です。先ほどの isFormal プロパティーの例であれば、実装を次のように書き換えることができます。

defaultを使わない実装
var isFormal: Bool {

    switch self {

    case .honorific:
        return true

    case .polite:
        return true

    case .humble:
        return true

    case .casual:
        return false

    case .slang:
        return false
    }
}

この実装では default を使わず、すべての場合を列記しています。こうすることで、今後にもし新しい候補が追加されても、それらの候補を想定できていないことをコンパイラーが検出できて、足りない条件を補うことをプログラマーに強制できます。

これはビルドエラーとして現れてくるため、コードのどこで想定していない箇所があるか一目瞭然なのが嬉しいですし、何より他の条件に埋もれないため、予期しない動作を絶対に起こさないで済むのが最大の利点です。

処理をまとめたい時

ただ、上のような書き方だと return true とか return false が何個も出てきて、を冗長に感じる人もいるかもしれません。そのような時は、次のように書き換えることも可能です。

defaultを使わない実装
var isFormal: Bool {

    switch self {

    case .honorific, .polite, .humble:
        return true

    case .casual, .slang:
        return false
}

いずれにしても、慣れないうちは『すべての列挙子を書かないといけないのは面倒だな』って感じると思うのですけど、列挙型が持つ列挙子の数が途方もなく多いことって基本的にないでしょうし、すべてを書くことで得られる恩恵は手間を遥かに上回るので、こんな風にしてすべての場合を列挙するのを、個人的には 強くお勧め したいです。

列挙型を if で裁くときの留意事項

上記と全く同じ理由なのですけれど、条件分岐を表現する構文の if もできるだけ避けた方が良いように思います。

ifで列挙型を判定するのは、ひとつのcaseとdefaultと同じ
if form == .casual {

    return false
}
else {

    return true
}

これって、次のように default を使って表現した switch 文と全く同じ重みになります。

switch form {

case .casual:
    return false

default:
    return true
}

つまり if 文を使っていると、今後もし列挙型に候補を追加したときに、条件が網羅されていないかもしれないことをコンパイラーで検出できなくなってしまうため、余程適切な理由がない限りは避けておくのが無難なように思います。

列挙型を if で裁くのが適切な場面

もし if で列挙型を裁くのが適切な場面があるとしたら、例えば次のような値付き列挙型があった場合でしょうか。

メモとして、文または単語を表現する値付き列挙型
enum Note {

    case sentence(String)
    case word(String)
}

このとき、この列挙型の値から簡単に word に添えた値を参照するプロパティー word を作るときには、次のように if 文を使って問題ないと考えます。理由としては、将来どんなに新しい候補が追加されても、ここで注目している word には絶対に成り得ないためです。

ifを使って実装して問題ないと思われる例
extension Note {

    var word: String? {

        if case let .word(value) = self {

            return value
        }
        else {

            return nil
        }
    }
}

ちなみにもちろん if だけでなく switch 文でも表現できます。上記のようにして、敢えて switch との差別化を図るか、それとも下記のように一般的な篩い分けと統一した書き方にするか、好みの分かれるところかもしれません。

switchとdefaultで実装する場合
extension Note {

    var word: String? {

        switch self {

        case let .word(value):
            return value

        default:
            return nil
        }
    }
}

列挙型に機能を実装できることについて

ところで、これは完全に価値観に依る捉え方になるのですけど、自分的には、列挙型にメソッドやプロパティーが実装できるようになっているのは、プロトコル指向を実現するためなんじゃないかなって思ってます。

プロトコル指向というのは、これも価値観に依ると思うのですけど、自分的には プロトコルによって型に性質(振る舞い)を付与する ものと思っていて、Swift では型に性質を付与するために、メソッドやプロパティーといったものを使って振る舞いを規定する、そんな感じの捉え方です。

そのため、列挙型の本質は列挙子である、そして型が振る舞いを持つ、そんな発想でこれ以降の話を展開していくつもりです。感覚的な話で捉えにくいところですけど、そんな風な視点で見るとこんな考えになったりするのか、みたいな感じで眺めてもらえれば幸いです。

Raw 値による表現

列挙型の基本的なところを話したところで、今回の本題である Raw 値 について、話を進めて行こうと思います。

難しそうな言葉に聞こえますけど、Raw 値というのは Objective-C では当たり前なものでした。Objective-C では、列挙子は最終的には絶対に 整数値 で表現することになっていて、むしろ列挙子名の方が便宜上のオマケみたいな位置づけだったように感じます。それが Swift では、何も値を持たないまま、列挙子としてそれを表現できるようになっていて、むしろ値の方がオマケです。

むしろ、値を計算するのが常識だった気がするプログラミングにおいて、値がないまま使える列挙型というのが、Swift を始めてしばらくの間、とても不思議に思えてなりませんでした。

列挙型で Raw 値を使う

Swift の列挙型は、何も明記しない場合は Raw 値というものを持ちません。つまり、列挙子は列挙子そのものであって、0 とか "honorific" とか、そういった具体的な値は持っていないことになります。

列挙子に Raw 値を持たせたいときには、列挙型の定義のときに、その列挙子がどの型として "も" 扱えるかどうかを : を使ってクラス継承みたいな書き方で指定した上で、具体的にどの値としても扱えるかどうかを明記することで実現できます。ちなみに具体的な値を省略すると 0 から始まる通し番号が指定されたのと同じになります。

Raw値を伴う列挙型
enum ExpressionForm : Int {

    case honorific = 0
    case polite = 1
    case casual = 2
    case slang = 3
    case humble = 4
}

Swift では Raw 値として、整数型の他にも、文字列型などの 一部のリテラルから変換可能な型 を指定できるようになっています。

列挙子の具体的な値はリテラルで記載して、それによって列挙子とリテラルとが関連づけられます。そして普段、列挙子を値として使うときには、Raw 値ではなく列挙子そのものが使用され、その Raw 値を取得したときに初めて、その Raw 値がリテラルから具体的に生成されます。

列挙子から Raw 値を取得する

列挙型を Raw 値に対応させると、自動的に rawValue プロパティが追加されて、それを参照することで、対応する Raw 値を取得できます。

Raw値を取得する
let form = ExpressionForm.polite
let raw = form.rawValue

この例では、列挙子が格納されている変数 form が持つ rawValue を参照することで、その列挙子で定義されている Raw 値が得られます。このとき変数 raw の型は、列挙型で定義した Raw 値の型になります。

Raw 値から列挙子を取得する

Raw 値に対応した列挙型には、Raw 値から列挙型の値を生成するための変換イニシャライザー init(rawValue:) が自動的に追加されます。

これを使うことで Raw 値を使って列挙子を自由に作れるようになるのですけど、性格上、Raw 値で表現できる範囲は、列挙型そのもので表現できる範囲以上になるため、変換できない可能性も考えられます。そのため、この変換イニシャライザーは 変換できなかった場合に nil を返す 失敗可能イニシャライザーになっています。

Raw値からの変換はnilになる場合もある
// オプショナルな値として取得されます。想定外の Raw 値から変換しようとすると nil になります。
let form: ExpressionForm? = ExpressionForm(rawValue: 2)

Raw 値を適切な列挙子に変換できるかどうかは重要な話題なので、通常は Optional Bindings (if letguard let) などをセットで使って、変換処理と成否判定を一緒にするのが一般的になると思います。

Raw値からの変換と成否判定を行う例
guard let form = ExpressionForm(rawValue: 2) else {

    fatalError("Invalid value")
}

print(form)

Raw 値を文字列で持たせたいとき

先ほども少し触れたように、Swift では Raw 値として文字列を使うこともできます。定義の仕方は整数のときと全く同じで、Raw 値の方を String で定義した上で、それぞれの列挙子に対応する値を文字列リテラルで明記します。

文字列型をRaw値として持つ列挙型の定義
enum ExpressionForm : String {

    case honorific = "honorific"
    case polite = "polite"
    case casual = "casual"
    case slang = "slang"
    case humble = "humble"
}

Raw 値が招く、予期しにくい事態

ちなみに Raw 値の型が String 型のとき、具体的な値を省略すると、列挙しと同じ記述の文字列リテラルが指定されたのと同じになります。つまり、上記の定義と次の定義は同じになります。

文字列型をRaw値として持つ列挙型の定義
enum ExpressionForm : String {

    case honorific
    case polite
    case casual
    case slang
    case humble
}

これは大きく手間が省けて便利なようにも思えますけど、もし列挙子を変更したとき Raw 値の値も変更されます。Raw 値をどのように活用するかで影響が分かれるところとは思いますけど、たとえば Raw 値の文字列を、たとえば UserDefaults で値を保存するときに使うキーとして使っていたりすると、何気ない列挙子名の変更が甚大な不具合を生みかねない ので、十分注意が必要になってきます。

そんな風に、不注意によるバグの発生を防ぐためにも、列挙型での Raw 値の使用は慎重に行う必要がありそうです。個人的には、余程の理由がない限り Raw 値は使わない方が賢明なようにも感じます。たとえば、Objective-C みたいな Raw 値でしか列挙子を表現できない環境とブリッジしなければならないみたいな、どうしても避けられない環境制約があるときに限って使う、みたいなくらいでちょうど良いのかもしれません。

Swift における Raw 値の扱い

ちなみに Swift における、列挙型に対する Raw 値の扱いは、列挙子をその型の値で 再表現できる という意味合いになっているように見えます。

そんな性質を表現しているのが RawRepresentable プロトコルで、Raw 値を持たせた列挙型は、自動的にこのプロトコルに準拠することになります。意味合い的には、この列挙型はあくまでも Raw 値の型で表現し直せるだけであって 列挙子そのものが Raw 値と同値ではない ところが、捉える上での大事なポイントみたいです。

それをよく表現していると思うのが、たとえばこんな場面です。

列挙子とRaw値とが明らかに異なると実感できる例
let value1: ExpressionForm = ExpressionForm.casual
let value2: String = ExpressionForm.casual.rawValue

この例では ExpressionForm が列挙型で、Raw 値として String 型を持つように定義してあるのですけど、このとき、すごく自然なことなのですけど、最初の素直に代入した value1ExpressionForm 型で、次の rawValue を代入した value2String 型になります。

型が違えば別の値、そんな感覚は Swift に慣れてくるほどに感じられる気がします。

Raw 値の扱いについての考察

こんな性格を持つ Raw 値について見てきて、実際にこれをどう扱っていったらいいのか、そのためには Raw 値のどういったところに注目して判断したら良いのか、そんなあたりを考えてみたいと思います。

以下では Raw 値として文字列型の値を持つ、次の列挙子を想定して話を進めてみることにします。

このセクションで注目する列挙型の定義
enum ExpressionForm : String {

    case honorific = "honorific"
    case polite = "polite"
    case casual = "casual"
    case slang = "slang"
    case humble = "humble"
}

Raw 値による直接操作は、網羅性の保証を完全に失う

まず、これが自分の中で 列挙型では Raw 値の使用を極力避けるべきではないか と感じる根本的なところなのですけど、Raw 値を扱うことによって、列挙型の最大のメリットではないかと思う「網羅性の保証」が完全に消失してしまうところです。

極端な例を挙げます。

列挙子を使った篩い分け

例えば、列挙子を使った篩い分けであれば、次のように書けます。

列挙子を直接判定した例
switch form {

case .honorific:
    print("敬語")

case .polite:
    print("丁寧語")

case .humble:
    print("謙譲語")

case .casual:
    print("通常語")

case .slang:
    print("俗語")
}

この書き方は一般的な書き方ですが、これまでにも述べた通り、大きなメリットがあって、すべての場面が網羅されていることが保証されています。今回のように default を使っていなければなおさら、基本的には確実に すべての場面を考慮した処理が書かれている ことが、このコードから伝わってきます。

Raw 値を使った篩い分け

そして、これが極端な例になりますけれど、上と同じ判定を Raw 値を使って行うことも可能です。

Raw値を使って判定した例
switch form.rawValue {

case "honorific":
    print("敬語")

case "polite":
    print("丁寧語")

case "humble":
    print("謙譲語")

case "casual":
    print("通常語")

case "slang":
    print("俗語")

default:
    fatalError("予期しない場面を検出しました")
}

これは、先ほどの列挙紙を直接使った場合とまったく同じ動作をします。ただし、完全に文字列型で扱うため、どんな文字列を取り得るか、コンパイラーが判断する術がなくなります。そのため、条件判定の最後に、それ以外の場合を裁く default を記載する必要が出てきます。これ以外の場面は有り得ないにも関わらず。

必要ないコードが書かれているだけなら、可読性を低下するのを除けば、致命的な実行時エラーなどは発生しないので良いですが、次の条件判定が間違っていること、漏れなくつぶさに見つけられるでしょうか。

Raw値を使って判定した例(間違い版)
switch form.rawValue {

case "honorifics":
    print("敬語")

case "poIite":
    print("丁寧語")

case "casuel":
    print("通常語")

case "Slang":
    print("俗語")

default:
    fatalError("予期しない場面を検出しました")
}

間違いは5箇所です。

  • 1つ目の条件で "honorific" が複数形になっています
  • 2つ目の条件で "i" であるべきところが "I" になっています
  • 3つ目の条件で "a" が "e" になっています
  • 条件 "humble" が抜けています(そのため default 行が実行されます)
  • 4つ目の条件が、大文字で始まっています

これらを瞬時に見つけられる人ってたぶん稀で、間違っていそうとアタリをつけて意識を集中させて探して、やっと見つけることができると思います。これが、Raw 値を使わずに列挙子を直接扱うだけで、コンパイラーが全て確実に見つけてくれるのは素晴らしいことです。

この例はとてもシンプルでしたけど、同じように 列挙子を Raw 値に変換して使うというのは、これくらい安全性を損なっているのと同じこと なんじゃないかなって感じます。そう考えると、普段 Raw 値を直接扱う場面ってほとんどない、この安全性を損なってまで Raw 値を扱う利点というのはあるのだろうか、そんなところが今回の話の大きなテーマです。

Raw 値から列挙型への変換について

先ほどの例とは逆で Raw 値から列挙型を作り出す分には、変換できたかどうかを判定できる分、網羅性の欠落は、最終的には問題にはならなさそうな気がします。そういう観点でみれば Raw 値からの変換は、Raw 値に変換するときほど致命的なことは起こらなそうに思いますけど、こちらの場合で気になってくるのが 変換できないかもしれない Raw 値の混入 です。

正しく変換できる例と、変換ミスの例
// 正しく ExpressionForm.polite を取得できる例
let form = ExpressionForm(rawValue: "polite")

// 綴りミスにより nil が取得される例
let form = ExpressionForm(rawValue: "honorifics")

具体的に、例えばこのような変換ミスが発生する可能性が出てきます。

ただ、列挙型が備える Raw 値からの変換イニシャライザーは失敗可能イニシャライザーとして規定されているので『もしかすると変換できないかもしれない』ことは明らかです。そのため通常は、上の方でも話したように ifguard といった構文と合わせて、変換に失敗したかどうかを確実に判定することが Raw 値から列挙型の値を作る上での大事なポイントになってきそうに思います。

それさえできれば、あとは列挙型ならではのメリットを享受することが可能になります。

Raw 値は速やかに変換する

変換すればメリットを享受できるとはいっても、Raw 値を必要になるまで変換せずに、変数などでしばらく持って回るのは、得策ではないように思います。

Raw値を持って回る例
let rawValues: Array<String> = [ "polite", "honorific", "casuel" ]

列挙子と Raw 値とは『再表現』という関係であると考えれば、原則として Raw 値はいつでも列挙子に再表現できるはず です。あくまでも Raw 値として取り得る範囲は、性質上、列挙子以上に広くなってしまうため、変換できない場面を想定せざるをえないというのが実際問題としてあるだけであって。もしこの捉え方が正しいとすれば、Raw 値を手に入れたら速やかに、対等な列挙型の値に変換することで、それ以降は Swift による網羅性検査などのメリットが得られるようになります。

また、速やかに列挙型の値に変換することは、万一、用意した Raw 値が何かの手違いで不適切だったりしたときに、できるだけ早くそれを察知できるというメリットにもなります。

先ほどのRaw値を列挙型に変換する例
// もしここで Raw 値に間違いが混入していたりすると、このタイミングで変換失敗が発生し、間違いに気づける
let values: Array<ExpressionForm> = rawValues.map { ExpressionForm(rawValue: $0)! }

Raw 値を取得したらすぐに列挙型に変換すれば、手違いが発生した現場をすぐに検出できます。間違いの現場を捉えられれば、原因も掴みやすいですし、値が確定していない不安定な状況を先送りせずに済むので、不要なバグを水面下で引きずる可能性が相当少なくなるのが期待できます。

闇雲に Raw 値を定義しないのがよさそう

ところで、上で記してきたような『Raw 値をどう扱うか』よりも、そもそも Raw 値が必要かどうか を検討することが重要なように思います。

というのも、列挙子に変換できない Raw 値が混入するということよりも 混入できる可能性を与える こと自体が問題のように思えるためです。たとえば、列挙型に String 型の Raw 値を持たせるということは、列挙子を Raw 値に変換できるのも然ることながら、文字列から列挙型を作れるということになります。

その機能が必要なければ、列挙型を Raw 値に対応させる必要はないでしょうから、Raw 値に対応させる以上はどこかでそれを使うことになるはずです。そうなると、必ずどこかで文字列から列挙型の値への変換が発生し、そこで変換できない可能性が発生するのは、先ほど記した通りです。

ここで、もうひとつ重要なのが、その列挙型は Raw 値との相互変換機能をすべてのプログラマーに提供する ことになることです。このとき Raw 値を適切に処理するという繊細な部分が各プログラマーに委ねられ、使われた分だけ大なり小なり安全性が損なわれますし、そもそも各所で(保存されたり、加工されたり、もしくは使われないことも含めて)どう Raw 値が扱われるか把握できなくなることは、かなりのデメリット のように思います。

列挙型で Raw 値に頼らない方法はないか

そうは言っても、列挙型に Raw 値を指定するということは、それを使いたい場合がほとんどだと思います。

それでも、たとえば Raw 値の型が定義されているだけだと『Raw 値が必要』というくらいしか読み取れません。なぜ、それが必要なのか、そのために何が必要なのか、それを汲み取れる形で Raw 値に相当する振る舞いを列挙型に付与できないかを考えるのが、大事になってくるのかなって思います。

そんな観点で、こういった場合はこう表現したらどうだろう、そんなことを記してみます。

文字列として表示する列挙型を作りたいとき

列挙型の値をそのまま文字列で表示したい、または少し変えた文字列で表示したいときがあるかもしれません。そのようなとき、次のようにして Raw 値に文字列を持たせて使いたくなることがあるかもしれません。

文字列表現で使うテキストをRaw値で持たせているケース
enum ExpressionForm : String {

    case honorific = "尊敬語"
    case polite = "丁寧語"
    case humble = "謙譲語"
    case casual = "普通語"
    case slang = "俗語"
}

// 文字列として表示したいときにすぐ使える…けれど
let form = ExpressionForm.humble

print("表現のスタイルは \(form.rawValue) です。")

このようにすると、文字列として表示したいときに form.rawValue みたいにしてすぐに使うことができますけど、後で誰かがこのコードを見ても Raw 値が、表示用のテキストを表現しているか確信を持つだけの材料が揃っていない ため、保守時の悩みが増えてしまうかもしれません。

また、Raw 値で表示文字列を表現した場合、逆に表示文字列から列挙子を作る機能も自動的に実装されます。それって、意図して使えるようにしたものでしょうか、表示文字から列挙子を作る必要ってあるでしょうか、自分には判断する自信がありません。

値そのものを文字列として表現するときは CustomStringConvertible を使う

Swift では、ある型の値をそのままぴったり表現する文字列に変換するための仕組みとして CustomStringConvertible プロトコルが用意されています。これに準拠した型は、それが表現する値そのものを表現できる性質を持ちます。例えば Int 型の値を print 関数で表示しようとすると 10 みたいに表示されるのも、この仕組みで実現されています。

今回は 列挙型の値を文字列で表示したい というのが要望なので、まさに CustomStringConvertible が適切に意図を表現してくれます。

CustomStringConvertibleを用いた文字列表現
enum ExpressionForm {

    case honorific
    case polite
    case humble
    case casual
    case slang
}

extension ExpressionForm : CustomStringConvertible {

    var description: String {

        switch self {

        case .honorific:
            return "尊敬語"

        case .polite:
            return "丁寧語"

        case .humble:
            return "謙譲語"

        case .casual:
            return "普通語"

        case .slang:
            return "俗語"
        }
    }
}

このようにすると Raw 値を使わなくても、列挙型の値を文字列で表現できるようになります。しかもこのとき、値そのものが CustomStringConvertible で表現した文字列として扱われるようになるため、文字列(文字列補完構文)で変数を直接埋め込むときに description プロパティーを明記しなくて良いので、とても自然なコードが書けるのも嬉しいところです。

文字列補完構文にそのまま埋め込める
// インスタンスをそのまま、文字列かのように表示できる。
let form = ExpressionForm.humble

print("表現のスタイルは \(form) です。")

文字列補完構文では上記の通り自動で変換してくれますけど、文字列型の変数に入れたりしたいときには String 型のイニシャライザーにインスタンスを渡すだけで、先ほど CustomStringConvertible で規定した適切な表現で文字列型にしてくれます。

明示的にString型に変換する
let form = ExpressionForm.humble
let string = String(form)

なお、今回の CustomStringConvertible を用いた方法では、列挙型の値を それを的確に表現する文字列に表現し直す ことだけができるようになり、表示用文字列から列挙型の値を作る機能は備わらないため、完全に安全性を保ったまま、文字列で表現する機能が備わるところが魅力です。

UserDefaults 等に保存するためのシリアライズ値を Raw 値に持たせたい

これは Raw 値をそのまま使っても、ある程度は妥当な気がします。

ただ、Raw 値というだけでは何のために使っているかが分からない ため、将来誰かがコードを保守するときに、Raw 値がシリアライズで使われていることを見落としてしまうかもしれません。もし シリアライズで使っていることを見落としてしまえば、即、値の破壊につながり、ひいては深刻な動作不良の原因にも 繋がりそうな気がします。

こういうときには Raw 値に頼らず、明らかに用途がわかる振る舞いを実装するのが良い気がします。

Raw値ではなくシリアライズ機能を明記する
enum ExpressionForm {

    case honorific
    case polite
    case humble
    case casual
    case slang
}

extension ExpressionForm {

    var serializedValue: Int {

        switch self {

        case .honorific:
            return 0

        case .polite:
            return 1

        case .humble:
            return 2

        case .casual:
            return 3

        case .slang:
            return 4
        }
    }

    init?(deserialize value: Int) {

        switch value {

        case 0:
            self = .honorific

        case 1:
            self = .polite

        case 2:
            self = .humble

        case 3:
            self = .casual

        case 4:
            self = .slang

        default:
            return nil
        }
    }
}

Raw 値と比べて冗長な実装に思えたり、ともするとこちらの方がバグを生みそうと思えるかもしれないですけど、こちらの方が 数値を何のために扱うかが明確 になって良いような気がします。

独自の実装によって、コードに意図を込められる(シリアライズして保存)
let value = ExpressionForm.polite
let defaults = UserDefaults.standard

defaults.set(value.serializedValue, forKey: "Form")
独自の実装によって、コードに意図を込められる(読み込んでデシリアライズ)
let defaults = UserDefaults.standard
let value = ExpressionForm(deserialize: defaults.integer(forKey: "Form"))!

実装に関する考察

なお、実装の観点では、少なくとも serializedValue プロパティにおいては、上の方で述べたように default を使わず実装すれば、今後に選択肢が増えたとしても、コンパイラーによる網羅性チェックのおかげで、新たに追加した値を登録し忘れなくて済むのが嬉しいところです。ただ、イニシャライザーについてはどうしても default が必要になってしまうため、追加し忘れが発生する可能性 は生まれます。

対して rawValue による自動実装では、どちらとも自動で必要な機能が実装される ため、追加し忘れは防げます。そのため、頻繁に列挙子が追加・変更されるような列挙型では、もしかすると素直に Raw 値を使った方が良かったりするかもわかりません。ただ、その時でも今回みたいに serializedValueinit?(deserialize:) を実装して中で Raw 値にアクセスし、外から Raw 値にアクセスしない運用ルールを設けたりする方が、コードの可読性が上がりそうな気がします。

そしてそもそも、それだけ頻繁に列挙子の実装が変化するような列挙型であるなら、もしかすると『値を永続化して UserDefaults などに保存する』という目的自体が無茶 なことかもしれません。

専用機能として持たせるメリット

また、今回みたいに Raw 値ではなく、専用のプロパティーやイニシャライザーとして機能を持たせるメリットとしては、この列挙型を、ある時は Int の値で表現して、またあるときは String 型の値で表現して、みたいな要望があるときに、ちゃんと対応できるところです。

もし Raw 値で実装するとなると、どちらかでしか表現できなくなってしまいます。どちらがメインでどちらがオマケということもないでしょうから、両方とも対等に独自機能として実装できるということは、なかなか良い利点かなって思います。

IB の tag 番号から設定したい場合

ボタンなどに設定した Tag 番号から列挙子を生成したいとき、Tag 番号と Raw 値とを対応させて init(rawValue:) を使って列挙子を作る手もあるかもしれません。ただ、この場合も先ほどと同様で Raw 値から初期化できると書かれているだけでは、意味を汲み取るのが難しい という問題を感じます。

このようなときも、列挙型自体には Raw 値を持たせずに、専用のイニシャライザーを実装して、コードを見れば何をしたいのかわかるようにするのが良いように感じます。ちなみに、列挙子から Tab 番号を取得する必要ってなさそうで、使わないのにあっても混乱するばかりなので、それについては実装しないておくことにします。

Raw値ではなくシリアライズ機能を明記する
extension ExpressionForm {

    init?(tag value: Int) {

        switch value {

        case 0:
            self = .honorific

        case 1:
            self = .polite

        case 2:
            self = .humble

        case 3:
            self = .casual

        case 4:
            self = .slang

        default:
            return nil
        }
    }
}

このようにすることで、押されたボタンのタグを使って列挙型を生成することも簡単ですし、コードを読むだけでそうしたいことがすぐにわかります。

列挙型をTag番号から生成する
func buttonPushed(sender: NSButton!) {

    let form = ExpressionForm(tag: sender.tag)
}

それに実装をこうやって Raw 値と分けて作ることで、Tag 番号は Tag 番号として、何にも縛られない自由なルールで番号付けできるところも嬉しいんじゃないかなって思います。

対応するアイコン画像を取得したい場合

たとえば、列挙型の値に応じたアイコン画像が用意されていたとして、それを取得できるようにしたいとします。

列挙型自体に取得機能を持たせる例

このとき1つの方法としては、列挙型自体に、アイコンを取得するためのプロパティー icon を追加する方法が考えられます。それを具体的に示すとすれば、次のようになるでしょうか。

列挙型自身にアイコン画像を取得するプロパティーを実装する場合(その1)
extension ExpressionForm {

    var icon: NSImage {

        switch self {

        case .honorific:
            return NSImage(named: "honorific-icon.png")!

        case .polite:
            return NSImage(named: "polite-icon.png")!

        case .humble:
            return NSImage(named: "humble-icon.png")!

        case .casual:
            return NSImage(named: "casual-icon.png")!

        case .slang:
            return NSImage(named: "slang-icon.png")!
        }
    }
}

このように実装することで、値に応じて適切な画像を取得できるようになります。ここでも default 行を書かないことで、今後に選択肢が追加されたときに網羅性チェックが働いて、新しい選択肢に対応するアイコン画像の返却忘れが起こらないことを期待しています。

なお、このとき、次のように実装する方法もあるかもしれません。

列挙型自身にアイコン画像を取得するプロパティーを実装する場合(その2)
extension ExpressionForm {

    var icon: NSImage {

        return NSImage(named: "\(self)-icon.png")!
    }
}

個人的には、列挙子としての文字列とファイル名の一部としての文字列は意味が違うかなと思うところがあって、どちらかというと最初に紹介した方(その1)が好みに思えますけど、アイコンファイル名の名前のつけ方が 列挙子名-icon.png みたいに徹底されているとするなら、後者(その2)でも悪くはないのかもしれません。

画像に、列挙型からの変換機能を持たせる例

もうひとつの方法として、画像型に変換機能を持たせる方法が考えられます。Swift の場合、目的になる側が責任を持って任務を遂行する 慣習がある印象なので、今回みたいに『アイコン画像を取得すること』が目的であれば、画像を扱う NSImage 型に、次のような実装を追加するのが理想のように感じます。

画像側に変換機能を持たせる
extension NSImage {

    convenience init(icon form: ExpressionForm) {

        switch form {

        case .honorific:
            self.init(named: "Honorific-Icon.png")!

        case .polite:
            self.init(named: "Polite-Icon.png")!

        case .humble:
            self.init(named: "Humble-Icon.png")!

        case .casual:
            self.init(named: "Casual-Icon.png")!

        case .slang:
            self.init(named: "Slang-Icon.png")!
        }
    }
}

このようにすることで『列挙子の値からアイコン画像を生成する』ということを Swift で一般的な 変換イニシャライザーを通して行う ことができるようになり、より自然にアイコン画像への変換ができるようになると感じます。

アイコン画像を変換イニシャライザーによって生成する
let form = ExpressionForm.casual
let icon = NSImage(icon: form)

余談ですけど、この実装で大事なポイントとしては、NSImage に拡張した init(icon:)失敗可能ではない ように実装しているところでしょうか。列挙型 ExpressionForm が採る値は限られていて、それらすべてに画像リソースが割り当てられている前提です。この時、仕様上は絶対に画像に変換できるため、ここでは失敗しないイニシャライザーとして実装することで、変換できることを約束(コードで主張)しています。

Objective-C でも使う列挙型を定義するとき

Swift は Objective-C との相互運用が視野に入れられている言語なので、Swift で作った列挙型を Objective-C でも使いたい場合があります。そのような時、Objective-C では Raw 値を使って列挙子を表現しなければいけない ので、ここでは明示的に Raw 値を定義するのが妥当に思います。

Objective-C では、列挙子を整数型の値で表現するので、Raw 値の型として Int などの整数型を指定します。また、このとき、この列挙型が Objective-C で使われることを明示するために @objc も添えて定義します。

Objective-Cでも使う列挙型の定義
@objc(ESExpressionForm) enum ExpressionForm : Int {

    case honorific = 0
    case polite = 1
    case casual = 2
    case slang = 3
    case humble = 4
}

これであれば Objective-C でも使う列挙型であることが明確ですし、そうである以上はどうしても Raw 値を持たなくてはいけなくなるため、これなら致し方ないけれど妥当と呼べそうです。

列挙型を文字列としても扱いたい場合

列挙型を文字列としても扱いた場合、というのはどういう場面なんだろう。今回の記事を書きたくなった発端の話で、列挙型を文字列と対等に扱う方法の模索があったので、自分もそれに興味が湧きました。

表示としての文字列でしたら、先ほどの CustomStringConvertible を使う方法が適切なように思いますけど、それ以外で もし列挙子を文字列として扱いたい場合があるとしたらどのような表現方法になるのか 考えてみることにします。

Raw 値を使う方法

列挙子を文字列と同等に扱う必要があるのだとしたら、いちばん適切に思えるのが String 型の Raw 値を設定する方法 です。

文字列型をRaw値として持つ列挙型
enum ExpressionForm : String {

    case honorific = "Honorific"
    case polite = "Polite"
    case humble = "Humble"
    case casual = "Casual"
    case slang = "Slang"
}

このようにすれば、列挙子をいつでも文字列に変換できますし、文字列から列挙子を生成できるます。そして何より 列挙子と文字列値とが同等ということが、Raw 値という仕組みのおかげでコードに明記 されます。そして通常、列挙子は列挙子のまま使うのがメリットを享受する上で最適なのでそのまま使うことになりますし、もし文字列として扱わないといけなくなっても rawValue プロパティーでいつでもすぐに文字列として扱えます。

実装上の注意としては あくまでも内部表現としての文字列値を Raw 値として設定するのが最適 と思うところです。表示したい文字列という観点からは離れて考えて、内部的に使う 識別子 としての文字列を設定するようにすると、もし今後、この列挙型の仕様変更を行うときに既存の Raw 値に縛られずに済むと思います。

Swift では『文字列型の Raw 値を指定しないと列挙子名と同じ文字列が割り当てられる』性質がありますけど、安易にそれを使ってしまうと、今後にもし列挙子の名前を変更したくなったときに困難になる恐れがある ので、原則的には全ての値を明記するのが、個人的にはお勧めです。

識別子としての文字列表現

列挙子を文字列として扱う理由が、特別な用途として具体的に決まっているなら、これまでにも紹介してきた Raw 値ではなく専用機能として実装する のが似合うかもしれません。例えば、文字列値を、各列挙子を表現するための識別子として扱いたいのであれば、次のように定義すると Raw 値で表現するよりも分かりやすいように感じます。

Raw値ではなく専用機能で定義
extension ExpressionForm {

    var identifier: String {

        switch self {

        case .honorific:
            return "Honorific"

        case .polite:
            return "Polite"

        case .humble:
            return "Humble"

        case .casual:
            return "Casual"

        case .slang:
            return "Slang"
        }
    }

    init?(identifier: String) {

        switch identifier {

        case "Honorific":
            self = .honorific

        case "Polite":
            self = .polite

        case "Humble":
            self = .humble

        case "Casual":
            self = .casual

        case "Slang":
            self = .slang

        default:
            return nil
        }
    }
}

このようにすれば rawValue よりも具体的な identifier というプロパティー名で文字列を扱うことができるので、例えば次のようなコードがあったとき、とりあえず何を従っているのか、察することも簡単になります。

独自実装したidentifierを参照する例
if form.identifier == "Slang" {

}

まとめ

今回は、列挙子の特徴と Raw 値を極力使わないようにするにはどうしたらいいか、そんな観点で思うところを綴ってみました。

Swift の列挙型は、言語自体のサポートもあって、そのままで扱うほどに多くのメリットをもたらしてくれるように思います。それを満喫するためにも できる限りぎりぎりまで Raw 値に頼らない方法を模索してみると 良いことあるんじゃないかなって思ってます。

他にもいろいろ楽しみ方はあると思うし、きっと Raw 値ならではの美しさみたいなものもあったりすると思うので、みんなぜひぜひ、これこそ自分の思う列挙型の美しさだ!みたいなお話を Qiita とかで聞かせてもらえると嬉しいです。

21
22
0

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
21
22