【问题标题】:OptionSet with associated value for each optionOptionSet 与每个选项的关联值
【发布时间】:2017-08-31 16:26:25
【问题描述】:

这可能最好用一个用例来解释。

我有一个记录器类。它将消息记录到各个级别的输出。

class Logger {
   var outputs: OutputOptions
   var filter: Level

   func log(_ message: String, at level: Level) {
      if level <= self.filter {
         outputs.log(message)
      }
   }
}

可能的输出由 OptionSet 定义,并确定应将消息记录到哪些输出(NSLog、Instabug 等)。 OptionSet 在这里很好,因为我可以定义选择多个输出,并在记录时轻松检查选择了哪些输出。

struct OutputOptions: OptionSet {
    let rawValue: Int

    static let console = OutputOptions(1 << 0)
    static let instabug = OutputOptions(1 << 1)

    func log(_ message: String) {
       if self.contains(.console) {
          NSLog(message)
       }

       // etc
    }
}

级别由枚举定义,表示消息的级别,例如错误、警告、信息等。如果我们对获取详细输出不感兴趣,记录器可以过滤掉某个级别以上的消息。记录器的过滤器也设置为一个级别。

enum Level: Int {none, no logs are shown. */
   case none = 0
   case error = 1
   case warning = 2
   case info = 3
   case verbose = 4
}

我想以某种方式组合输出选项和级别,允许我指定某些输出可以将消息过滤到某个级别,而其他输出可以过滤到其他级别。例如,我想将详细消息记录到控制台,但只将错误记录到 Instabug。从表面上看,OptionSets 看起来像可组合的枚举,所以我立即想到了相关的值。如果每个选项都有一个关联的过滤级别,我可以像这样设置记录器的输出:

let logger = Loggger()
logger.outputs = [.console(filter: .verbose), .instabug(filter: .error)]

为了让它工作,我向 OutputOptions 添加了一个过滤器属性。我的选项现在看起来像这样:

struct OutputOptions: OptionSet {
   let rawValue: Int
   var filter: Level = .info

   init(rawValue: Int, filter: Level) {
      self.rawValue = rawValue
      self.filter = filter
   }

   static func console(filter: Level = .info) {
      return OutputOptions(rawValue: 1 << 0, filter: filter)
   }

   // etc

但我不知道如何访问log 中元素的过滤器变量。根据我对枚举的经验,我希望能够做到

   func log(_ message: String, at level: Level) {
      if self.contains(.console(let filter)) { // <== does not compile!
         if level <= filter {
            NSLog(message)
         }
      }
   }
}

但这不会编译。事实上,filter 属性似乎不是为每个选项单独定义的,而是为整个选项集定义的。

SO:有没有办法将值与选项集中的各个选项相关联?

