Как отобразить значения OptionSet в удобочитаемой форме?

Swift имеет тип OptionSet, который в основном добавляет операции установки к битовым флагам C-Style. Apple довольно широко использует их в своих фреймворках. Примеры включают параметр options в animate(withDuration:delay:options:animations:completion:).

С положительной стороны, это позволяет вам использовать чистый код, например:

options: [.allowAnimatedContent, .curveEaseIn]

Однако есть и обратная сторона.

Если я хочу отобразить указанные значения OptionSet, похоже, нет простого способа сделать это:

let options: UIViewAnimationOptions = [.allowAnimatedContent, .curveEaseIn]
print("options = " + String(describing: options))

Отображает очень бесполезное сообщение:

параметры = UIViewAnimationOptions (rawValue: 65664)

Документы для некоторых из этих битовых полей выражают константу как значение степени двойки:

flag0    = Flags(rawValue: 1 << 0)

Но документы для моего примера OptionSet, UIViewAnimationOptions, ничего не говорят вам о числовом значении этих флагов, и вычисление битов из десятичных чисел не так просто.

Вопрос:

Есть ли какой-нибудь чистый способ сопоставить OptionSet с выбранными значениями?

Мой желаемый результат будет примерно таким:

options = UIViewAnimationOptions([.allowAnimatedContent, .curveEaseIn])

Но я не могу придумать способ сделать это без добавления беспорядочного кода, который потребовал бы от меня поддержки таблицы отображаемых имен для каждого флага.

(Мне интересно сделать это как для системных фреймворков, так и для пользовательских наборов параметров, которые я создаю в своем собственном коде.)

Перечисления позволяют вам иметь как имя, так и необработанное значение для перечисления, но они не поддерживают функции набора, которые вы получаете с помощью OptionSets.


person Duncan C    schedule 03.03.2017    source источник
comment
Я считаю это недостатком консольного вывода. Мне хотелось бы, чтобы имена появлялись в выводе. Я не согласен с тем, что это ограничивается наборами опций; например, почему UIApplicationState не может войти в журнал как .active? — Однако я думаю, что вы не хуже меня знаете, что это ненастоящий вопрос; ты знаешь, что нет ответа, и ты просто стонешь. Вы правы, когда стонете, но это не вопрос переполнения стека. :)   -  person matt    schedule 04.03.2017
comment
@matt, На самом деле я надеюсь, что есть какой-то трюк, о котором я не знаю, и что вместо того, чтобы стонать, я смогу станцевать счастливый танец.   -  person Duncan C    schedule 04.03.2017
comment
Я думаю сделать свой класс CustomStringConvertible и написать код для отображения этих значений, но это было бы неприятно и требовало ручного обслуживания.   -  person Duncan C    schedule 04.03.2017
comment
Возможно связаны? Почему перечисление @objc имеет другое описание, чем чистое перечисление Swift?. Я думаю, что суть в том, что UIViewAnimationOptions — это просто тип @objc OptionSet, который является просто оболочкой битовой маски NS_OPTIONS. Я думаю, что это просто связано со Swift как базовое целое число без каких-либо базовых метаданных.   -  person JAL    schedule 13.03.2017
comment
Разве это не возможно сделать общим способом для всех перечислений, используя зеркало?   -  person Kartick Vaddadi    schedule 13.03.2017
comment
@VaddadiKartick Эта статья, кажется, намекает, что возможно, но у меня возникли проблемы с его применением к типу UIKit OptionSet.   -  person JAL    schedule 13.03.2017


Ответы (5)


Вот один из подходов, который я использовал, используя словарь и перебирая ключи. Не супер, но работает.

struct MyOptionSet: OptionSet, Hashable, CustomStringConvertible {

    let rawValue: Int
    static let zero = MyOptionSet(rawValue: 1 << 0)
    static let one = MyOptionSet(rawValue: 1 << 1)
    static let two = MyOptionSet(rawValue: 1 << 2)
    static let three = MyOptionSet(rawValue: 1 << 3)

    var hashValue: Int {
        return self.rawValue
    }

    static var debugDescriptions: [MyOptionSet:String] = {
        var descriptions = [MyOptionSet:String]()
        descriptions[.zero] = "zero"
        descriptions[.one] = "one"
        descriptions[.two] = "two"
        descriptions[.three] = "three"
        return descriptions
    }()

    public var description: String {
        var result = [String]()
        for key in MyOptionSet.debugDescriptions.keys {
            guard self.contains(key),
                let description = MyOptionSet.debugDescriptions[key]
                else { continue }
            result.append(description)
        }
        return "MyOptionSet(rawValue: \(self.rawValue)) \(result)"
    }

}

let myOptionSet = MyOptionSet([.zero, .one, .two])

// prints MyOptionSet(rawValue: 7) ["two", "one", "zero"]
person MH175    schedule 28.10.2018

Протокол StrOptionSet:

  • Добавьте свойство набора меток для проверки каждого значения метки на Я.

Расширение StrOptionSet:

  • Отфильтруйте те, которые не пересекаются.
  • Верните текст метки в виде массива.
  • Соединено с "," как CustomStringConvertible::description

Вот фрагмент:

protocol StrOptionSet : OptionSet, CustomStringConvertible {
    typealias Label = (Self, String)
    static var labels: [Label] { get }
}
extension StrOptionSet {
    var strs: [String] { return Self.labels
                                .filter{ (label: Label) in self.intersection(label.0).isEmpty == false }
                                .map{    (label: Label) in label.1 }
    }
    public var description: String { return strs.joined(separator: ",") }
}

Добавьте набор меток для целевого набора параметров VTDecodeInfoFlags.

