Bagaimana cara membuat enumerasi bitmask bergaya NS_OPTIONS di Swift?

137

Dalam dokumentasi Apple tentang berinteraksi dengan C API, mereka mendeskripsikan cara NS_ENUMpencacahan gaya-C yang diimpor sebagai pencacahan Swift. Ini masuk akal, dan karena enumerasi di Swift sudah tersedia sebagai enumtipe nilai, mudah untuk melihat cara membuatnya sendiri.

Lebih jauh ke bawah, dikatakan ini tentang NS_OPTIONSopsi C-style bertanda:

Swift juga mengimpor opsi yang ditandai dengan NS_OPTIONSmakro. Sedangkan opsi berperilaku sama mantri diimpor, pilihan juga dapat mendukung beberapa operasi bitwise, seperti &, |, dan ~. Di Objective-C, Anda mewakili set opsi kosong dengan konstanta nol ( 0). Di Swift, gunakan niluntuk menunjukkan tidak adanya opsi apa pun.

Mengingat bahwa tidak ada optionstipe nilai di Swift, bagaimana kita bisa membuat variabel opsi C-Style untuk digunakan?

Nate Cook
sumber
3
"NSHipster" yang sangat terkenal dari @ Mattt memiliki deskripsi ekstensif tentang RawOptionsSetType: nshipster.com/rawoptionsettype
Klaas
Kemungkinan duplikat dari Mendeklarasikan dan menggunakan sedikit field enum di Swift
Peter Ahlberg

Jawaban:

261

Swift 3.0.0

Hampir identik dengan Swift 2.0. OptionSetType diubah namanya menjadi OptionSet dan enum ditulis dengan huruf kecil berdasarkan konvensi.

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

Alih-alih memberikan noneopsi, rekomendasi Swift 3 cukup menggunakan literal array kosong:

let noOptions: MyOptions = []

Penggunaan lainnya:

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

Di Swift 2.0, ekstensi protokol menangani sebagian besar boilerplate untuk ini, yang sekarang diimpor sebagai struct yang sesuai OptionSetType. ( RawOptionSetTypetelah menghilang sejak Swift 2 beta 2.) Deklarasi ini jauh lebih sederhana:

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

Sekarang kita dapat menggunakan semantik berbasis set dengan 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.0

Melihat pilihan Objective-C yang diimpor oleh Swift ( UIViewAutoresizingmisalnya), kita dapat melihat bahwa pilihan dinyatakan sebagai structyang paling sesuai untuk protokol RawOptionSetType, yang pada gilirannya sesuai dengan _RawOptionSetType, Equatable, RawRepresentable, BitwiseOperationsType, dan NilLiteralConvertible. Kita bisa membuatnya sendiri seperti ini:

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

Sekarang kita dapat menangani rangkaian opsi baru ini MyOptions, seperti yang dijelaskan dalam dokumentasi Apple: Anda dapat menggunakan enumsintaks -seperti:

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

Dan itu juga berperilaku seperti kita mengharapkan opsi untuk berperilaku:

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

Saya telah membangun generator untuk membuat satu set opsi Swift tanpa semua menemukan / mengganti.

Terbaru: Modifikasi untuk Swift 1.1 beta 3.

Nate Cook
sumber
1
Itu tidak berhasil untuk saya kecuali saya membuat valuefile UInt32. Anda juga tidak perlu menentukan salah satu fungsi, fungsi yang relevan sudah ditentukan untuk RawOptionSets (misalnya func |<T : RawOptionSet>(a: T, b: T) -> T)
David Lawson
Terima kasih, poin yang bagus tentang fungsinya - Saya pikir kompiler mengeluh tentang mereka ketika saya tidak memiliki kesesuaian protokol lainnya. Masalah apa yang Anda lihat UInt? Ini bekerja dengan baik untuk saya.
Nate Cook
2
Apakah ada solusi yang menggunakan enum daripada struct? Saya perlu milik saya agar kompatibel dengan obyektif-c ...
jowie
1
@jowieenum CollisionTypes: UInt32 { case Player = 1 case Wall = 2 case Star = 4 case Vortex = 8 case Finish = 16 }
mccoyLBI
1
Dalam hal ini, dokumen Apple sangat bagus.
Tuan Rogers
12

Xcode 6.1 Beta 2 membawa beberapa perubahan pada RawOptionSetTypeprotokol (lihat entri blog Airspeedvelocity ini dan catatan rilis Apple ).

Berdasarkan contoh Nate Cooks, berikut adalah solusi yang diperbarui. Anda dapat menentukan set opsi Anda sendiri seperti ini:

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

Kemudian dapat digunakan seperti ini untuk mendefinisikan variabel:

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

Dan seperti ini untuk menguji bit:

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
sumber
8

Contoh Swift 2.0 dari dokumentasi:

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

Anda bisa menemukannya di sini

Tomasz Bąk
sumber
6

Di Swift 2 (saat ini beta sebagai bagian dari Xcode 7 beta), NS_OPTIONStipe-gaya diimpor sebagai subtipe dari OptionSetTypetipe baru . Dan berkat fitur Ekstensi Protokol baru dan caranya OptionSetTypediimplementasikan di pustaka standar, Anda dapat mendeklarasikan tipe Anda sendiri yang memperluas OptionsSetTypedan mendapatkan semua fungsi dan metode yang sama dengan yang NS_OPTIONSdidapat tipe gaya- impor .

