¿Cómo crear enumeraciones de máscara de bits de estilo NS_OPTIONS en Swift?

137

En la documentación de Apple sobre la interacción con las API de C, describen la forma NS_ENUM enumeraciones de estilo C marcadas se importan como enumeraciones Swift. Esto tiene sentido, y dado que las enumeraciones en Swift se proporcionan fácilmente como el enumtipo de valor, es fácil ver cómo crear el nuestro.

Más abajo, dice esto sobre NS_OPTIONS opciones de estilo C marcadas:

Swift también importa opciones marcadas con la NS_OPTIONSmacro. Mientras que las opciones se comportan de manera similar a las enumeraciones importados, las opciones también pueden apoyar algunas operaciones bit a bit, tales como &, |, y ~. En Objective-C, representa un conjunto de opciones vacío con la constante cero ( 0). En Swift, use nilpara representar la ausencia de opciones.

Dado que no hay un optionstipo de valor en Swift, ¿cómo podemos crear una variable de opciones de estilo C para trabajar?

Nate Cook
fuente
3
El muy famoso "NSHipster" de @ Mattt tiene una extensa descripción del RawOptionsSetType: nshipster.com/rawoptionsettype
Klaas

Respuestas:

258

Swift 3.0

Casi idéntico a Swift 2.0. OptionSetType se renombró a OptionSet y las enumeraciones se escriben en minúsculas por convención.

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

En lugar de proporcionar una noneopción, la recomendación de Swift 3 es simplemente usar un literal de matriz vacío:

let noOptions: MyOptions = []

Otro uso:

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

En Swift 2.0, las extensiones de protocolo se encargan de la mayoría de las repeticiones, que ahora se importan como una estructura que se ajusta a ellas OptionSetType. ( RawOptionSetTypeha desaparecido a partir de Swift 2 beta 2.) La declaración es mucho más simple:

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

Ahora podemos usar la semántica basada en conjuntos con 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

En cuanto a las opciones de Objective-C que se importaron por Swift ( UIViewAutoresizingpor ejemplo), podemos ver que las opciones se declaran como una structque cumpla con el protocolo RawOptionSetType, que cumple en turno para _RawOptionSetType, Equatable, RawRepresentable, BitwiseOperationsType, y NilLiteralConvertible. Podemos crear el nuestro así:

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

Ahora podemos tratar este nuevo conjunto de opciones MyOptions, tal como se describe en la documentación de Apple: puede usar una enumsintaxis similar:

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

Y también se comporta como esperaríamos que se comporten las opciones:

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

He construido un generador para crear un conjunto de opciones Swift sin todo el buscar / reemplazar.

Último: Modificaciones para Swift 1.1 beta 3.

Nate Cook
fuente
1
No funcionó para mí a menos que hiciera valueun UInt32. Tampoco necesita definir ninguna de las funciones, las funciones relevantes ya están definidas para RawOptionSets (por ejemplo func |<T : RawOptionSet>(a: T, b: T) -> T)
David Lawson
Gracias, gran punto sobre las funciones: creo que el compilador se quejaba de ellas cuando no tenía el resto de la conformidad del protocolo. ¿Con qué problemas viste UInt? Esta trabajando bien para mi.
Nate Cook
2
¿Hay alguna solución que use enum en lugar de struct? Necesito que el mío sea compatible con el objetivo-c ...
jowie
1
@jowieenum CollisionTypes: UInt32 { case Player = 1 case Wall = 2 case Star = 4 case Vortex = 8 case Finish = 16 }
mccoyLBI
1
En este caso, los documentos de Apple son realmente buenos.
Sr. Rogers
12

Xcode 6.1 Beta 2 trajo algunos cambios al RawOptionSetTypeprotocolo (consulte esta entrada de blog de Airspeedvelocity y las notas de la versión de Apple ).

Basado en el ejemplo de Nate Cooks, aquí hay una solución actualizada. Puede definir su propio conjunto de opciones de esta manera:

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

Luego se puede usar así para definir variables:

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

Y así para probar bits:

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")
}
Klaas
fuente
8

Ejemplo de Swift 2.0 de la documentación:

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

Lo puedes encontrar aqui

Tomasz Bąk
fuente
6

En Swift 2 (actualmente beta como parte de Xcode 7 beta), los NS_OPTIONStipos de estilo se importan como subtipos del nuevo OptionSetTypetipo. Y gracias a la nueva característica Extensiones de protocolo y la forma en que OptionSetTypese implementa en la biblioteca estándar, puede declarar sus propios tipos que se extienden OptionsSetTypey obtienen las mismas funciones y métodos que los NS_OPTIONStipos de estilo importados .

