🌟

Swift OptionSet(Macro)の使い方

に公開

OptionSet Protocol

OptionSet Protocolとは1つの型でArrayのように値を管理できるSwiftのprotocolです。

let singleOption: ShippingOptions = .priority
let multipleOptions: ShippingOptions = [.nextDay, .secondDay, .priority]
let noOptions: ShippingOptions = []

SwiftUIだとEdge.Setなどで使われています。

Text("Hello World!")
  .padding([.leading, .trailing], 16)

OptionSetの定義方法

OptionSetにはRawValueが必要です。またinit(rawValue:)というInitializerも必要です。

struct ShippingOptions: OptionSet {
  typealias RawValue = UInt8
  let rawValue: RawValue

  init(rawValue: RawValue) {
    self.rawValue = rawValue
  }

  static let nextDay    = ShippingOptions(rawValue: 1 << 0)
  static let secondDay  = ShippingOptions(rawValue: 1 << 1)
  static let priority   = ShippingOptions(rawValue: 1 << 2)
  static let standard   = ShippingOptions(rawValue: 1 << 3)

  static let express: ShippingOptions = [.nextDay, .secondDay]
  static let all: ShippingOptions = [.express, .priority, .standard]
}

Macroを使ったOptionSetの定義方法

上の宣言をMacroを使うと簡単に実装可能です。

@OptionSet<UInt8>
struct ShippingOptions {
  private enum Options: UInt8 {
    case nextDay
    case secondDay
    case priority
    case standard
  }
}

OptionSetMacroはそのまま使えるように定義されていないため、自分で定義しないといけません。(Swift 6.1時点)

@attached(member, names: named(RawValue), named(rawValue), named(`init`), arbitrary)
@attached(extension, conformances: OptionSet)
public macro OptionSet<RawType>() = #externalMacro(module: "SwiftMacros", type: "OptionSetMacro")

OptionSetの注意点

1. オーバーフローしてしまう

OptionSetはそれぞれの値を各ビットに格納しているため、UInt8の場合8ビットのみ格納のため、8つのパターンまでしか持つことができません。それ以上もつと値がオーバーフローしてしまいrawValueが0のパターンが複数持つことになってしまいます。

下の例ではp8, p9がオーバーフローしてしまい、rawValueが0になってしまいます。
UInt16にすれば16個までパターンを持つことが可能です。

@OptionSet<UInt8>
struct Parameters {
  enum Options: UInt8 {
    case p0
    case p1
    case p2
    case p3
    case p4
    case p5
    case p6
    case p7
    case p8
    case p9
  }
}

print(Parameters.p0.rawValue) // 1
print(Parameters.p1.rawValue) // 2
print(Parameters.p2.rawValue) // 4
print(Parameters.p3.rawValue) // 8
print(Parameters.p4.rawValue) // 16
print(Parameters.p5.rawValue) // 32
print(Parameters.p6.rawValue) // 64
print(Parameters.p7.rawValue) // 128
print(Parameters.p8.rawValue) // 0
print(Parameters.p9.rawValue) // 0

2. RawValueに意味を持たせてはいけない

RawValueは各範囲の値を持っているだけなので、rawValueに意味のあるようなデータには使用できません。
そのためrawValueの参照が必要なプログラムには利用不可です。

enum Signal: UInt8 {
  case powerOn = 0
  case powerOff = 13
  case sendData = 18
}

利用可能な場面

rawValueの指定をせずに、使えると判断できる場合のみ利用可能です。
持たせられるパターン数がデータサイズによるので、注意です。UInt8だと8個までです。

@OptionSet<UInt8>
struct ShippingOptions {
  private enum Options: UInt8 {
    case nextDay
    case secondDay
    case priority
    case standard
  }
}

Discussion