Help us understand the problem. What is going on with this article?

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

これはゆめみ Advent Calendar 2019 8 日目の記事です。本来は @guitar_char さんの担当ですが、最近担当案件がぷち修羅場とのことですので私が代わりに投稿します。

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


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

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

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

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

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

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]
}

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

class ViewController: UIViewController {

    var buttons: ButtonOption // ...

    override func viewDidLoad() {
        super.viewDidLoad()

        if buttons.contains(.positive) {
            // ポジティブボタンを表示
        }

        if buttons.contains(.negative) {
            // ネガティブボタンを表示
        }

    }

}

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

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

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

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

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

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 プロパティーだけであって、それ以外のものはそもそも見ていません。

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

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

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

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

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

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

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)
        }
    }
}

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

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 文回すのも簡潔でいいです:

    override func viewDidLoad() {
        super.viewDidLoad()

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

  1. 0bxxxx の書式は二進数を表すリテラルです。例えば 0b0000 は十進数で 00b0001 は十進数で 10b0010 は十進数で 20b0011 は十進数で 3, 0b0100 は十進数で 4…といった具合です。 

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした