OptionSet で rawValue 以外のプロパティーを作ったらどうなるか

これは[ゆめみ Advent Calendar 2019](https://qiita.com/advent-calendar/2019/yumemi) 8 日目の記事です。本来は @guitar_char さんの担当ですが、最近担当案件がぷち修羅場とのことですので私が代わりに投稿します。

また、この記事自体は半年ほど下書きに眠っていたものです。

--- 

単刀直入に結論を言うと：**インスタンス作成当時はそれらのプロパティーは生きていますが、何か処理が入った途端彼らは消されます**。そのため、`enum` のように何か関連付けさせたいデータがある場合は `OptionSet` は使えません。

考えてみれば当然ですが、`OptionSet` はあくまでフラグフィールドであって、Swift は `OptionSet` に対してあくまでフラグフィールドとしてしか扱ってくれません。自分で無理やりプロパティーを入れるのはその瞬間は生かしてくれますが、何らかの処理が挟まれたらフラグフィールド以外は残るわけもありません。

なぜこの話題を言い出すかと言うと、例えば何らかの画面にボタンを表示する用件があるとします。ボタンの種類にはポジティブボタンとネガティブボタンの 2 種類、配置は両方あり、どっちか片方あり、そして両方なしと言う計 4 パターンがあるとします。

| パターン | ポジティブボタン | ネガティブボタン |
|---:|:---:|:---:|
| 1 | なし | なし |
| 2 | なし | あり |
| 3 | あり | なし |
| 4 | あり | あり |

これだけの要件でしたら、`OptionSet` で簡単に条件を切り分けられますね：まずボタン種類を作ります

```swift
struct ButtonOption: OptionSet {
    let rawValue: UInt8
    static let positive: ButtonOption = .init(rawValue: 1 << 0)
    static let negative: ButtonOption = .init(rawValue: 1 << 1)
    static let none: ButtonOption = []
    static let both: ButtonOption = [.positive, .negative]
}
```

次に条件に応じて表示ロジックを書きます：

```swift
class ViewController: UIViewController {
    
    var buttons: ButtonOption // ...
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        if buttons.contains(.positive) {
            // ポジティブボタンを表示
        }
        
        if buttons.contains(.negative) {
            // ネガティブボタンを表示
        }
        
    }
    
}
```

こうすれば、簡単な 2 つの `if` 文だけで、全ての 4 つのパターンを網羅できました。

ところが、ちょっと用件が増えました。ポジティブボタンとネガティブボタンのそれぞれのボタンテキストを外から定義しなくてはいけない、と言う要件です。どうしましょう？

例えば、とりあえず `OptionSet` をそのまま利用するのはどうでしょうか？こう言う定義の仕方はあるかもしれませんね：

```swift
struct ButtonOption: OptionSet {
    let rawValue: UInt8
    var title: String?
    // ...
}
```

おお、これでタイトルのプロパティーの定義が追加できた！と思う人もいるかもしれませんが、残念ながらこんな定義の仕方はダメです。なぜなら、こちらのコードを読んでみましょう：

```swift
var positive = ButtonOption.positive
positive.title = "OK"

var negative = ButtonOption.negative
negative.title = "NG"

let buttons: ButtonOption = [positive, negative]
print(buttons.title)
```

さて、こちらのコードは何を出力するでしょうか？

はい、お気づきましたよね？`OptionSet` の結合演算には配列の書式が使われていますが、その演算結果は配列ではなくあくまで `OptionSet` 単体のインスタンスです。そのため、配列の書式にあった複数のインスタンスの `title` プロパティーは、配列にはなりません。単体のままです。

最初に言いました通り、`OptionSet` は Swift から見てただのフラグフィールドです。そのため、ここの配列書式によって生成したのは、フラグフィールドが結合した新しいフラグフィールドに過ぎません。この結合演算の途中で Swift が見てるのはあくまで `rawValue` プロパティーだけであって、それ以外のものはそもそも見ていません。

例えばこの例で出てきた `positive` と `negative` は、それぞれ `0b0001`[^binary] と `0b0010`[^binary] の `rawValue` を持っており、結合演算によって `0b0011`[^binary] の結果が生成されたに過ぎません。もともと `positive` と `negative` が持ってた `title` というプロパティーはそもそもこの結合演算に無視されているのです。よって `print` しても `nil` しか出てきません。

[^binary]: `0bxxxx` の書式は二進数を表すリテラルです。例えば `0b0000` は十進数で `0`、`0b0001` は十進数で `1`、`0b0010` は十進数で `2`、`0b0011` は十進数で `3`, `0b0100` は十進数で `4`…といった具合です。

ではこの場合どうすればいいかというと、`enum` の方が適しています。`enum` には AssociatedValue というものがあるので、ボタンの種類とテキストをくっつけさせることが可能です：

```swift
enum ButtonOption {
    case positive(title: String?)
    case negative(title: String?)
}
```

もちろんこの方法には一つ欠点があります、それは管理する時は配列で管理することになるので、フラグフィールドみたいにどっちかには一つだけという制限を入れるのが難しいということです。しかし実はちょっと工夫をすれば、それも不可能ではありません。

同様なものが複数入らないようにする仕組みとして、Swift は `Set` というものが提供されています。通常の配列と違って、`Set` は同値のものが複数入らないように制御しています；また内容はハッシュテーブルで管理しているため、検索パフォーマンスも配列より高いというメリットを持っています；半面ハッシュテーブルで管理しているからこそ、順番の保証もないというのが配列との大きな違いですが、少なくとも今回の要件は順番関係ないので使えると思います。

`Set` で使えるようにするためには、`Equatable` と `Hashable` に適合する必要があるので、ひとまず実装しましょう：

```swift
extension ButtonOption: Equatable {
    static func == (lhs: Self, rhs: Self) -> Bool {
        switch (lhs, rhs) {
        case (.positive, .positive),
             (.negative, .negative):
            return true
            
        case (.positive, .negative),
             (.negative, .positive):
            return false
        }
    }
}

extension ButtonOption: Hashable {
    func hash(into hasher: inout Hasher) {
        switch self {
        case .positive:
            hasher.combine(0b01)
            
        case .negative:
            hasher.combine(0b10)
        }
    }
}
```

用件としてはポジティブボタンかネガティブボタンかの区別だけがしたいので、タイトルの内容は関係ないから同値比較もハッシュ関数もタイトルプロパティーを敢えて外しています。そしてこうすれば利用時はこんなふうに呼び出せます：

```swift
class ViewController: UIViewController {
    
    var buttons: Set<ButtonOption> = []
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        if let positive = buttons.first(where: { $0 == .positive(title: nil) }) {
            // ポジティブボタンを表示
        }
        
        if let negative = buttons.first(where: { $0 == .negative(title: nil) }) {
            // ネガティブボタンを表示
        }
    }
    
}
```

このようにすれば、ポジティブボタンとネガティブボタンがそれぞれ最大一つだけ、そして 4 つのパターンを同じく 2 つの `if` 文だけで網羅することも達成できます。

もちろん、もしポジティブネガティブの呼び出し順番は関係ないなら、普通に `for` 文回すのも簡潔でいいです：

```swift
    override func viewDidLoad() {
        super.viewDidLoad()
        
        for button in buttons {
            switch button {
            case .positive(title: let title):
                // ポジティブボタンを表示
            case .negative(title: let title):
                // ネガティブボタンを表示
            }
        }
    }
```