如何在 Swift 中创建 NS_OPTION 样式的位掩码枚举?

在 Apple 关于与 C API 交互的文档中,他们描述了将带 NS_ENUM标记的 C 样式枚举作为 Swift 枚举导入的方式。这是有意义的,因为 Swift 中的枚举很容易作为 enum值类型提供,所以很容易看到如何创建我们自己的。

更进一步,该公司对标记为 NS_OPTIONS的 C 风格选项如下表示:

Swift 还导入用 NS_OPTIONS宏标记的选项 选项的行为类似于导入的枚举,选项还可以 支持一些按位操作,例如 &|~, 表示一个常量为零(0)的空选项集 Swift 使用 nil表示没有任何选项。

考虑到 Swift 中没有 options值类型,我们如何创建一个 C 样式选项变量来使用?

45483 次浏览

Swift 3.0

与 Swift 2.0几乎完全相同。 OptionSetType 被重命名为 OptionSet,枚举按照约定写成小写。

struct MyOptions : OptionSet {
let rawValue: Int


static let firstOption  = MyOptions(rawValue: 1 << 0)
static let secondOption = MyOptions(rawValue: 1 << 1)
static let thirdOption  = MyOptions(rawValue: 1 << 2)
}

Swift 3建议不要提供 none选项,而是简单地使用一个空数组文字:

let noOptions: MyOptions = []

其他用途:

let singleOption = MyOptions.firstOption
let multipleOptions: MyOptions = [.firstOption, .secondOption]
if multipleOptions.contains(.secondOption) {
print("multipleOptions has SecondOption")
}
let allOptions = MyOptions(rawValue: 7)
if allOptions.contains(.thirdOption) {
print("allOptions has ThirdOption")
}

Swift 2.0

在 Swift 2.0中,协议扩展处理了大部分样板文件,这些样板文件现在作为符合 OptionSetType的结构导入。(从 Swift 2 beta 2开始,RawOptionSetType已经消失了。)宣言要简单得多:

struct MyOptions : OptionSetType {
let rawValue: Int


static let None         = MyOptions(rawValue: 0)
static let FirstOption  = MyOptions(rawValue: 1 << 0)
static let SecondOption = MyOptions(rawValue: 1 << 1)
static let ThirdOption  = MyOptions(rawValue: 1 << 2)
}

现在我们可以在 MyOptions中使用基于集合的语义:

let singleOption = MyOptions.FirstOption
let multipleOptions: MyOptions = [.FirstOption, .SecondOption]
if multipleOptions.contains(.SecondOption) {
print("multipleOptions has SecondOption")
}
let allOptions = MyOptions(rawValue: 7)
if allOptions.contains(.ThirdOption) {
print("allOptions has ThirdOption")
}

Swift 1.2

查看 Swift 导入的 Objective-C 选项(例如 UIViewAutoresizing) ,我们可以看到选项被声明为符合协议 RawOptionSetTypestruct,而 RawOptionSetType又符合 _RawOptionSetTypeEquatableRawRepresentableBitwiseOperationsTypeNilLiteralConvertible。我们可以像这样创造我们自己的:

struct MyOptions : RawOptionSetType {
typealias RawValue = UInt
private var value: UInt = 0
init(_ value: UInt) { self.value = value }
init(rawValue value: UInt) { self.value = value }
init(nilLiteral: ()) { self.value = 0 }
static var allZeros: MyOptions { return self(0) }
static func fromMask(raw: UInt) -> MyOptions { return self(raw) }
var rawValue: UInt { return self.value }


static var None: MyOptions { return self(0) }
static var FirstOption: MyOptions   { return self(1 << 0) }
static var SecondOption: MyOptions  { return self(1 << 1) }
static var ThirdOption: MyOptions   { return self(1 << 2) }
}

现在我们可以像苹果文档中描述的那样对待这个新的选项集 MyOptions: 你可以使用类似于 enum的语法:

