これはゆめみ 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
プロパティーだけであって、それ以外のものはそもそも見ていません。
例えばこの例で出てきた positive
と negative
は、それぞれ 0b0001
1 と 0b0010
1 の rawValue
を持っており、結合演算によって 0b0011
1 の結果が生成されたに過ぎません。もともと positive
と negative
が持ってた title
というプロパティーはそもそもこの結合演算に無視されているのです。よって print
しても nil
しか出てきません。
ではこの場合どうすればいいかというと、enum
の方が適しています。enum
には AssociatedValue というものがあるので、ボタンの種類とテキストをくっつけさせることが可能です:
enum ButtonOption {
case positive(title: String?)
case negative(title: String?)
}
もちろんこの方法には一つ欠点があります、それは管理する時は配列で管理することになるので、フラグフィールドみたいにどっちかには一つだけという制限を入れるのが難しいということです。しかし実はちょっと工夫をすれば、それも不可能ではありません。
同様なものが複数入らないようにする仕組みとして、Swift は Set
というものが提供されています。通常の配列と違って、Set
は同値のものが複数入らないように制御しています;また内容はハッシュテーブルで管理しているため、検索パフォーマンスも配列より高いというメリットを持っています;半面ハッシュテーブルで管理しているからこそ、順番の保証もないというのが配列との大きな違いですが、少なくとも今回の要件は順番関係ないので使えると思います。
Set
で使えるようにするためには、Equatable
と Hashable
に適合する必要があるので、ひとまず実装しましょう:
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):
// ネガティブボタンを表示
}
}
}