如何以人类可读的形式显示OptionSet值?

17

Swift有OptionSet类型,基本上将集合操作添加到C风格的位标志中。苹果在其框架中广泛使用它们,例如animate(withDuration:delay:options:animations:completion:)中的选项参数。

好处是它使您可以使用干净的代码,例如:

options: [.allowAnimatedContent, .curveEaseIn]

然而,它也有缺点。

如果我想要显示 OptionSet 的特定值,似乎没有干净的方法:

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

显示非常不易懂的消息:

options = UIViewAnimationOptions(rawValue: 65664)

一些位字段的文档将常量表示为2的幂值:

flag0    = Flags(rawValue: 1 << 0)

但是,我的示例OptionSet——UIViewAnimationOptions的文档并没有告诉你关于这些标志的数字值,并且从十进制数中找出位并不简单。

问题:

是否有一种简洁的方法将OptionSet映射到所选值?

我想得到的输出应该像这样:

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

但我想不出一种方法来做到这一点,而不添加混乱的代码,这将要求我维护每个标志的显示名称表。

(我对在自己的代码中创建的系统框架和自定义OptionSets都感兴趣。)

枚举让您拥有枚举的名称和原始值,但它们不支持OptionSets提供的设置功能。



1
我认为这是控制台输出的一个缺陷。我希望名称能够出现在输出中。我不同意这仅限于选项集;例如,为什么UIApplicationState不能记录为.active? - 但是,我认为你和我一样知道这不是一个真正的问题;你知道没有答案,你只是在抱怨。你抱怨是对的,但这不是Stack Overflow应该处理的事情. :) - matt
@matt,实际上我希望有一些诀窍我还不知道,这样我就能跳起欢快的舞蹈,而不是抱怨。 - Duncan C
我在考虑将我的类设置为CustomStringConvertible,并编写代码来显示这些值,但这将很麻烦并需要手动维护。 - Duncan C
可能相关?为什么@objc枚举的描述与纯Swift枚举不同?。我认为要点在于UIViewAnimationOptions只是一个@objc OptionSet类型,它只是一个围绕着NS_OPTIONS位掩码的包装器。我认为这只是作为底层整数桥接到Swift中,没有任何底层元数据。 - JAL
使用 mirror,难道不能以通用的方式处理所有枚举类型吗? - Kartick Vaddadi
@VaddadiKartick 这篇文章似乎暗示这是可能的,但我在将其应用于UIKit OptionSet类型时遇到了麻烦。 - JAL
5个回答

4
这篇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)")

这段文字讲述了如何将一个枚举类型转换成集合类型,并展示了集合类型的输出方式。集合类型的成员会以逗号分隔的列表形式显示在方括号内,使用每个成员的`description`属性来显示该成员。`description`属性只是简单地以字符串形式显示每个项(即枚举的名称),并在前面加上一个点号。由于`Set

我们是否可以从rawValue中获取配料的方法?一个人可以将rawValue存储在数据库中,然后再检索出确切的配料。 - Master Stroke

3

以下是我的一种方法,使用字典并迭代键。虽然不太好,但它能起作用。

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

我不确定我喜欢“零”是0001的事实,应该是0。 - chikuba

3
这是我做到的方法。
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)"
        }
    }

是的,在创建值和显示名称之间的映射时,有各种方法可以实现这一点,但我的问题的关键在于避免这样做。这会增加一个要求,即您必须随着时间的推移维护OptionSet值及其显示名称。 - Duncan C
很遗憾,如果您正在使用具有Int作为原始值的OptionSet,则需要在某个地方维护字符串映射。具有Int原始值的OptionSet是一个数字,它不知道它应该是什么字符串。接受的答案仍然在进行映射,但使用具有String类型的enum来自动存储该映射。 - LightningStryk
是的。所以对于我的问题,“是否有一种简洁的方式将OptionSet映射到所选值,而不需要添加会要求我维护每个标志的显示名称表的混乱代码”?答案是否定的。 - Duncan C

3

StrOptionSet 协议:

  • 添加一个标签集合属性,以测试每个标签值在 Self 上的情况。

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

1
不错的实现。但是让我感到困扰的是,你必须维护一个包含所有值及其名称的结构。这就是我希望避免的部分。 - Duncan C

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

当然,您可以手动创建一个描述属性来记录值,但是换句话说我的问题是:“……我想不出一种方法来做到这一点,而不需要添加混乱的代码,这将要求我维护每个标志的显示名称表。” - Duncan C

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接