let opt1 = MyOptions.FirstOption
let opt2: MyOptions = .SecondOption
let opt3 = MyOptions(4)

而且它的行为就像我们期望的选项一样:

let singleOption = MyOptions.FirstOption
let multipleOptions: MyOptions = singleOption | .SecondOption
if multipleOptions & .SecondOption != nil {     // see note
println("multipleOptions has SecondOption")
}
let allOptions = MyOptions.fromMask(7)   // aka .fromMask(0b111)
if allOptions & .ThirdOption != nil {
println("allOptions has ThirdOption")
}

我已经构建了一个 生成器来创建 Swift 选项集,没有所有的发现/替换。

最新: 对 Swift 1.1 beta 3的修改。

Xcode 6.1 Beta 2对 RawOptionSetType协议做了一些修改(参见 Airspeed 的博客条目苹果发布说明)。

基于 Nate Cooks 的例子,这里有一个更新的解决方案,你可以像这样定义你自己的选项集:

struct MyOptions : RawOptionSetType, BooleanType {
private var value: UInt
init(_ rawValue: UInt) { self.value = rawValue }


// MARK: _RawOptionSetType
init(rawValue: UInt) { self.value = rawValue }


// MARK: NilLiteralConvertible
init(nilLiteral: ()) { self.value = 0}


// MARK: RawRepresentable
var rawValue: UInt { return self.value }


// MARK: BooleanType
var boolValue: Bool { return self.value != 0 }


// MARK: BitwiseOperationsType
static var allZeros: MyOptions { return self(0) }


// MARK: User defined bit values
static var None: MyOptions          { return self(0) }
static var FirstOption: MyOptions   { return self(1 << 0) }
static var SecondOption: MyOptions  { return self(1 << 1) }
static var ThirdOption: MyOptions   { return self(1 << 2) }
static var All: MyOptions           { return self(0b111) }
}

然后可以像这样用它来定义变量:

let opt1 = MyOptions.FirstOption
let opt2:MyOptions = .SecondOption
let opt3 = MyOptions(4)

像这样测试比特:

let singleOption = MyOptions.FirstOption
let multipleOptions: MyOptions = singleOption | .SecondOption
if multipleOptions & .SecondOption {
println("multipleOptions has SecondOption")
}


let allOptions = MyOptions.All
if allOptions & .ThirdOption {
println("allOptions has ThirdOption")
}

如果我们需要的唯一功能是一种将选项与 |组合起来的方法,并检查组合选项是否包含与 &的特定选项,那么 Nate Cook 的回答可能是:

创建一个选项 protocol并重载 |&:

protocol OptionsProtocol {


var value: UInt { get }
init (_ value: UInt)


}


func | <T: OptionsProtocol>(left: T, right: T) -> T {
return T(left.value | right.value)
}


func & <T: OptionsProtocol>(left: T, right: T) -> Bool {
if right.value == 0 {
return left.value == 0
}
else {
return left.value & right.value == right.value
}
}

现在我们可以创建更简单的期权结构,如下所示:

struct MyOptions: OptionsProtocol {


private(set) var value: UInt
init (_ val: UInt) {value = val}


static var None: MyOptions { return self(0) }
static var One: MyOptions { return self(1 << 0) }
static var Two: MyOptions { return self(1 << 1) }
static var Three: MyOptions { return self(1 << 2) }
}

它们可用于以下方面:

func myMethod(#options: MyOptions) {
if options & .One {
// Do something
}
}


myMethod(options: .One | .Three)

如果你不需要与 Objective-C 进行互操作,只需要 Swift 中位掩码的 表面语义学,我已经编写了一个简单的“库”,名为 BitwiseOptions,它可以通过常规的 Swift 枚举来实现这一点,例如:

enum Animal: BitwiseOptionsType {
case Chicken
case Cow
case Goat
static let allOptions = [.Chicken, .Cow, .Goat]
}


