【问题标题】:Enum multiple cases with the same value in Swift在Swift中枚举具有相同值的多个案例
【发布时间】:2015-01-20 04:30:14
【问题描述】:

在 C 中,你可以让你的枚举有这个:

typedef enum _Bar {
    A = 0,
    B = 0,
    C = 1
} Bar;

在 Swift 中,我想做一个等价的。但是,编译器抱怨它不是唯一的。如何告诉它我希望两个案例具有相同的值?

enum Bar : Int {
    case A = 0
    case B = 0 // Does not work
    case C = 1
}

我试过了

case A | B = 0

case A, B = 0

但它似乎并没有像我想要的那样工作。

【问题讨论】:

    标签: swift enums


    【解决方案1】:

    Swift 不支持重复值(或语义上的“别名”)。如果你不介意,你可以使用类似这样的东西来模仿它:

    enum Foo: Int {
        case Bar = 0
    
        static var Baz:Foo {
            get {
                return    Bar
            }
        }
        static var Jar:Foo {
            get {
                return    Foo(rawValue: 0)!
            }
        }
    }
    

    使用最新版本的 Swift,可以这样缩短:

    enum Foo: Int {
        case bar = 0
    
        static var baz:Foo { .bar }
        static var jar:Foo { Foo(rawValue: 0)! }
    }
    

    请注意,Swift 已将枚举变量的命名约定从 PascalCase 更改为 camelCase

    【讨论】:

    • 或者,如果既没有指定get 也没有指定set,则属性隐含有getter 代码,您可以将它们缩短为static var Baz:Foo { return Bar }static var Jar:Foo { return Foo(rawValue: 0)! }
    • 或者,似乎在 Swift 3 中,存储的类属性可以工作,所以只需 static let Baz = Foo.Barstatic let Jar = Foo(rawValue: 0)!
    • @SlippD.Thompson 我认为使用let 会占用额外的内存来存储副本并为存储创建不同的身份。因此理论上略有不同。实际上,我不确定编译器将如何使用它。
    • @0-1 据我所知,几年前这是不可能的,现在看来有些事情已经改变了。这是有意设计的吗?
    • @eonil 我不确定这是否是故意的。我会对此做一些研究。我很确定文档仍然说它不好:“原始值可以是字符串、字符或任何整数或浮点数类型。每个原始值在其枚举声明中必须是唯一的。”摘自:Apple Inc. “Swift 编程语言 (Swift 5.2)。”苹果图书。 books.apple.com/us/book/…
    【解决方案2】:

    这是绕过它的另一种方法:

    enum Animal {
    
      case dog
      case cat
      case mouse
      case zebra
    
      var description: String {
        switch self {
        case .dog:
          return "dog"
    
        case .cat:
          return "dog"
    
        case .mouse:
          return "dog"
    
        case .zebra:
          return "zebra"
    
        default:
          break
        }
      }
    }
    

    【讨论】:

      【解决方案3】:

      Swift 不允许 enum 的元素共享值。从“原始值”标题下的枚举文档(强调我的):

      原始值可以是字符串、字符或任何整数或浮点数类型。 每个原始值在其枚举声明中必须是唯一的。

      【讨论】:

      • 如果你想要的话,你总是可以创建常量类成员来构造别名值——它们可以用在构造函数和赋值中,但不能用在 switch 语句中。
      【解决方案4】:

      我不确定你能做到。以下内容来自苹果。

      “与 C 和 Objective-C 不同,Swift 枚举成员在创建时不会被分配默认整数值。在上面的 CompassPoints 示例中,North、South、East 和 West 并不隐式等于 0、1、2 和 3。相反,不同的枚举成员本身就是完全成熟的值,具有明确定义的 CompassPoint 类型。 ”

      摘自:Apple Inc. “The Swift Programming Language”。电子书。 https://itunes.apple.com/us/book/swift-programming-language/id881256329?mt=11

      由于枚举成员不隐式等于 0、1 等,编译器将每个成员视为唯一值。当您尝试复制它时,编译器会抱怨,因为它已经被创建了。

      【讨论】:

      • 是的,我想使用枚举来表示索引路径。不同的部分将有一个枚举,并且行名将与行索引相关联。行索引可以是相同的值,但有不同的部分。
      • 听起来你想要一个结构,而不是枚举。
      【解决方案5】:

      我遇到了关于具有相同值的多个案例的相同问题。在谷歌搜索这个问题后,我更喜欢computed propertiesenumwhich come from this post 的以下解决方案。


      【讨论】:

        【解决方案6】:

        我如何告诉它我希望两个案例具有相同的值?

        我将证明这对于所有 Swift 版本都是可能的

        不是解决方法。这实际上是可能的。尽管他们说不是。

        这确实回答了你的问题:

        enum Foo: Bool {
            case bar = 0
            case bas = 1
            case bat = 2
        }
        
        // THIS IS THE KEY!!
        extension Bool: ExpressibleByIntegerLiteral {
            public init(integerLiteral value: Int) { self = true }
        }
        
        func bug() {
            assert(Foo.bar == Foo.bas) // This assertion is legitimate!
            assert(Foo.bar.rawValue == Foo.bas.rawValue) // This assertion is legitimate!
        }
        

        令人难以置信的是,枚举 Foo 上的所有案例都有一个原始值 true。您绝对可以重新调整它以支持更多的重复类型,而不仅仅是 Bool。

        想象一下所有的影响.. Set、Dictionary、switch 语句、CaseIterable 等。


        我想我必须在 bugs.swift.org 中提交一个 Swift Radar...或者我们可以保守这个小秘密

        您可以关注SR-13212 以获取有关此错误修复的更新。

        编辑: - 他们将其保留为一项功能。


        答案

        您提到您想要一个原始值为0, 0, and 1 的枚举。在这里!

        enum Bar: Int {
            case A = "0"
            case B = "0 "
            case C = "1"
        }
        
        extension Int: ExpressibleByStringLiteral {
            public init(stringLiteral elements: String) {
                self = Int(String(elements.first ?? Character("0"))) ?? 0
            }
        }
        

        此扩展获取每个 String 的第一个字符并将其转换为 Int。因此,使原始值真正 0, 0, and 1

        真实陈述:Bar.A == Bar.B

        它目前适用于所有 Swift 版本。


        编辑:异常

        这里有另一个提示,为什么我认为这是一个错误。 Double 符合 ExpressibleByIntegerLiteral。但是,这段代码不会编译:

        enum Foo: Double {
            case a = 1.0
            case b = 1
        }
        

        并试图覆盖它会导致此错误:

        • 警告:Double 与协议ExpressibleByIntegerLiteral 的一致性已在类型模块Swift 中说明

        Swift 工程师以某种方式阻止了这一点,但其他一切都很好。

        【讨论】:

          【解决方案7】:

          我完全是巧合地发现了这一点,并且很惊讶在任何答案中都没有 Swift-y 的黑魔法(当然,这仍然很好)。就我个人而言,我不太喜欢 static 属性,因为它们在其他地方引入了开销,例如在模式匹配中。

          我认为“最干净”的解决方案是依靠 RawRepresentable 并自己定义。然后你可以反驳 Swift 不支持这个开箱即用的主要原因:init?(rawValue:) 方法。通过仅将 String 指定为采用的原始值(即enum MyEnum: String { ... }),编译器无法自行生成逻辑。它需要知道要跳过哪种情况,因为没有定义“足够”的潜在原始值(因为一个或多个加倍)。自己采用协议允许您简单地选择一种情况作为给定原始值字符串的“默认值”,并基本上防止用原始值构造另一种情况(当然,常规构造仍然有效)。

          不幸的是,这也是一些开销,因为前者 case myCase = "its_raw_value" 然后被分发到

          • 常规情况(没有原始值字符串):case myCase(定义)
          • init?(rawValue:) 方法中的一个开关:... case "its_raw_value": return .myCase ...
          • 在所需的rawValue 属性中的一个开关:... case .myCase: return "its_raw_value" ...

          我认为我有一个很好的方法来减少一点,并防止原始值过多地浮动。我写了一个 Playground 来说明,这里简单粘贴一下:


          // define protocol and a default implementation
          protocol SloppyRawRepresentable
          where Self: RawRepresentable, Self.RawValue: Hashable, Self: CaseIterable {
          }
          extension SloppyRawRepresentable {
              init?(rawValue: RawValue) {
                  var tempMapping = [RawValue: Self]()
                  for oneCase in Self.allCases {
                      // first come first served. any case after the first having the same
                      // raw value is simply ignored, so the first is the "default"
                      if tempMapping[oneCase.rawValue] == nil {
                          tempMapping[oneCase.rawValue] = oneCase
                      }
                  }
                  guard let candidate = tempMapping[rawValue] else { return nil }
                  self = candidate
              }
          }
          
          // use it. Note we don't need the init
          enum EventNames: SloppyRawRepresentable {
              typealias RawValue = String
          
              var rawValue: String {
                  switch self {
                  case .standardEvent: return "standard"
                  case .joesConfig: return "iAmJoe"
                  case .myConfig: return "iAmJoe"
                  }
              }
          
              case standardEvent
              case joesConfig
              case myConfig
          }
          
          // some example output
          
          print(EventNames.standardEvent)
          print(EventNames.joesConfig)
          print(EventNames.myConfig)
          
          print(EventNames.standardEvent.rawValue)
          print(EventNames.joesConfig.rawValue)
          print(EventNames.myConfig.rawValue)
          
          print(EventNames(rawValue: "standard")!)
          print(EventNames(rawValue: "iAmJoe")!)
          print(EventNames(rawValue: "iAmJoe")!)
          
          print(EventNames(rawValue: "standard")!.rawValue)
          print(EventNames(rawValue: "iAmJoe")!.rawValue)
          print(EventNames(rawValue: "iAmJoe")!.rawValue)
          

          它应该是不言自明的。案例定义并为其分配原始值仍然是分裂的,我认为没有办法解决这个问题,但它可以派上用场。当然,所有这些都需要 RawValue 是约束,我依靠枚举是 CaseIterable 来轻松构建该帮助字典,但我认为这应该没问题,性能明智。

          【讨论】:

            【解决方案8】:

            免责声明:我不建议这样做。大多数人不会考虑最佳实践。有缺点,具体取决于应用程序。我们只是说这些想法是理论的、假设的、不纯的,只是为了思考而提出的

            一种可能性是添加某种无意义的微分器,足以阻止 Swift 的身份检查,这不会影响您对代码中的原始值所做的事情。

            他是一个Float 的例子,在音乐中,A♯(升A)与B♭(降B)频率相同。换句话说,它们都是同一事物的不同注释。

            以 microHz 为单位测量的差异对音频引擎没有意义,但足以区分 Swift 枚举中的 case 值。也就是说,实际上,在大多数音乐应用中,116.540 和 116.540001 没有区别:

            enum NoteFrequency : Float = {
               case A = 110.0, A_SHARP = 116.540, B_FLAT = 116.540001
            }
            

            从准容忍到丑陋:对于只被分配和打印的字符串,您可以添加一个不可打印的字符来区分。

            显然这有缺点(可能很严重),如果你做了这样的事情,后来你(或其他人)可能不知道你做了什么,并编写了不考虑它的代码(例如在 rawValue 中使用字符串比较中的枚举)并且它们的代码失败。要在字符串比较中进行这项工作,您可能必须做一些事情来忽略最后一个字符,或者在比较之前用myEnumString.rawValue.dropLast() 将其标记为不可打印的字符...

             enum Animals : String = {
                 case CAT = "Feline\u{01}", LION = "Feline\u{02}"
             }
            

            【讨论】:

              猜你喜欢
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 2021-09-25
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              相关资源
              最近更新 更多