Pero esas funciones ya no se basan en operadores aritméticos bit a bit. Que trabajar con un conjunto de opciones booleanas no exclusivas en C requiere enmascarar y girar bits en un campo es un detalle de implementación. Realmente, un conjunto de opciones es un conjunto ... una colección de artículos únicos. Por lo tanto, OptionsSetTypeobtiene todos los métodos del SetAlgebraTypeprotocolo, como la creación de sintaxis literal de matriz, consultas como contains, enmascaramiento intersection, etc. (¡No más tener que recordar qué personaje divertido usar para qué prueba de membresía!)

rickster
fuente
5
//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`
}
PhuocLuong
fuente
4

Si no necesita interoperar con Objective-C y solo desea la semántica de superficie de las máscaras de bits en Swift, he escrito una simple "biblioteca" llamada BitwiseOptions que puede hacer esto con enumeraciones regulares de Swift, por ejemplo:

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

y así. Aquí no se están volteando partes reales. Estas son operaciones establecidas en valores opacos. Puedes encontrar la esencia aquí .

Gregory Higley
fuente
@ChrisPrince Lo más probable es que se haya creado para Swift 1.0 y no se haya actualizado desde entonces.
Gregory Higley
De hecho, estoy trabajando en una versión Swift 2.0 de esto.
Gregory Higley
2

Como Rickster ya mencionó, puede usar OptionSetType en Swift 2.0. Los tipos NS_OPTIONS se importan conforme al OptionSetTypeprotocolo, que presenta una interfaz similar a un conjunto para las opciones:

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

Te ofrece esta forma de trabajar:

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
    }
}
Antoine
fuente
2

Si la única funcionalidad que necesitamos es una forma de combinar opciones |y verificar si las opciones combinadas contienen una opción particular con& una alternativa a la respuesta de Nate Cook podría ser esta:

Crea una opción protocoly sobrecarga |y &:

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

Ahora podemos crear estructuras de opciones más simplemente así:

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

Se pueden usar de la siguiente manera:

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

myMethod(options: .One | .Three) 
Simple99
fuente
2

Solo publico un ejemplo adicional para cualquier persona que se preguntara si podría combinar opciones compuestas. Puede, y se combinan como es de esperar si está acostumbrado a los viejos campos de bits:

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

Se aplana el conjunto [.AB, .X]en [.A, .B, .X](al menos semánticamente):

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

Nadie más lo ha mencionado, y me equivoqué después de algunos retoques, pero un Swift Set parece funcionar bastante bien.

Si pensamos (¿tal vez en un diagrama de Venn?) Acerca de lo que realmente representa una máscara de bits, es un conjunto posiblemente vacío.

Por supuesto, al abordar el problema desde los primeros principios, perdemos la comodidad de los operadores bit a bit, pero obtenemos métodos poderosos basados ​​en conjuntos que mejoran la legibilidad.

Aquí está mi retoque, por ejemplo:

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

Encuentro esto agradable porque siento que proviene de un enfoque de primeros principios para el problema, al igual que Swift, en lugar de tratar de adaptar soluciones de estilo C.

También me gustaría escuchar algunos casos de uso de Obj-C que desafiarían este paradigma diferente, donde los valores brutos enteros aún muestran mérito.

Repelente de insectos
fuente
1

Con el fin de evitar la codificación duro las posiciones de bit, que es inevitable cuando se utilizan (1 << 0), (1 << 1), (1 << 15)etc., o incluso peor 1, 2, 16384etc., o alguna variación hexadecimal, se podría primero define los bits en una enum, entonces que dijo enum hacer el cálculo ordinal bits:

// 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)
}
Arquitecto Swift
fuente
Acabo de agregar un ejemplo donde no tienes que codificar nada.
Peter Ahlberg
1

Utilizo lo siguiente. Necesito los dos valores que puedo obtener, rawValue para indexar matrices y valor para flags.

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

Y si uno necesita más, simplemente agregue una propiedad calculada.

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"
        }
    }
}
Peter Ahlberg
fuente
1

re: Sandbox y creaciones de marcadores usando conjuntos de opciones con varias opciones

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

solución a la necesidad de combinar opciones para creaciones, útil cuando no todas las opciones son mutuamente excluyentes.

slashlos
fuente
0

La respuesta de Nate es buena, pero lo haría DIY, así:

struct MyOptions : OptionSetType {
    let rawValue: Int

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

Use un tipo de conjunto de opciones, en uso rápido 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]
}
geek1706
fuente
1
Esto está más o menos cubierto en esta respuesta .
Pang