var animals = Animal.Chicken | Animal.Goat
animals ^= .Goat
if animals & .Chicken == .Chicken {
println("Chick-Fil-A!")
}

诸如此类。这里没有任何实质性的改动。这些是对不透明值的设置操作。你可以找到要点 给你

在 Swift 2(目前作为 Xcode 7 beta 的一部分测试版)中,NS_OPTIONS风格的类型作为新的 OptionSetType类型的子类型导入。由于新的 协议扩展特性和 OptionSetType在标准库中的实现方式,您可以声明自己的扩展 OptionsSetType的类型,并获得与导入的 NS_OPTIONS样式类型相同的函数和方法。

但是这些函数不再基于位算术运算符。在 C 中使用一组非排他布尔选项需要在字段中屏蔽和旋转位,这是一个实现细节。实际上,一组选项是一个 准备好了... 一个独特项目的集合。所以 OptionsSetTypeSetAlgebraType协议中获得了所有的方法,比如从数组文本语法创建、像 contains这样的查询、用 intersection掩码等等(不再需要记住哪个有趣的字符用于哪个成员资格测试!)

正如 Rickster 已经提到的,您可以在 Swift 2.0中使用 OptionSetType。 NS _ OPTION 类型被导入为符合 OptionSetType协议,该协议为选项提供了一个类似于 set 的接口:

struct CoffeeManipulators : OptionSetType {
let rawValue: Int
static let Milk     = CoffeeManipulators(rawValue: 1)
static let Sugar    = CoffeeManipulators(rawValue: 2)
static let MilkAndSugar = [Milk, Sugar]
}

它给了你这样的工作方式:

struct Coffee {
let manipulators:[CoffeeManipulators]


// You can now simply check if an option is used with contains
func hasMilk() -> Bool {
return manipulators.contains(.Milk)
}


func hasManipulators() -> Bool {
return manipulators.count != 0
}
}

文档中的 Swift 2.0示例:

struct PackagingOptions : OptionSetType {
let rawValue: Int
init(rawValue: Int) { self.rawValue = rawValue }


static let Box = PackagingOptions(rawValue: 1)
static let Carton = PackagingOptions(rawValue: 2)
static let Bag = PackagingOptions(rawValue: 4)
static let Satchel = PackagingOptions(rawValue: 8)
static let BoxOrBag: PackagingOptions = [Box, Bag]
static let BoxOrCartonOrBag: PackagingOptions = [Box, Carton, Bag]
}

你可以找到它 给你

//Swift 2.0
//create
struct Direction : OptionSetType {
let rawValue: Int
static let None   = Direction(rawValue: 0)
static let Top    = Direction(rawValue: 1 << 0)
static let Bottom = Direction(rawValue: 1 << 1)
static let Left   = Direction(rawValue: 1 << 2)
static let Right  = Direction(rawValue: 1 << 3)
}
//declare
var direction: Direction = Direction.None
//using
direction.insert(Direction.Right)
//check
if direction.contains(.Right) {
//`enter code here`
}

只是为那些想知道是否可以组合复合选项的人提供一个额外的例子。你可以,而且它们的组合就像你所期望的那样,如果你已经习惯了很好的老式位字段:

struct State: OptionSetType {
let rawValue: Int
static let A      = State(rawValue: 1 << 0)
static let B      = State(rawValue: 1 << 1)
static let X      = State(rawValue: 1 << 2)


static let AB:State  = [.A, .B]
static let ABX:State = [.AB, .X]    // Combine compound state with .X
}


let state: State = .ABX
state.contains(.A)        // true
state.contains(.AB)       // true

它将集合 [.AB, .X]变为 [.A, .B, .X](至少在语义上) :

print(state)      // 0b111 as expected: "State(rawValue: 7)"
print(State.AB)   // 0b11 as expected: "State(rawValue: 3)"