Tetapi fungsi tersebut tidak lagi didasarkan pada operator aritmatika bitwise. Bekerja dengan sekumpulan opsi Boolean non-eksklusif di C membutuhkan bit masking dan twiddling di bidang adalah detail implementasi. Sungguh, satu set opsi adalah satu set ... kumpulan barang-barang unik. Jadi OptionsSetTypedapatkan semua metode dari SetAlgebraTypeprotokol, seperti pembuatan dari sintaks literal array, kueri seperti contains, masking dengan intersection, dll. (Tidak perlu lagi mengingat karakter lucu mana yang akan digunakan untuk tes keanggotaan mana!)

rickster
sumber
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
sumber
4

Jika Anda tidak perlu bekerja sama dengan Objective-C dan hanya ingin semantik permukaan bit mask di Swift, saya telah menulis "library" sederhana yang disebut BitwiseOptions yang dapat melakukan ini dengan enumerasi Swift biasa, misalnya:

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

dan seterusnya. Tidak ada bit aktual yang dibalik di sini. Ini adalah operasi set pada nilai buram. Anda dapat menemukan intinya di sini .

Gregory Higley
sumber
@ChrisPrince Kemungkinan besar itu karena dibuat untuk Swift 1.0 dan belum diperbarui sejak itu.
Gregory Higley
Saya sebenarnya sedang mengerjakan versi Swift 2.0 ini.
Gregory Higley
2

Seperti yang telah disebutkan Rickster, Anda dapat menggunakan OptionSetType di Swift 2.0. Jenis NS_OPTIONS diimpor sesuai dengan OptionSetTypeprotokol, yang menampilkan antarmuka set-like untuk opsi:

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

Ini memberi Anda cara kerja ini:

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
sumber
2

Jika satu-satunya fungsi yang kami perlukan adalah cara untuk menggabungkan opsi dengan |dan memeriksa apakah opsi gabungan berisi opsi tertentu dengan &alternatif jawaban Nate Cook bisa jadi ini:

Buat opsi protocoldan beban berlebih |dan &:

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

Sekarang kita dapat membuat pilihan struct lebih sederhana seperti ini:

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

Mereka dapat digunakan sebagai berikut:

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

myMethod(options: .One | .Three) 
Sederhana99
sumber
2

Hanya memposting contoh tambahan untuk siapa pun yang bertanya-tanya apakah Anda dapat menggabungkan opsi gabungan. Anda bisa, dan mereka bergabung seperti yang Anda harapkan jika Anda terbiasa dengan bitfield lama yang bagus:

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

Ini meratakan set [.AB, .X]menjadi [.A, .B, .X](setidaknya secara semantik):

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

Tidak ada orang lain yang menyebutkannya - dan saya agak melakukan kesalahan padanya setelah beberapa mengutak-atik - tetapi Swift Set tampaknya bekerja dengan cukup baik.

Jika kita berpikir (mungkin untuk diagram Venn?) Tentang apa yang sebenarnya diwakili oleh bit mask, itu adalah set yang mungkin kosong.

Tentu saja, dalam mendekati masalah dari prinsip pertama, kami kehilangan kenyamanan operator bitwise, tetapi mendapatkan metode berbasis set yang kuat yang meningkatkan keterbacaan.

Ini contoh mengutak-atik saya:

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

Saya merasa ini bagus karena saya merasa ini berasal dari pendekatan prinsip pertama untuk masalah - seperti Swift - daripada mencoba menyesuaikan solusi C-style.

Juga ingin mendengar beberapa kasus penggunaan Obj-C yang akan menantang paradigma yang berbeda ini, di mana nilai mentah integer masih menunjukkan manfaat.

Semprotan serangga
sumber
1

Untuk menghindari hard coding posisi bit, yang tidak dapat dihindari ketika menggunakan (1 << 0), (1 << 1), (1 << 15)dll atau bahkan lebih buruk 1, 2, 16384dll atau beberapa variasi heksadesimal, yang pertama bisa mendefinisikan bit dalam enum, kemudian biarkan kata enum melakukan sedikit ordinal perhitungan:

// 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
sumber
Hanya menambahkan contoh di mana Anda tidak perlu kode keras apa pun.
Peter Ahlberg
1

Saya menggunakan yang berikut ini, saya membutuhkan kedua nilai yang bisa saya peroleh, rawValue untuk mengindeks array dan nilai untuk bendera.

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

Dan jika seseorang membutuhkan lebih banyak, tambahkan saja properti yang dihitung.

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
sumber
1

re: Kreasi sandbox dan bookmark menggunakan set opsi dengan beberapa opsi

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

solusi untuk kebutuhan menggabungkan opsi untuk kreasi, berguna ketika tidak semua opsi saling eksklusif.

slashlos
sumber
0

Jawaban Nate bagus tapi saya akan membuatnya DIY, seperti ini:

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
sumber
0

Gunakan Jenis Set Opsi, dalam penggunaan 3 cepat 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
sumber
1
Ini kurang lebih sudah tercakup dalam jawaban ini .
Pang