【问题讨论】:

    标签: swift


    【解决方案1】:

    但这不会编译。实际上,看起来 filter 属性并不是为每个选项单独定义的,而是为整个选项集定义的。

    这是因为 OptionSet 本身并不是真正的 set。如果我有以下选项集:

    struct MyOptions: OptionSet {
        let rawValue: Int
    
        static let foo = MyOptions(1 << 0)
        static let bar = MyOptions(1 << 1)
    }
    

    然后我这样设置:

    let opts: MyOptions = [.foo, .bar]

    我实际上没有包含两个 MyOptions 实例的集合。相反,我有一个 MyOptions 的新实例,其 rawValue 设置为 (.foo.rawValue | .bar.rawValue),即3、原来的两个MyOptions实例一生成opts就被丢弃。

    同样,您的logger.outputs 将是OutputOptions 的一个实例,其中rawValue 3 和filter 的默认值。

    因此,用OptionSet 做你想做的事是不可能的。

    【讨论】:

    • 有道理。也许我应该放弃 OptionSet 的好处,然后让我的选项成为枚举数组。
    【解决方案2】:

    这实际上不是真的,OptionSet 可以有关联的值,但它需要一些工作。幸运的是没有那么多,这里是一个简单的示例。

    这是 OptionSetIterator,实际上并不需要它,但有助于演示,它来自 here

    public struct OptionSetIterator<Element: OptionSet>: IteratorProtocol where Element.RawValue == Int {
        
        private let value: Element
        private lazy var remainingBits = value.rawValue
        private var bitMask = 1
    
        public init(element: Element) {
            self.value = element
        }
            
        public mutating func next() -> Element? {
            while remainingBits != 0 {
                defer { bitMask = bitMask &* 2 }
                if remainingBits & bitMask != 0 {
                    remainingBits = remainingBits & ~bitMask
                    return Element(rawValue: bitMask)
                }
            }
            return nil
        }
        
    }
    
    extension OptionSet where Self.RawValue == Int {
        
        public func makeIterator() -> OptionSetIterator<Self> { OptionSetIterator(element: self) }
        
    }
    

    下面是带有相关值的 OptionSet 的实际代码示例:

    public struct MyOptions: OptionSet, Equatable, Sequence, CustomStringConvertible {
        
        public let rawValue: Int
        public fileprivate(set) var tag: Int?
        public fileprivate(set) var text: String?
            
        mutating public func formUnion(_ other: __owned MyOptions) {
            self = Self(rawValue: self.rawValue | other.rawValue, tag: other.tag ?? self.tag, text: other.text ?? self.text)
        }
    
        @discardableResult
        public mutating func insert(
            _ newMember: Element
        ) -> (inserted: Bool, memberAfterInsert: Element) {
            let oldMember = self.intersection(newMember)
            let shouldInsert = oldMember != newMember
            let result = (
                inserted: shouldInsert,
                memberAfterInsert: shouldInsert ? newMember : oldMember)
            if shouldInsert {
                self.formUnion(newMember)
            } else {
                self.tag = newMember.tag ?? self.tag
                self.text = newMember.text ?? self.text
            }
            return result
        }
        
        @discardableResult
        public mutating func remove(_ member: Element) -> Element? {
            let intersectionElements = intersection(member)
            guard !intersectionElements.isEmpty else {
                return nil
            }
            let tag: Int? = self.tag
            let text: String? = self.text
            self.subtract(member)
            self.tag = tag
            self.text = text
            return intersectionElements
        }
        
        private init(rawValue: Int, tag: Int?, text: String?) {
            self.rawValue = rawValue
            self.tag = tag
            self.text = text
        }
        
        public init(rawValue: Int) {
            self.rawValue = rawValue
            self.tag = nil
            self.text = nil
        }
    
        private static var _tag: Int { 1 << 0 }
        private static var _text: Int { 1 << 1 }
    
        public static func tag(_ value: Int) -> MyOptions {
            MyOptions(rawValue: _tag, tag: value, text: nil)
        }
    
        public static func text(_ value: String) -> MyOptions {
            MyOptions(rawValue: _text, tag: nil, text: value)
        }
        
        public var description: String {
            var modes: [String] = []
            self.forEach {
                var text: String = ""
                switch $0.rawValue {
                    case MyOptions._tag: text = "tag{" + ( self.tag?.description ?? "nil" ) + "}"
                    case MyOptions._text: text = "text=" + ( self.text ?? "nil" )
                    default: text = "unknown"
                }
                modes.append(text)
            }
            guard !modes.isEmpty else { return "none" }
            guard modes.count > 1 else { return modes.first ?? "error" }
            return "[" + modes.joined(separator: ", " ) + "]"
        }
    
    }
    

    在这个示例中,MyOptions 不需要符合 Equatable 和 Sequence,它们是用于 OptionSetIterator 的。此外,CustomStringConvertible 仅用于演示目的,变量描述也是如此。

    这里有一些测试:

    var options: MyOptions = [
    .text("hello"),
    .text("world"),
    .tag(10)
    ]
    
    options.insert(.tag(9))
    options.update(with: .tag(0))
    options.remove(.tag(-1))
    
    print("Options: " + options.description)
    

    结果:

    选项:文本=世界

    当从集合中移除成员时,它的关联值会被保留。通过小的更改,它们也可以在成员离开集合时被清除,但是如果您没有在集合的关联类型中存储大量数据......我通常使用 OptionSet 作为我的类和结构的配置器,以及可选选项,所以我很少从集合中移除成员 - 所以我只是出于懒惰而选择退出..

    这是覆盖默认方法的微小更改。 OptionSet 的原始源代码在github.com

    有什么 cmets 或改进..?

    【讨论】:

      【解决方案3】:

      其实……为了回答我自己的问题……我写了一些东西……

      这是协议 OptionSetAssociated 及其扩展:

      public protocol OptionSetAssociated: OptionSet where RawValue: BinaryInteger {
          var store: [RawValue: Any] { get set }
      }
      
      extension OptionSetAssociated {
          
          public init<T>(rawValue: RawValue, value: T) {
              self.init(rawValue: rawValue)
              self.store[rawValue] = value
          }
          
          fileprivate init(rawValue: RawValue, store: [RawValue: Any]) {
              self.init(rawValue: rawValue)
              self.store = store
          }
          
          fileprivate static func combinedStore(_ old: [RawValue: Any], new: [RawValue: Any]) -> [RawValue: Any] {
              new.map {$0.key}.reduce(into: old) {
                  $0[$1] = new[$1] ?? old[$1]
              }
          }
      
          fileprivate static func storeOverride(_ store: [RawValue: Any], member: RawValue?, value: Any?) -> [RawValue: Any] {
              guard let member: RawValue = member else { return store }
              var store: [RawValue: Any] = store
              store[member] = value
              return store
          }
      
          public func getValue<T>(key: RawValue) -> T? {
              self.store[key] as? T
          }
          
          mutating public func formUnion(_ other: __owned Self) {
              self = Self(rawValue: self.rawValue | other.rawValue, store: Self.combinedStore(self.store, new: other.store))
          }
      
      }
      
      extension OptionSet where Self: OptionSetAssociated, Self == Element {
          
          @discardableResult
          public mutating func insert(
              _ newMember: Element
          ) -> (inserted: Bool, memberAfterInsert: Element) {
              let oldMember = self.intersection(newMember)
              let shouldInsert = oldMember != newMember
              var result = (
                  inserted: shouldInsert,
                  memberAfterInsert: shouldInsert ? newMember : oldMember)
              if shouldInsert {
                  self.formUnion(newMember)
              } else {
                  self.store = Self.storeOverride(
                      Self.combinedStore(self.store, new: newMember.store),
                      member: newMember.rawValue, value: newMember.store[newMember.rawValue])
                  result.memberAfterInsert.store[newMember.rawValue] = newMember.store[newMember.rawValue]
              }
              return result
          }
      
          @discardableResult
          public mutating func remove(_ member: Element) -> Element? {
              var intersectionElements = intersection(member)
              guard !intersectionElements.isEmpty else {
                  return nil
              }
              let store: [RawValue: Any] = self.store
              self.subtract(member)
              self.store = store
              self.store[member.rawValue] = nil
              intersectionElements.store = Self.storeOverride([:], member: member.rawValue, value: store[member.rawValue])
              return intersectionElements
          }
          
          @discardableResult
          public mutating func update(with newMember: Element) -> Element? {
              let previousValue: Any? = self.store[newMember.rawValue]
              var r = self.intersection(newMember)
              self.formUnion(newMember)
              self.store[newMember.rawValue] = newMember.store[newMember.rawValue]
              if r.isEmpty { return nil } else {
                  r.store = Self.storeOverride([:], member: newMember.rawValue, value: previousValue)
                  r.store[newMember.rawValue] = previousValue
                  return r
              }
          }
          
      }
      

      这是一个结构,它使用它并像普通的 OptionSet 一样工作..

      public struct TestSet: OptionSetAssociated {
          
          public typealias RawValue = Int
      
          public let rawValue: Int
          public var store: [RawValue : Any] = [:]
          
          public init(rawValue: RawValue) {
              self.rawValue = rawValue
          }
              
      }
      
      extension TestSet { // Members
          
          public static var bool: TestSet { TestSet(rawValue: 1 << 0) }
          public static var int: TestSet { TestSet(rawValue: 1 << 1) }
          public static var string: TestSet { TestSet(rawValue: 1 << 2) }
          public static var optString: TestSet { TestSet(rawValue: 1 << 3) }
          
          public static func int(_ value: Int) -> TestSet {
              TestSet(rawValue: TestSet.int.rawValue, value: value)
          }
          
          public static func string(_ value: String) -> TestSet {
              TestSet(rawValue: TestSet.string.rawValue, value: value)
          }
          
          public static func optString(_ value: String?) -> TestSet {
              TestSet(rawValue: TestSet.optString.rawValue, value: value)
          }
          
      }
      
      extension TestSet { // Member options
          
          public var int: Int? {
              self.getValue(key: TestSet.int.rawValue) ?? ( self.contains(TestSet.int) ? Int() : nil ) // Returns default Int() if member is included and no value was given...
          }
      
          public var string: String? {
              self.getValue(key: TestSet.string.rawValue) ?? ( self.contains(TestSet.string) ? String() : nil )
          }
      
          public var optString: String? { // Returns nil when value was not given
              self.getValue(key: TestSet.optString.rawValue)
          }
      
      }
      

      这里是一些调试的扩展(我选择了以前对迭代器的依赖):

      extension TestSet: CustomStringConvertible {
          
          public var description: String {
              var members: [String] = []
              var vars: [String] = []
      
              if self.contains(TestSet.bool) { members.append("bool" ) }
              if self.contains(TestSet.int) { members.append("int") }
              if self.contains(TestSet.string) { members.append("string") }
              if self.contains(TestSet.optString) { members.append("Optional<String>") }
              
              if let int: Int = self.int { vars.append("Int(" + int.description + ")") }
              if let string: String = self.string { vars.append("String(" + string + ")") }
              if let optString: String = self.optString { vars.append("Optional<String>(" + optString + ")")}
                      
              return "Members: " + (members.isEmpty ? ["none"] : members).joined(separator: ", ") + "\nVariables: " + vars.joined(separator: ", ")
          }
          
      }
      

      试试看。初始化就像普通的 OptionSet..

      var testset: TestSet = []
      testset = [.int, .string("hello")]
      testset.remove(.int)
      testset.update(with: .string("world"))
      testset.insert(.int(10))
      
      print(testset.description)
      

      结果:

      成员:整数、字符串 变量:Int(10), String(world)

      因此,在这个版本中,每个成员都可以使用一个值(除非它是一个元组..)——当成员离开集合时,它会被删除。值保存在变量存储中,这是一个以 RawValue 为关键元素的字典。因为我使用字典而不是 KeyValuePairs - 要求 RawValue 符合 Hashable(BinaryInteger)。

      在示例中,如果没有为不使用可选值的成员提供值,则返回默认值,然后有一个成员具有可选值。也是一个毫无价值的传统成员。

      创建与普通的 OptionSet 相同,不同之处在于:

      public var store: [RawValue : Any] = [:]
      

      不幸的是,我确信我忘记考虑一些事情,这使得我的实现仍然不完美,尽管它并不重要或太重要.. 我刚刚在我的脑海里有它,但失去了它在那之后,它来得很快:)

      【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2013-08-21
      • 2016-06-14
      • 2021-12-02
      • 2016-02-23
      • 2018-02-10
      • 2012-06-27
      • 1970-01-01
      • 2012-01-15
      相关资源
      最近更新 更多