没有其他人提到过它——我在一些修修补补之后偶然发现了它——但是 Swift Set 似乎工作得相当不错。

如果我们思考(也许是一个维恩图?)关于一个位掩码实际上表示什么,它可能是一个空集。

当然,在从第一原则处理这个问题时,我们失去了位运算符的便利,但是获得了强大的基于集合的方法,从而提高了可读性。

下面是我的修修补补的例子:

enum Toppings : String {
// Just strings 'cause there's no other way to get the raw name that I know of...
// Could be 1 << x too...
case Tomato = "tomato"
case Salami = "salami"
case Cheese = "cheese"
case Chicken = "chicken"
case Beef = "beef"
case Anchovies = "anchovies"


static let AllOptions: Set<Toppings> = [.Tomato, .Salami, .Cheese, .Chicken, .Anchovies, .Beef]
}


func checkPizza(toppings: Set<Toppings>) {
if toppings.contains(.Cheese) {
print("Possible dairy allergies?")
}


let meats: Set<Toppings> = [.Beef, .Chicken, .Salami]
if toppings.isDisjointWith(meats) {
print("Vego-safe!")
}
if toppings.intersect(meats).count > 1 {
print("Limit one meat, or 50¢ extra charge!")
}


if toppings == [Toppings.Cheese] {
print("A bit boring?")
}
}


checkPizza([.Tomato, .Cheese, .Chicken, .Beef])


checkPizza([.Cheese])

我觉得这很好,因为我觉得它来自解决问题的第一原则方法——很像 Swift ——而不是尝试采用 C 风格的解决方案。

还希望听到一些 Obj-C 用例,这些用例将挑战这种不同的范例,其中整数原始值仍然显示优点。

使用选项集类型,在快速3使用 OptionSet

struct ShippingOptions: OptionSet {
let rawValue: Int


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

为了避免在使用 (1 << 0)(1 << 1)(1 << 15)等编码时不可避免的硬编码位置,甚至更糟的 1216384等编码位置或某些十六进制变化,可以先在 enum中定义位,然后让枚举进行位序计算:

// Bits
enum Options : UInt {
case firstOption
case secondOption
case thirdOption
}


// Byte
struct MyOptions : OptionSet {
let rawValue: UInt


static let firstOption  = MyOptions(rawValue: 1 << Options.firstOption.rawValue)
static let secondOption = MyOptions(rawValue: 1 << Options.secondOption.rawValue)
static let thirdOption  = MyOptions(rawValue: 1 << Options.thirdOption.rawValue)
}

我使用以下两个值,我需要我可以得到,rawValue 索引数组和值的标志。

enum MyEnum: Int {
case one
case two
case four
case eight


var value: UInt8 {
return UInt8(1 << self.rawValue)
}
}


let flags: UInt8 = MyEnum.one.value ^ MyEnum.eight.value


(flags & MyEnum.eight.value) > 0 // true
(flags & MyEnum.four.value) > 0  // false
(flags & MyEnum.two.value) > 0   // false
(flags & MyEnum.one.value) > 0   // true


MyEnum.eight.rawValue // 3
MyEnum.four.rawValue  // 2

如果需要更多,只需添加一个计算属性。

enum MyEnum: Int {
case one
case two
case four
case eight


var value: UInt8 {
return UInt8(1 << self.rawValue)
}


var string: String {
switch self {
case .one:
return "one"
case .two:
return "two"
case .four:
return "four"
case .eight:
return "eight"
}
}
}

Re: 使用带有多个选项的选项集创建沙箱和书签

let options:NSURL.BookmarkCreationOptions = [.withSecurityScope,.securityScopeAllowOnlyReadAccess]
let temp = try link.bookmarkData(options: options, includingResourceValuesForKeys: nil, relativeTo: nil)

解决创建时需要组合选项的问题,在不是所有选项都互相排斥的情况下非常有用。