Jak utworzyć wyliczenia maski bitowej w stylu NS_OPTIONS w języku Swift?

137

W dokumentacji firmy Apple dotyczącej interakcji z interfejsami API języka C opisują sposób, w jaki NS_ENUMwyliczenia w stylu C ze znacznikami są importowane jako wyliczenia Swift. Ma to sens, a ponieważ wyliczenia w języku Swift są łatwo dostarczane jako enumtyp wartości, łatwo jest zobaczyć, jak utworzyć własne.

W dalszej części jest to napisane o NS_OPTIONSoznaczonych opcjach w stylu C:

Swift importuje również opcje oznaczone NS_OPTIONSmakrem. Podczas gdy opcje zachowują się podobnie do zaimportowanych wyliczeń, opcje mogą również obsługiwać niektóre operacje bitowe, takie jak& , |i ~. W Objective-C reprezentujesz pusty zestaw opcji ze stałą zerową ( 0). W języku Swift służy nildo reprezentowania braku jakichkolwiek opcji.

Biorąc pod uwagę, że optionsw języku Swift nie ma typu wartości, w jaki sposób możemy utworzyć zmienną opcji w stylu C do pracy?

Nate Cook
źródło
3
Słynny „NSHipster” @ Mattta zawiera obszerny opis RawOptionsSetType: nshipster.com/rawoptionsettype
Klaas

Odpowiedzi:

258

Swift 3.0

Prawie identyczny z Swift 2.0. Nazwa OptionSetType została zmieniona na OptionSet i zgodnie z konwencją wyliczenia są zapisywane małymi literami.

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

Zamiast oferować noneopcję, zaleceniem Swift 3 jest po prostu użycie pustego literału tablicy:

let noOptions: MyOptions = []

Inne zastosowanie:

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

W Swift 2.0 rozszerzenia protokołów zajmują się większością standardowych elementów, które są teraz importowane jako struktura zgodna z OptionSetType. ( RawOptionSetTypezniknął od Swift 2 beta 2.) Deklaracja jest znacznie prostsza:

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

Teraz możemy używać semantyki opartej na zbiorach z 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

Patrząc na opcjach Objective-C, które zostały przywiezione przez SWIFT ( UIViewAutoresizingna przykład), możemy zobaczyć, że opcje są zadeklarowane jako structzgodnym z protokołem RawOptionSetType, który z kolei jest zgodny z _RawOptionSetType, Equatable, RawRepresentable, BitwiseOperationsType, i NilLiteralConvertible. Możemy stworzyć własne w ten sposób:

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

Teraz możemy traktować ten nowy zestaw opcji MyOptions, tak jak opisano w dokumentacji Apple: możesz użyć enumskładni podobnej do -jak:

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

Zachowuje się również tak, jakbyśmy oczekiwali, że opcje będą się zachowywać:

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

Zbudowałem generator do tworzenia zestawu opcji Swift bez konieczności znajdowania / zastępowania.

Najnowsze: Modyfikacje do Swift 1.1 beta 3.

Nate Cook
źródło
1
To nie zadziałało, chyba że zrobiłem valueplik UInt32. Nie musisz także definiować żadnej funkcji, odpowiednie funkcje są już zdefiniowane dla RawOptionSets (np. func |<T : RawOptionSet>(a: T, b: T) -> T)
David Lawson
Dzięki, świetna uwaga na temat funkcji - myślę, że kompilator narzekał na te, gdy nie miałem na miejscu zgodności z pozostałymi protokołami. Jakie problemy widziałeś UInt? U mnie działa dobrze.
Nate Cook
2
Czy istnieje rozwiązanie, które używa enum zamiast struct? Potrzebuję mojego, aby był kompatybilny z celem-c ...
jowie
1
@jowieenum CollisionTypes: UInt32 { case Player = 1 case Wall = 2 case Star = 4 case Vortex = 8 case Finish = 16 }
mccoyLBI
1
W tym przypadku dokumenty Apple są naprawdę dobre.
Mr Rogers
12

Xcode 6.1 Beta 2 wprowadził pewne zmiany w RawOptionSetTypeprotokole (zobacz ten wpis na blogu Airspeedvelocity i informacje o wydaniu Apple ).

Na podstawie przykładu Nate Cooks tutaj jest zaktualizowane rozwiązanie. Możesz zdefiniować swój własny zestaw opcji w następujący sposób:

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

Można go następnie użyć w następujący sposób do zdefiniowania zmiennych:

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

I tak, aby przetestować bity:

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
źródło
8

Przykład Swift 2.0 z dokumentacji:

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

Znajdziesz go tutaj

Tomasz Bąk
źródło
6

W Swift 2 (obecnie beta jako część wersji beta Xcode 7) NS_OPTIONStypy-style są importowane jako podtypy nowego OptionSetTypetypu. Dzięki nowej funkcji Protocol Extensions i sposobowi OptionSetTypeimplementacji w standardowej bibliotece możesz zadeklarować własne typy, które rozszerzają OptionsSetTypei uzyskują te same funkcje i metody, które zostały zaimportowaneNS_OPTIONS otrzymują typy.