extension VTDecodeInfoFlags : StrOptionSet {
    static var labels: [Label] { return [
        (.asynchronous, "asynchronous"),
        (.frameDropped, "frameDropped"),
        (.imageBufferModifiable, "imageBufferModifiable")
    ]}
}

Используй это

let flags: VTDecodeInfoFlags = [.asynchronous, .frameDropped]
print("flags:", flags) // output: flags: .asynchronous,frameDropped
person Chen OT    schedule 21.10.2019
comment
Хорошая реализация. Меня все еще беспокоит, что вам нужно поддерживать структуру, которая содержит все значения и их имена. Это та часть, которую я надеюсь избежать. - person Duncan C; 21.10.2019

Эта статья в NSHipster дает альтернативу OptionSet, которая предлагает все функции OptionSet, а также простое ведение журнала:

https://nshipster.com/optionset/

Если вы просто добавите требование, чтобы тип Option был CustomStringConvertible, вы можете очень аккуратно регистрировать наборы этого типа. Ниже приведен код с сайта NSHipster — единственным изменением является добавление соответствия CustomStringConvertible к классу Option.

protocol Option: RawRepresentable, Hashable, CaseIterable, CustomStringConvertible {}

enum Topping: String, Option {
    case pepperoni, onions, bacon,
    extraCheese, greenPeppers, pineapple

    //I added this computed property to make the class conform to CustomStringConvertible
    var description: String {
        return ".\(self.rawValue)"
    }
}

extension Set where Element == Topping {
    static var meatLovers: Set<Topping> {
        return [.pepperoni, .bacon]
    }

    static var hawaiian: Set<Topping> {
        return [.pineapple, .bacon]
    }

    static var all: Set<Topping> {
        return Set(Element.allCases)
    }
}

typealias Toppings = Set<Topping>

extension Set where Element: Option {
    var rawValue: Int {
        var rawValue = 0
        for (index, element) in Element.allCases.enumerated() {
            if self.contains(element) {
                rawValue |= (1 << index)
            }
        }
        return rawValue
    }
}

Затем с его помощью:

let toppings: Set<Topping> = [.onions, .bacon]

print("toppings = \(toppings), rawValue = \(toppings.rawValue)")

Это выводит

начинки = [.onions, .bacon], rawValue = 6

Так же, как вы этого хотите.

Это работает, потому что набор отображает свои элементы в виде списка с разделителями-запятыми в квадратных скобках и использует свойство description каждого члена набора для отображения этого члена. Свойство description просто отображает каждый элемент (имя перечисления как String) с префиксом ..

И поскольку rawValue Set<Option> совпадает с OptionSet с тем же списком значений, вы можете легко конвертировать их между собой.

Я бы хотел, чтобы Swift просто сделал это функцией родного языка для OptionSets.

person Duncan C    schedule 30.06.2019

Вот как я это сделал.

public struct Toppings: OptionSet {
        public let rawValue: Int
        
        public static let cheese = Toppings(rawValue: 1 << 0)
        public static let onion = Toppings(rawValue: 1 << 1)
        public static let lettuce = Toppings(rawValue: 1 << 2)
        public static let pickles = Toppings(rawValue: 1 << 3)
        public static let tomatoes = Toppings(rawValue: 1 << 4)
        
        public init(rawValue: Int) {
            self.rawValue = rawValue
        }
    }
    
    extension Toppings: CustomStringConvertible {
        
        static public var debugDescriptions: [(Self, String)] = [
            (.cheese, "cheese"),
            (.onion, "onion"),
            (.lettuce, "lettuce"),
            (.pickles, "pickles"),
            (.tomatoes, "tomatoes")
        ]
        
        public var description: String {
            let result: [String] = Self.debugDescriptions.filter { contains($0.0) }.map { $0.1 }
            let printable = result.joined(separator: ", ")
            
            return "\(printable)"
        }
    }
person active_sludge    schedule 18.12.2020
comment
Да, есть разные способы сделать это, когда вы создаете сопоставление между значениями и отображаемыми именами, но суть моего вопроса заключается в том, чтобы избежать этого. Он добавляет требование, чтобы вы сохраняли значения OptionSet и их отображаемые имена по мере роста вашего optionSet с течением времени. - person Duncan C; 18.12.2020

struct MyOptionSet: OptionSet {
    let rawValue: UInt
    static let healthcare   = MyOptionSet(rawValue: 1 << 0)
    static let worldPeace   = MyOptionSet(rawValue: 1 << 1)
    static let fixClimate   = MyOptionSet(rawValue: 1 << 2)
    static let exploreSpace = MyOptionSet(rawValue: 1 << 3)
}

extension MyOptionSet: CustomStringConvertible {
    static var debugDescriptions: [(Self, String)] = [
        (.healthcare, "healthcare"),
        (.worldPeace, "world peace"),
        (.fixClimate, "fix the climate"),
        (.exploreSpace, "explore space")
    ]

    var description: String {
        let result: [String] = Self.debugDescriptions.filter { contains($0.0) }.map { $0.1 }
        return "MyOptionSet(rawValue: \(self.rawValue)) \(result)"
    }
}

использование

var myOptionSet: MyOptionSet = []
myOptionSet.insert(.healthcare)
print("here is my options: \(myOptionSet)")
person neoneye    schedule 05.04.2020
comment
Конечно, вы можете вручную создать свойство описания, которое регистрирует значения, но перефразируя мой вопрос: ... я не могу придумать способ сделать это без добавления беспорядочного кода, который потребовал бы от меня ведения таблицы отображаемых имен для каждый флаг. - person Duncan C; 07.05.2021