Ale te funkcje nie są już oparte na bitowych operatorach arytmetycznych. To, że praca z zestawem niewyłącznych opcji boolowskich w C wymaga maskowania i kręcenia bitów w polu, jest szczegółem implementacji. Tak naprawdę zestaw opcji to zestaw ... zbiór unikalnych przedmiotów. Więc OptionsSetTypepobiera wszystkie metody z SetAlgebraTypeprotokołu, takie jak tworzenie ze składni literału tablicowego, zapytania, takie jak containsmaskowanie intersectionitp. ( Koniec z koniecznością pamiętania, którego zabawnego znaku użyć dla którego testu członkostwa!)

rickster
źródło
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
źródło
4

Jeśli nie potrzebujesz współdziałać z Objective-C i po prostu potrzebujesz powierzchniowej semantyki masek bitowych w Swift, napisałem prostą „bibliotekę” o nazwie BitwiseOptions, która może to zrobić za pomocą zwykłych wyliczeń w języku Swift, np .:

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

i tak dalej. Żadne rzeczywiste bity nie są tutaj odwracane. Są to operacje na ustawieniach nieprzezroczystych wartości. Możesz znaleźć sedno tutaj .

Gregory Higley
źródło
@ChrisPrince Najprawdopodobniej dzieje się tak dlatego, że został stworzony dla Swift 1.0 i od tego czasu nie był aktualizowany.
Gregory Higley
Aktualnie pracuję nad wersją Swift 2.0 tego.
Gregory Higley
2

Jak już wspomniał Rickster, możesz użyć OptionSetType w Swift 2.0. Typy NS_OPTIONS są importowane jako zgodne z OptionSetTypeprotokołem, co przedstawia interfejs podobny do zestawu dla opcji:

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

Daje ci taki sposób pracy:

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
źródło
2

Jeśli jedyną funkcjonalnością, której potrzebujemy, jest sposób łączenia opcji |i sprawdzania, czy połączone opcje zawierają konkretną opcję z& alternatywą dla odpowiedzi Nate Cooka, może to być:

Utwórz opcje protocoli przeciąż |i &:

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

Teraz możemy tworzyć struktury opcji w prostszy sposób:

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

Można ich używać w następujący sposób:

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

myMethod(options: .One | .Three) 
Prosty 99
źródło
2

Po prostu zamieszczam dodatkowy przykład dla każdego, kto zastanawiał się, czy można połączyć opcje złożone. Możesz i łączą się tak, jak można by się spodziewać, jeśli jesteś przyzwyczajony do starych, dobrych pól bitowych:

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

Spłaszcza zestaw [.AB, .X]do [.A, .B, .X](przynajmniej semantycznie):

print(state)      // 0b111 as expected: "State(rawValue: 7)"
print(State.AB)   // 0b11 as expected: "State(rawValue: 3)"
Jarrod Smith
źródło
1

Nikt inny o tym nie wspomniał - i trochę się pomyliłem po kilku majstrach - ale Swift Set wydaje się działać całkiem dobrze.

Jeśli pomyślimy (może na diagramie Venna?) O tym, co właściwie reprezentuje maska ​​bitowa, jest to prawdopodobnie pusty zbiór.

Oczywiście, podchodząc do problemu od pierwszych zasad, tracimy wygodę operatorów bitowych, ale zyskujemy potężne metody oparte na zbiorach, które poprawiają czytelność.

Oto moje majsterkowanie na przykład:

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

Uważam to za przyjemne, ponieważ czuję, że wynika to z podejścia do problemu opartego na podstawowych zasadach - podobnie jak w przypadku Swift - zamiast próby dostosowania rozwiązań w stylu C.

Chciałbym również usłyszeć kilka przypadków użycia Obj-C, które podważałyby ten inny paradygmat, w którym nieprzetworzone wartości całkowite nadal wykazują zalety.

Spray na owady
źródło
1

W celu uniknięcia twarde kodowania pozycji bitowych, co jest nieuniknione przy korzystaniu (1 << 0), (1 << 1), (1 << 15)itd., A nawet gorzej 1, 2, 16384itd. Lub jakąś odmianę szesnastkowy, można najpierw definiuje bity w enum, pozwól powiedział enum zrobić bit porządkowej obliczenia:

// 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)
}
SwiftArchitect
źródło
Właśnie dodałem przykład, w którym nie musisz niczego twardo kodować.
Peter Ahlberg
1

Używam następujących potrzebuję obu wartości, które mogę uzyskać, rawValue do indeksowania tablic i wartości dla flag.

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

A jeśli potrzeba więcej, po prostu dodaj obliczoną właściwość.

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
źródło
1

re: Tworzenie piaskownicy i zakładek przy użyciu zestawów opcji z kilkoma opcjami

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

rozwiązanie polegające na konieczności łączenia opcji dla kreacji, przydatne, gdy nie wszystkie opcje wykluczają się wzajemnie.

slashlos
źródło
0

Odpowiedź Nate'a jest dobra, ale zrobiłbym to samodzielnie, na przykład:

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
źródło
0

Użyj typu zestawu opcji w szybkim 3 użyciu 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
źródło
1
Jest to mniej więcej omówione w tej odpowiedzi .
Pang