iOS - Swift Enum 枚举

前言

  • 枚举声明的类型是囊括可能状态的有限集,且可以具有附加值。通过内嵌(nesting),方法(method),关联值(associated values) 和模式匹配(pattern matching) 枚举可以分层次地定义任何有组织的数据。

  • 和 switch 语句类似,Swift 中的枚举乍看之下更像是 C 语言中枚举的进阶版本,即允许你定义一种类型,用于表示普通事情中某种用例。不过深入挖掘之后,凭借 Swift 背后特别的设计理念,相比较 C 语言枚举来说其在实际场景中的应用更为广泛。特别是作为强大的工具,Swift 中的枚举能够清晰表达代码的意图。

  • 在通常情况下,枚举是很容易进行相等性判断的。一个简单的 enum T { case a, b } 实现默认支持相等性判断 T.a == T.b, T.b != T.a。然而,一旦我们为枚举增加了关联值,Swift 就没有办法正确地为两个枚举进行相等性判断,需要我们自己实现 == 运行符。

1、枚举的创建

1.1 标准定义, 基本枚举类型

  • 枚举的定义

    • 多个成员值的定义写在一行上时,需使用逗号分隔。

          /// 试想我们正在开发一款游戏,玩家能够朝四个方向移动。玩家的运动轨迹受到了限制,
          /// 我们能够使用枚举来表述这一情况
          enum Movement {
      
              case left
              case right
              case top
              case bottom
          }
      
          enum CompassPoint {
      
              case north, south, east, west
          }
  • 枚举的使用

    • 使用时,我们可以无须明确指出 enum 的实际名称(即 case Move.left: print("Left"))。因为类型检查器能够自动为此进行类型推算。这对于那些 UIKit 以及 AppKit 中错综复杂的枚举是非常有用的。

          /// 如果 switch 的条件声明在同一个函数内,这时会提示 Switch condition evaluates to a constant,
          /// 要去除这个,只需要将声明的变量放在函数外就可以
          let aMovement = Movement.left
      
          /// aDirection 的类型是已知的,所以在设定它的值时,可以不写该类型
          let aDirection: CompassPoint = .south
    • 可以使用多种模式匹配结构获取到枚举的值,或者按照特定情况执行操作。

          /// switch 分情况处理
          switch aMovement {              // print left
      
              case .left:
                  print("enum1 left")
      
              case .right:
                  print("enum1 right")
      
              default:
                  break
          }
      
          switch aDirection {             // print Watch out for penguins
      
              case .east:
                  print("Where the sun rises")
      
              case .west:
                  print("Where the skies are blue")
      
              case .south:
                  print("Watch out for penguins")
      
              case .north:
                  print("Lots of planets have a north")
          }
      
          /// 明确的 case 情况
          if case .left = aMovement {     // print left
              print("enum1 left")
          }
      
          /// 条件语句特定情况判断
          if aMovement == .left {         // print left
              print("enum1 left")
          }

1.2 原始值定义,枚举值

  • 枚举的定义

    • 当然,你可能想要为 enum 中每个 case 分配一个值。这相当有用,比如枚举自身实际与某事或某物挂钩时,往往这些东西又需要使用不同类型来表述。在 C 语言中,你只能为枚举 case 分配整型值,而 Swift 则提供了更多的灵活性。

    • Swift 枚举中支持以下四种关联值类型:
      • 整型 (Integer)
      • 浮点数 (Float Point)
      • 字符串 (String)
      • 布尔类型 (Boolean)
    • 因此你无法为枚举分配诸如 CGPoint 类型的值。

          /// 映射到整型
          enum Seasons: Int {
      
              case spring = 0
              case summer = 1
              case autumn = 2
              case winter = 3
          }
      
          /// 映射到 float double, 注意枚举中的花式 unicode
          enum Constants: Double {
      
              case π = 3.14159
              case e = 2.71828
              case φ = 1.61803398874
              case λ = 1.30357
          }
      
          /// 映射到字符串
          enum House: String {
      
              case baratheon = "Ours is the Fury"
              case greyjoy   = "We Do Not Sow"
              case martell   = "Unbowed, Unbent, Unbroken"
              case stark     = "Winter is Coming"
              case tully     = "Family, Duty, Honor"
              case tyrell    = "Growing Strong"
          }
    • 对于 StringInt 类型来说,甚至可以忽略为枚举中的 case 赋值,Swift 编译器也能正常工作。

          /// Planet 枚举中 mercury = 1, venus = 2, ... neptune = 8
          enum Planet: Int {
      
              case mercury = 1, venus, earth, mars, jupiter, saturn, uranus, neptune
          }
      
          /// CompassPointStr 枚举中 north = "north", ... west = "west"
          enum CompassPointStr: String {
      
              case north, south, east, west
          }
    • 如果想要以底层 C 二进制编码形式呈现某物或某事,使得更具可读性,可以看一下 BSD kqeue library 中的 VNode Flags 标志位的编码方式,如此便可以使你的 Delete 或 Write 用例声明一目了然,稍后一旦需要,只需将 raw value 传入 C 函数中即可。

          enum VNodeFlags: UInt32 {
      
              case delete   = 0x00000001
              case write    = 0x00000002
              case extended = 0x00000004
              case attrib   = 0x00000008
              case link     = 0x00000010
              case rename   = 0x00000020
              case revoke   = 0x00000040
              case none     = 0x00000080
          }
  • 枚举的使用

    • 如果想要读取枚举的值,可以通过 rawValue 属性来实现。

    • 如果想要通过一个已有的 raw value 来创建一个 enum case。这种情况下,枚举提供了一个指定构造方法来实现。
    • 倘若使用 rawValue 构造器,切记它是一个可失败构造器 (failable initializer)。换言之,构造方法返回值为可选类型值,因为有时候传入的值可能与任意一个 case 都不匹配。比如 Seasons(rawValue: 10)

          let bestHouse = House.stark
      
          /// 通过构造方法来实现
          let springSeasons: Seasons? = Seasons(rawValue: 0)
      
          /// 通过 rawValue 属性来实现
          let houseValue = bestHouse.rawValue
      
          print(houseValue)                        // print Winter is Coming
      
          print(String(describing: springSeasons)) // print Optional(Swift_Enum.Enum.Seasons.spring)

1.2.1 使用自定义类型作为枚举的值

  • 如果我们忽略关联值,则枚举的值就只能是整型,浮点型,字符串和布尔类型。如果想要支持别的类型,则可以通过实现 ExpressibleByStringLiteral 协议(Swift 4 之前名称为 StringLiteralConvertible)来完成,这可以让我们通过对字符串的序列化和反序列化来使枚举支持自定义类型。

        /// 作为一个例子,假设我们要定义一个枚举来保存不同的 iOS 设备的屏幕尺寸
        enum Devices: CGSize {
    
            case iPhone3GS   = CGSize(width: 320, height: 480)
            case iPhone5     = CGSize(width: 320, height: 568)
            case iPhone6     = CGSize(width: 375, height: 667)
            case iPhone6Plus = CGSize(width: 414, height: 736)
        }
    • 然而,这段代码不能通过编译。因为 CGPoint 并不是一个常量,不能用来定义枚举的值。

    • 我们需要为想要支持的自定义类型增加一个扩展,让其实现 ExpressibleByStringLiteral 协议。这个协议要求我们实现三个构造方法,这三个方法都需要使用一个 String 类型的参数,并且我们需要将这个字符串转换成我们需要的类型(此处是 CGSize)。

          extension CGSize: ExpressibleByStringLiteral {
      
              public init(stringLiteral value: String) {
      
                  let size = CGSizeFromString(value)
                  self.init(width: size.width, height: size.height)
              }
      
              public init(extendedGraphemeClusterLiteral value: String) {
      
                  let size = CGSizeFromString(value)
                  self.init(width: size.width, height: size.height)
              }
      
              public init(unicodeScalarLiteral value: String) {
      
                  let size = CGSizeFromString(value)
                  self.init(width: size.width, height: size.height)
              }
          }
    • 现在就可以来实现我们需要的枚举了,不过这里有一个缺点:初始化的值必须写成字符串形式,因为这就是我们定义的枚举需要接受的类型(记住,我们实现了 ExpressibleByStringLiteral,因此 String 可以转化成 CGSize 类型)。

          enum Devices: CGSize {
      
              case iPhone3GS   = "{320, 480}"
              case iPhone5     = "{320, 568}"
              case iPhone6     = "{375, 667}"
              case iPhone6Plus = "{414, 736}"
          }
    • 终于,我们可以使用 CGPoint 类型的枚举了。需要注意的是,当要获取真实的 CGPoint 的值的时候,我们需要访问枚举的是 rawValue 属性。

          let a = Devices.iPhone5
          let b = a.rawValue
      
          print("the phone size string is \(a), width is \(b.width), height is \(b.height)")
          // print the phone size string is iPhone5, width is 320.0, height is 568.0
  • 使用字符串序列化的形式,会让使用自定义类型的枚举比较困难,然而在某些特定的情况下,这也会给我们增加不少便利(比较使用NSColor / UIColor 的时候)。不仅如此,我们完全可以对自己定义的类型使用这个方法。

1.3 嵌套定义

  • 枚举的定义

    • 如果有特定子类型的需求,可以对 enum 进行嵌套。这样就允许为实际的 enum 中包含其他明确信息的 enum。

          /// 以 RPG 游戏中的每个角色为例,每个角色能够拥有武器,因此所有角色都可以获取同一个武器集合。
          /// 而游戏中的其他实例则无法获取这些武器(比如食人魔,它们仅使用棍棒)
          enum Character {
      
              enum Weapon {
      
                  case bow
                  case sword
                  case lance
                  case dagger
              }
      
              enum Helmet {
      
                  case wooden
                  case iron
                  case diamond
              }
      
              case thief
              case warrior
              case knight
          }
  • 枚举的使用

    • 可以通过层级结构来获取枚举的值

          /// 可以通过层级结构来描述角色允许访问的项目条
          let character = Character.thief
          let weapon = Character.Weapon.bow
          let helmet = Character.Helmet.iron
      
          print(character)            // print thief
          print(weapon)               // print bow
          print(helmet)               // print iron

1.4 包含定义

  • 枚举的定义

    • 能够在 structs 或 classes 中内嵌枚举,这也将有助于我们将相关的信息集中在一个位置。

          struct Characters {
      
              enum CharacterType {
      
                  case thief
                  case warrior
                  case knight
              }
      
              enum Weapon {
      
                  case bow
                  case sword
                  case lance
                  case dagger
              }
      
              let type: CharacterType
              let weapon: Weapon
          }
  • 枚举的使用

        let warrior = Characters(type: .warrior, weapon: .sword)
    
        print(warrior)      // print Characters(type: Swift_Enum.Enum.Characters.CharacterType.warrior, 
                                           // weapon: Swift_Enum.Enum.Characters.Weapon.sword)

1.5 关联值定义

  • 枚举的定义

    • Swift 的 enum 类型可以存储值, 每个枚举成员设定一个或多个关联值,关联值是将额外信息附加到 enum case 中的一种极好的方式。

    • 关联值附加标签的声明

          /// 打个比方,你正在开发一款交易引擎,可能存在买和卖两种不同的交易类型。除此之外每手交易还要制定明确的
          /// 股票名称和交易数量,然而股票的价值和数量显然从属于交易,让他们作为独立的参数显得模棱两可。你可能往 
          /// 已经想到要 struct 中内嵌一个枚举了,不过关联值提供了一种更清爽的解决方案。
          enum Trade {
      
              case buy(stock: String, amount: Int)
              case sell(stock: String, amount: Int)
          }
    • 关联值不附加标签的声明

          enum Barcode {
      
              case UPCA(Int, Int, Int)
              case QRCode(String)
          }
    • 关联值可以以多种方式使用

          /// 拥有不同值的用例
          enum UserAction {
      
              case openURL(url: NSURL)
              case switchProcess(processId: UInt32)
              case restart(time: NSDate?, intoCommandLine: Bool)
          }
      
          /// 假设你在实现一个功能强大的编辑器,这个编辑器允许多重选择,
          /// 正如 Sublime Text : https://www.youtube.com/watch?v=i2SVJa2EGIw
          enum Selection {
      
              case none
              case single(Range)
              case multiple([Range])
          }
      
          /// 或者假设你在封装一个 C 语言库,正如 Kqeue BSD/Darwin 通知系统:
          /// https://www.freebsd.org/cgi/man.cgi?query=kqueue&sektion=2
          enum KqueueEvent {
      
              case userEvent(identifier: UInt, fflags: [UInt32], data: Int)
              case readFD(fd: UInt, data: Int)
              case writeFD(fd: UInt, data: Int)
              case vnodeFD(fd: UInt, fflags: [UInt32], data: Int)
              case errorEvent(code: UInt, message: String)
          }
      
          /// 又或者一个 RPG 游戏中的所有可穿戴装备可以使用一个枚举来进行映射,
          /// 可以为一个装备增加重量和持久两个属性
          /// 现在可以仅用一行代码来增加一个"钻石"属性,如此一来我们便可以增加几件新的镶嵌钻石的可穿戴装备
          enum Wearable {
      
              enum Weight: Int {
      
                  case light = 1
                  case mid = 4
                  case heavy = 10
              }
      
              enum Armor: Int {
      
                  case light = 2
                  case strong = 8
                  case heavy = 20
              }
      
              case Helmet(weight: Weight, armor: Armor)
              case Breastplate(weight: Weight, armor: Armor)
              case Shield(weight: Weight, armor: Armor)
          }
          let woodenHelmet = Wearable.Helmet(weight: .light, armor: .light)
  • 枚举的使用

    • 如果添加了标签,那么,每当创建枚举用例时,都需要将这些标签标示出来。

          let trade1 = Trade.buy(stock: "APPL", amount: 500)
          let trade2 = Trade.sell(stock: "TSLA", amount: 100)
      
          if case let Trade.buy(stock, amount) = trade1 {
      
              print("buy \(amount) of \(stock)")            // print buy 500 of APPL
          }
      
          if case let Trade.sell(stock, amount) = trade2 {
      
              print("sell \(amount) of \(stock)")           // print sell 100 of TSLA
          }
          let productBarcode1: Barcode = .UPCA(8, 85909_51226, 3)
          let productBarcode2: Barcode = Barcode.QRCode("ABCDEFGHIJKLMNOP")
      
          switch productBarcode1 {                         // print UPC-A with value of 8, 8590951226, 3.
      
              case .UPCA(let numberSystem, let identifier, let check):
                  print("UPC-A with value of \(numberSystem), \(identifier), \(check).")
      
              case .QRCode(let productCode):
                  print("QR code with value of \(productCode).")
          }
      
          switch productBarcode2 {                         // print QR code with value of ABCDEFGHIJKLMNOP.
      
              /// 如果所有的枚举成员的关联值的提取为常数,或者当所有被提取为变量,为了简洁起见,可以放置一个 let 或 var 
              /// 标注在成员名称前
              case let .UPCA(numberSystem, identifier, check):
                  print("UPC-A with value of \(numberSystem), \(identifier), \(check).")
      
              case let .QRCode(productCode):
                  print("QR code with value of \(productCode).")
          }
          let code3: Barcode = .QRCode("123")
          let code4: Barcode = .QRCode("456")
      
          if code3 == code4 {
      
          }
      
          /// 系统提示:Binary operator '==' cannot be applied to two 'Enum.Barcode' operands, 在 Swift 中,
          /// enum 不提供 == 运算符的操作
      
          /// 正确使用方法 使用 switch 去判断类型
      
          switch (code3, code4) {                          // print code3 == code4: false
      
              case (.QRCode(let a), .QRCode(let b)) where a == b:
                  print("code3 == code4: true")
      
              default:
                  print("code3 == code4: false")
          }

1.5.1 枚举的递归/间接类型

  • 间接类型是 Swift 2.0 新增的一个类型。它们允许将枚举中一个 case 的关联值再次定义为枚举。举个例子,假设我们想定义一个文件系统,用来表示文件以及包含文件的目录。如果将文件和目录定义为枚举的 case,则目录 case 的关联值应该再包含一个文件的数组作为它的关联值。因为这是一个递归的操作,编译器需要对此进行一个特殊的准备。Swift 文档中是这么写的:枚举和 case 可以被标记为间接的(indrect),这意味它们的关联值是被间接保存的,这允许我们定义递归的数据结构。

  • 所以,如果我们要定义 FileNode 的枚举,它应该会是这样的

        enum FileNode {
    
            case file(name: String)
            indirect case folder(name: String, files: [FileNode])
        }
  • 此处的 indrect 关键字告诉编译器间接地处理这个枚举的 case。也可以对整个枚举类型使用这个关键字。

  • 作为例子,我们来定义一个二叉树:

        indirect enum Tree {
    
            case empty
            case node(Tree, Element, Tree)
        }
  • 这是一个很强大的特性,可以让我们用非常简洁的方式来定义一个有着复杂关联的数据结构。

1.5.2 对枚举的关联值进行比较

  • 在通常情况下,枚举是很容易进行相等性判断的。一个简单的 enum T { case a, b } 实现默认支持相等性判断 T.a == T.b, T.b != T.a

  • 然而,一旦我们为枚举增加了关联值,Swift 就没有办法正确地为两个枚举进行相等性判断,需要我们自己实现 == 运行符。

        enum Trade2 {
    
            case buy(stock: String, amount: Int)
            case sell(stock: String, amount: Int)
        }
    
        func ==(lhs: Trade2, rhs: Trade2) -> Bool {
    
            switch (lhs, rhs) {
    
                case let (.buy(stock1, amount1), .buy(stock2, amount2))
                    where stock1 == stock2 && amount1 == amount2:
                    return true
    
                case let (.sell(stock1, amount1), .sell(stock2, amount2))
                    where stock1 == stock2 && amount1 == amount2:
                    return true
    
                default:
                    return false
            }
        }
  • 正如我们所见,我们通过 switch 语句对两个枚举的 case 进行判断,并且只有当它们的 case 是匹配的时候(比如 buybuy)才对它们的真实关联值进行判断。

        let buy1 = Trade2.buy(stock: "buy1", amount: 10)
        let buy2 = Trade2.buy(stock: "buy1", amount: 11)
    
        print(buy1 == buy2)         // print false

2、枚举的属性

  • 尽管增加一个存储属性到枚举中不被允许,但你依然能够创建计算属性。当然,计算属性的内容都是建立在枚举值下或者枚举关联值得到的。

  • 枚举属性的定义

        enum Device {
    
            case iPad, iPhone
    
            var year: Int {
    
                switch self {
    
                    case .iPhone:
                        return 2007
    
                    case .iPad:
                        return 2010
                }
            }
        }
  • 枚举属性的使用

        let device = Device.iPhone.year
    
        print(device)               // print 2007

3、枚举的方法

  • 枚举中的方法为每一个 enum case 而 “生”。所以倘若想要在特定情况执行特定代码的话,需要分支处理或采用 switch 语句来明确正确的代码路径。

  • 也能够为枚举创建一些静态方法(static methods)。换言之通过一个非枚举类型来创建一个枚举。
  • 方法可以声明为 mutating。这样就允许改变隐藏参数 selfcase 值了。

  • 方法和静态方法的添加允许我们为 enum 附加功能,这意味着无须依靠额外函数就能实现。

  • 枚举方法的定义

    • 可以在 enum 中像这样定义方法

          enum Wearable1 {
      
              enum Weight: Int {
                  case light = 1
              }
      
              enum Armor: Int {
                  case light = 2
              }
      
              case helmet(weight: Weight, armor: Armor)
      
              func attributes() -> (weight: Int, armor: Int) {
      
                  switch self {
      
                      case .helmet(let w, let a):
                          return (weight: w.rawValue * 2, armor: a.rawValue * 4)
                  }
              }
          }
      
          enum Device1 {
      
              case iPad, iPhone, AppleTV, AppleWatch
      
              func introduced() -> String {
      
                  switch self {
      
                      case .iPad:
                          return "\(self) was introduced 2010"
                      case .iPhone:
                          return "\(self) was introduced 2007"
                      case .AppleTV:
                          return "\(self) was introduced 2006"
                      case .AppleWatch:
                          return "\(self) was introduced 2014"
                  }
              }
          }
    • 静态方法, static 修饰

          enum Device2 {
      
              case AppleWatch
      
              static func fromSlang(term: String) -> Device2? {
      
                  if term == "iWatch" {
                      return .AppleWatch
                  }
      
                  return nil
              }
          }
    • 可变方法,mutating 修饰

          enum TriStateSwitch {
      
              case off, low, high
      
              mutating func next() {
      
                  switch self {
      
                      case .off:
                          self = .low
                      case .low:
                          self = .high
                      case .high:
                          self = .off
                  }
              }
          }
  • 枚举方法的使用

        let woodenHelmetProps = Wearable1.helmet(weight: .light, armor: .light).attributes()
        let device1 = Device1.iPhone.introduced()
        let device2 = Device2.fromSlang(term: "iWatch")
        var ovenLight = TriStateSwitch.low
    
        print(woodenHelmetProps)    // print woodenHelmetProps (weight: 2, armor: 8)
    
        print(device1)              // print device1 iPhone was introduced 2007
    
        print(String(describing: device2)) // print device2 Optional(Swift_Enum.Enum.Device2.AppleWatch)
    
        ovenLight.next()            // ovenLight 现在等于 .high
        print(ovenLight)            // print ovenLight high
    
        ovenLight.next()            // ovenLight 现在等于 .off
        print(ovenLight)   

3.1 自定义构造方法

  • 我们也可以使用自定义构造方法来替换静态方法。

  • 枚举与结构体和类的构造方法最大的不同在于,枚举的构造方法需要将隐式的 self 属性设置为正确的 case

        enum Device3 {
    
            case AppleWatch
    
            init?(term: String) {           // 可失败(failable)的构造方法
    
                if term == "iWatch" {
                    self = .AppleWatch
                    return
                }
    
                return nil
            }
        }
    
        enum NumberCategory {
    
            case small
            case medium
            case big
            case huge
    
            init(number n: Int) {           // 普通的构造方法
    
                if n < 10000 {
                    self = .small
                } else if n < 1000000 {
                    self = .medium
                } else if n < 100000000 {
                    self = .big
                } else {
                    self = .huge
                }
            }
        }
        let device = Device3(term: "iWatch")
        print(String(describing: device))              // print small
    
        let aNumber = NumberCategory(number: 100)
        print(aNumber)                                 // print small

4、枚举的协议

  • Swift 协议 定义一个接口或类型以供其他数据结构来遵循。enum 当然也不例外。
  • 除了附加方法的能力之外,Swift 也允许你在枚举中使用协议(Protocols)和协议扩展(Protocol Extension)。

  • 枚举

        /// 你也许会简单地拿 struct 实现这个协议,但是考虑应用的上下文,enum 是一个更明智的处理方法。
        /// 不过你无法添加一个存储属性到 enum 中,就像 var remainingFuns:Int。那么你会如何构造呢?
        /// 答案灰常简单,你可以使用关联值完美解决
        enum Account {
    
            case empty
            case funds(remaining: Int)
    
            enum Error1: Error {
                case overdraft(amount: Int)
            }
    
            var remainingFunds: Int {
    
                switch self {
                    case .empty: return 0
                    case .funds(let remaining):
                        return remaining
                }
            }
        }
  • 枚举协议的定义

        /// 我们先从 Swift 标准库中的一个例子开始。CustomStringConvertible 是一个以打印为目的的自定义
        /// 格式化输出的类型。该协议只有一个要求,即一个只读(getter)类型的字符串(String 类型)。我们可以
        /// 很容易为 enum 实现这个协议。
        protocol CustomStringConvertible {
            var description: String { get }
        }
        /// 一些协议的实现可能需要根据内部状态来相应处理要求。例如定义一个管理银行账号的协议
        protocol AccountCompatible {
    
            var remainingFunds: Int { get }
    
            mutating func addFunds(amount: Int) throws
            mutating func removeFunds(amount: Int) throws
        }
    
        /// 为了保持代码清爽,我们可以在 enum 的协议扩展(protocl extension)中定义必须的协议函数
        extension Account: AccountCompatible {
    
            mutating func addFunds(amount: Int) throws {
    
                var newAmount = amount
                if case let .funds(remaining) = self {
                    newAmount += remaining
                }
    
                if newAmount < 0 {
                    throw Error1.overdraft(amount: -newAmount)
                } else if newAmount == 0 {
                    self = .empty
                } else {
                    self = .funds(remaining: newAmount)
                }
            }
    
            mutating func removeFunds(amount: Int) throws {
                try self.addFunds(amount: amount * -1)
            }
        }
  • 枚举协议的使用

        var account = Account.funds(remaining: 20)
    
        print("add: ", try? account.addFunds(amount: 10))             // print add:  Optional(())
        print("remove 1: ", try? account.removeFunds(amount: 15))     // print remove 1:  Optional(())
        print("remove 2: ", try? account.removeFunds(amount: 55))     // print remove 2:  nil

5、枚举的扩展

  • 枚举也可以进行扩展。最明显的用例就是将枚举的 case 和 method 分离,这样阅读你的代码能够简单快速地消化掉 enum 内容。

  • 枚举

        enum Entities {
    
            case soldier(x: Int, y: Int)
            case tank(x: Int, y: Int)
            case player(x: Int, y: Int)
        }
  • 枚举扩展的定义

        /// 为 enum 扩展方法
        extension Entities {
    
            mutating func move(dist: CGVector) {}
            mutating func attack() {}
        }
        /// 你同样可以通过写一个扩展来遵循一个特定的协议
        extension Entities: CustomStringConvertible {
    
            var description: String {
    
                switch self {
    
                    case let .soldier(x, y):
                        return "\(x), \(y)"
    
                    case let .tank(x, y):
                        return "\(x), \(y)"
    
                    case let .player(x, y):
                        return "\(x), \(y)"
                }
            }
        }
  • 枚举扩展的使用

        var entities = Entities.tank(x: 2, y: 5)
    
        print(entities.attack())            // print ()
        print(entities.description)         // print 2, 5

6、枚举的泛型

  • 枚举也支持泛型参数定义。你可以使用它们以适应枚举中的关联值。

  • 就拿直接来自 Swift 标准库中的简单例子来说,即 Optional 类型。你主要可能通过以下几种方式使用它:
    • 可选链(optional chaining(?))
    • if-let 可选绑定
    • guard let
    • switch
  • 但是从语法角度来说你也可以这么使用 Optional

        let aValue  = Optional.some(5)
        let noValue = Optional.none
    
        if noValue == Optional.none {
            print("No value")           // print No value
        }
  • 这是 Optional 最直接的用例,并未使用任何语法糖,但是不可否认 Swift 中语法糖的加入使得你的工作更简单。如果你观察上面的实例代码,你恐怕已经猜到 Optional 内部实现是这样的。

        // Simplified implementation of Swift's Optional
        enum MyOptional {
    
            case some(T)
            case none
        }
  • 这里有啥特别呢?注意枚举的关联值采用泛型参数 T 作为自身类型,这样可选类型构造任何你想要的返回值。
    枚举可以拥有多个泛型参数。就拿熟知的 Either 类为例,它并非是 Swift 标准库中的一部分,而是实现于众多开源库以及
    其他函数式编程语言,比如 HaskellF#。设计想法是这样的: 相比较仅仅返回一个值或没有值(née Optional),你更期望返回一个成功值或者一些反馈信息(比如错误值)。

        // The well-known either type is, of course, an enum that allows you to return either
        // value one (say, a successful value) or value two (say an error) from a function
        enum Either {
    
            case left(T1)
            case right(T2)
        }
  • Swift 中所有在 class 和 struct 中奏效的类型约束,在 enum 中同样适用。

        // Totally nonsensical example. A bag that is either full (has an array with contents) or empty.
        // Totally nonsensical example. A bag that is either full (has an array with contents) or empty.
        enum Bag where T.Iterator.Element == Equatable {
    
            case empty
            case full(contents: T)
        }

7、枚举 case 的 迭代

  • 一个特别经常被问到的问题就是如何对枚举中的 case 进行迭代。可惜的是,枚举并没有遵守 SequenceType 协议,因此没有一个官方的做法来对其进行迭代。取决于枚举的类型,对其进行迭代可能也简单,也有可能很困难。在 StackOverflow 上有一个很好的讨论贴。贴子里面讨论到的不同情况太多了,如果只在这里摘取一些会有片面性,而如果将全部情况都列出来,则会太多。

8、对 Objective-C 的支持

  • 基于整型的枚举,如 enum Bit: Int { case zero = 0; case one = 1 } 可以通过 @objc 标识来将其桥接到 Objective-C 当中。然而,一旦使用整型之外的类型(如 String)或者开始使用关联值,我们就无法在 Objective-C 当中使用这些枚举了。

  • 有一个名为 _ObjectiveCBridgeable 的隐藏协议,可以让规范我们以定义合适的方法,如此一来,Swift 便可以正确地将枚举转成 Objective-C 类型。然而,从理论上来讲,这个协议还是允许我们将枚举(包括其实枚举值)正确地桥接到 Objective-C 当中。

  • 但是,我们并不一定非要使用上面提到的这个方法。为枚举添加两个方法,使用 @objc 定义一个替代类型,如此一来我们便可以自由地将枚举进行转换了,并且这种方式不需要遵守私有协议。

        enum Trade3 {
    
            case buy(stock: String, amount: Int)
            case sell(stock: String, amount: Int)
        }
    
        // 这个类型也可以定义在 Objective-C 的代码中
        @objc class OTrade: NSObject {
    
            var type: Int
    
            var stock: String
            var amount: Int
    
            init(type: Int, stock: String, amount: Int) {
    
                self.type = type
                self.stock = stock
                self.amount = amount
            }
        }
    
        extension Trade3  {
    
            func toObjc() -> OTrade {
    
                switch self {
    
                    case let .buy(stock, amount):
                        return OTrade(type: 0, stock: stock, amount: amount)
    
                    case let .sell(stock, amount):
                        return OTrade(type: 1, stock: stock, amount: amount)
                }
            }
    
            static func fromObjc(source: OTrade) -> Trade3? {
    
                switch (source.type) {
    
                    case 0:
                        return Trade3.buy(stock: source.stock, amount: source.amount)
    
                    case 1:
                        return Trade3.sell(stock: source.stock, amount: source.amount)
    
                    default:
                        return nil
                }
            }
        }
  • 这个方法有一个的缺点,我们需要将枚举映射为 Objective-C 中的 NSObject 基础类型(我们也可以直接使用 NSDictionary),但是,当我们碰到一些确实需要在 Objective-C 当中获取有关联值的枚举时,这是一个可以使用的方法。

9、枚举的底层

  • Erica Sadun 写过一篇关于枚举底层的博客,涉及到枚举底层的方方面面。在生产代码中绝不应该使用到这些东西,但是学习一下还是相当有趣的。

  • 在这里,只提到那篇博客中一条,如果想了解更多,请移步到原文:枚举通常都是一个字节长度。[...] 如果你真的很傻很天真,你当然可以定义一个有成百上千个 case 的枚举,在这种情况下,取决于最少所需要的比特数,枚举可能占据两个字节或者更多。

10、Swift 标准库中的枚举

  • Bit 这个枚举有两个值,onezero。它被作为 CollectionOfOne 中的 Index 类型。

  • FloatingPointClassification 这个枚举定义了一系列 IEEE 754 可能的类别,比如 NegativeInfinity, PositiveZeroSignalingNaN

  • Mirror.AncestorRepresentationMirror.DisplayStyle 这两个枚举被用在 Swift 反射 API 的上下文当中。

  • Optional 这个就不用多说了

  • Process 这个枚举包含了当前进程的命令行参数(Process.argc, Process.arguments)。这是一个相当有趣的枚举类型,因为在 Swift 1.0 当中,它是被作为一个结构体来实现的。

11、枚举的实践用例

  • 在很多场合,使用枚举要胜过使用结构体和类。一般来讲,如果问题可以被分解为有限的不同类别,则使用枚举应该就是正确的选择。即使只有两种 case,这也是一个使用枚举的完美场景,正如 OptionalEither 类型所展示的。

11.1 错误处理

  • 说到枚举的实践使用,当然少不了在 Swift 2.0 当中新推出的错误处理。标记为可抛出的函数可以抛出任何遵守了 Error 空协议(Swift 4 之前名称为 ErrorType)的类型。正如 Swift 官方文档中所写的:Swift 的枚举特别适用于构建一组相关的错误状态,可以通过关联值来为其增加额外的附加信息。

  • 作为一个示例,我们来看下流行的 JSON 解析框架 Argo。当 JSON 解析失败的时候,它有可能是以下两种主要原因:
    • JSON 数据缺少某些最终模型所需要的键(比如你的模型有一个 username 的属性,但是 JSON 中缺少了)
    • 存在类型不匹配,比如说 username 需要的是 String 类型,而 JSON 中包含的是 NSNull6

    • 除此之外,Argo 还为不包含在上述两个类别中的错误提供了自定义错误。它们的 Error 枚举是类似这样的,所有的 case 都有一个关联值用来包含关于错误的附加信息。

          enum DecodeError: ErrorType {
      
              case TypeMismatch(expected: String, actual: String)
              case MissingKey(String)
              case Custom(String)
          }
  • 一个更加通用的用于完整 HTTP / REST API 错误处理的 Error 应该是类似这样的

        enum APIError: Error {
    
            // Can't connect to the server (maybe offline?)
            case ConnectionError(error: NSError)
    
            // The server responded with a non 200 status code
            case ServerError(statusCode: Int, error: NSError)
    
            // We got no data (0 bytes) back from the server
            case NoDataError
    
            // The server response can't be converted from JSON to a Dictionary
            case JSONSerializationError(error: ErrorType)
    
            // The Argo decoding Failed
            case JSONMappingError(converstionError: DecodeError)
        }
    • 这个 Error 实现了完整的 REST 程序栈解析有可能出现的错误,包含了所有在解析结构体与类时会出现的错误。
    • 如果你看得够仔细,会发现在 JSONMappingError 中,我们将 Argo 中的 DecodeError 封装到了我们的 APIError 类型当中,因为我们会用 Argo 来作实际的 JSON 解析。
  • 更多关于 Error 以及此种枚举类型的示例可以参看官方文档。

11.2 观察者模式

  • 在 Swift 当中,有许多方法来构建观察模式。如果使用 @objc 兼容标记,则我们可以使用 NSNotificationCenter 或者 KVO。即使不用这个标记,didSet 语法也可以很容易地实现简单的观察模式。在这里可以使用枚举,它可以使被观察者的变化更加清晰明了。

  • 设想我们要对一个集合进行观察。如果我们稍微思考一下就会发现这只有几种可能的情况:一个或多个项被插入,一个或多个项被删除,一个或多个项被更新。这听起来就是枚举可以完成的工作。

        enum Change {
    
            case Insertion(items: [Item])
            case Deletion(items: [Item])
            case Update(items: [Item])
        }
    • 之后,观察对象就可以使用一个很简洁的方式来获取已经发生的事情的详细信息。这也可以通过为其增加 oldValuenewValue 的简单方法来扩展它的功能。

11.3 状态码

  • 如果我们正在使用一个外部系统,而这个系统使用了状态码(或者错误码)来传递错误信息,类似 HTTP 状态码,这种情况下枚举就是一种很明显并且很好的方式来对信息进行封装。

        enum HttpError: String {
    
            case Code400 = "Bad Request"
            case Code401 = "Unauthorized"
            case Code402 = "Payment Required"
            case Code403 = "Forbidden"
            case Code404 = "Not Found"
        }

11.4 结果类型映射

  • 枚举也经常被用于将 JSON 解析后的结果映射成 Swift 的原生类型。类似地,如果我们解析了其它的东西,也可以使用这种方式将解析结果转化我们 Swift 的类型。

        enum JSON {
    
            case JSONString(Swift.String)
            case JSONNumber(Double)
            case JSONObject([String : JSONValue])
            case JSONArray([JSONValue])
            case JSONBool(Bool)
            case JSONNull
        }

11.5 UIKit 标识

  • 枚举可以用来将字符串类型的重用标识或者 storyboard 标识映射为类型系统可以进行检查的类型。

  • 假设我们有一个拥有很多原型 CellUITableView

        enum CellType: String {
    
            case ButtonValueCell = "ButtonValueCell"
            case UnitEditCell    = "UnitEditCell"
            case LabelCell       = "LabelCell"
            case ResultLabelCell = "ResultLabelCell"
        }

11.6 单位

  • 单位以及单位转换是另一个使用枚举的绝佳场合。可以将单位及其对应的转换率映射起来,然后添加方法来对单位进行自动的转换。另一个示例是货币的转换。以及数学符号(比如角度与弧度)也可以从中受益。

        enum Liquid: Float {
    
            case ml = 1.0
            case l = 1000.0
    
            func convert(amount amount: Float, to: Liquid) -> Float {
    
                if self.rawValue < to.rawValue {
                    return (self.rawValue / to.rawValue) * amount
                } else {
                    return (self.rawValue * to.rawValue) * amount
                }
            }
        }
    
        // Convert liters to milliliters
        print(Liquid.l.convert(amount: 5, to: Liquid.ml))

11.7 游戏

  • 游戏也是枚举中的另一个相当好的用例,屏幕上的大多数实体都属于一个特定种族的类型(敌人,障碍,纹理,...)。相对于本地的 iOS 或者 Mac 应用,游戏更像是一个白板。即开发游戏我们可以使用全新的对象以及全新的关联创造一个全新的世界,而 iOS 或者 macOS 需要使用预定义的 UIButtonsUITableViewsUITableViewCells 或者 NSStackView

  • 不仅如此,由于枚举可以遵守协议,我们可以利用协议扩展和基于协议的编程为不同为游戏定义的枚举增加功能。

        enum FlyingBeast { case Dragon, Hippogriff, Gargoyle }
        enum Horde { case Ork, Troll }
        enum Player { case Mage, Warrior, Barbarian }
        enum NPC { case Vendor, Blacksmith }
        enum Element { case Tree, Fence, Stone }
    
        protocol Hurtable {}
        protocol Killable {}
        protocol Flying {}
        protocol Attacking {}
        protocol Obstacle {}
    
        extension FlyingBeast: Hurtable, Killable, Flying, Attacking {}
        extension Horde: Hurtable, Killable, Attacking {}
        extension Player: Hurtable, Obstacle {}
        extension NPC: Hurtable {}
        extension Element: Obstacle {}

11.8 字符串类型化

  • 在一个稍微大一点的 Xcode 项目中,我们很快就会有一大堆通过字符串来访问的资源。在前面的小节中,我们已经提过重用标识和 storyboard 的标识,但是除了这两样,还存在很多资源:图像,Segues,Nibs,字体以及其它资源。通常情况下,这些资源都可以分成不同的集合。如果是这样的话,一个类型化的字符串会是一个让编译器帮我们进行类型检查的好方法。

        enum DetailViewImages: String {
    
            case Background     = "bg1.png"
            case Sidebar        = "sbg.png"
            case ActionButton1  = "btn1_1.png"
            case ActionButton2  = "btn2_1.png"
        }
  • 对于 iOS 开发者,R.swift 这个第三方库可以为以上提到的情况自动生成结构体。但是有些时候你可能需要有更多的控制(或者你可能是一个 Mac 开发者)。

11.9 API 端点

  • Rest API 是枚举的绝佳用例。它们都是分组的,它们都是有限的 API 集合,并且它们也可能会有附加的查询或者命名的参数,而这可以使用关联值来实现。

  • 这里有个 Instagram API 的简化版。

        enum Instagram {
    
            enum Media {
    
                case Popular
                case Shortcode(id: String)
                case Search(lat: Float, min_timestamp: Int, lng: Float, max_timestamp: Int, distance: Int)
            }
    
            enum Users {
    
                case User(id: String)
                case Feed
                case Recent(id: String)
            }
        }
  • Ash Furrow 的 Moya 框架就是基本这个思想,使用枚举对 rest 端点进行映射。

11.10 链表

  • Airspeed Velocity 有一篇极好的文章说明了如何使用枚举来实现一个链表。那篇文章中的大多数代码都超出了枚举的知识,并涉及到了大量其它有趣的主题,但是,链表最基本的定义是类似这样的(对其进行了一些简化)。

        enum List {
    
            case End
            indirect case Node(Int, next: List)
        }
    • 每一个节点(Node)case 都指向了下一个 case,通过使用枚举而非其它类型,我们可以避免使用一个可选的 next 类型以用来表示链表的结束。
  • Airspeed Velocity 还写过一篇超赞的博客,关于如何使用 Swift 的间接枚举类型来实现红黑树,所以如果你已经阅读过关于链表的博客,你可能想继续阅读这篇关于红黑树的博客。

11.11 设置字典

  • 这是 Erica Sadun 提出的非常非常机智的解决方案。简单来讲,就是任何我们需要用一个属性的字典来对一个项进行设置的时候,都应该使用一系列有关联值的枚举来替代。使用这方法,类型检查系统可以确保配置的值都是正确的类型。关于更多的细节,以及合适的例子,可以阅读下她的文章。

12、枚举的局限

12.1 提取关联值

  • David Owens 写过一篇文章,他觉得当前的关联值提取方式是很笨重的:为了从一个枚举中获取关联值,我们必须使用模式匹配。然而,关联值就是关联在特定枚举 case 的高效元组。而元组是可以使用更简单的方式来获取它内部值,即 .keyword 或者 .0

        // Enum(枚举)
        enum Ex {
    
            case Mode(ab: Int, cd: Int) 
        }
    
        if case Ex.Mode(let ab, let cd) = Ex.Mode(ab: 4, cd: 5) {
            print(ab)
        }
    
        // vs tuples(元组)
        let tp = (ab: 4, cd: 5)
        print(tp.ab)
  • 如果你也同样觉得我们应该使用相同的方法来对枚举进行解构(deconstruct),这里有个 rdar: rdar://22704262。

12.2 相等性

  • 拥有关联值的枚举没有遵守 equatable 协议。这是一个遗憾,因为它为很多事情增加了不必要的复杂和麻烦。深层的原因可能是因为关联值的底层是使用了元组,而元组并没有遵守 equatable 协议。然而,对于限定的 case 子集,如果这些关联值的类型都遵守了 equatable 类型,编译器应该默认为其生成 equatable 扩展。

        // Int 和 String 是可判等的, 所以 Mode 应该也是可判等的
        enum Ex { 
    
            case Mode(ab: Int, cd: String) 
        }
    
        // Swift 应该能够自动生成这个函数
        func ==(lhs: Ex.Mode, rhs: Ex.Mode) -> Bool {
    
            switch (lhs, rhs) {
    
                case (.Mode(let a, let b), .Mode(let c, let d)):
                    return a == c && b == d
    
                default:
                    return false
            }
        }

12.3 元组

  • 最大的问题就是对元组的支持。他们目前还处于无文档状态并且在很多场合都无法使用。在枚举当中,我们无法使用元组作为枚举的值。

        enum Devices: (intro: Int, name: String) {
    
            case iPhone     = (intro: 2007, name: "iPhone")
            case AppleTV    = (intro: 2006, name: "Apple TV")
            case AppleWatch = (intro: 2014, name: "Apple Watch")
        }
    • 这似乎看起来并不是一个最好的示例,但是我们一旦开始使用枚举,就会经常陷入到需要用到类似上面这个示例的情形中。

12.4 迭代枚举的所有 case

  • 这个我们已经在前面讨论过了。目前还没有一个很好的方法来获得枚举中的所有 case 的集合以使我们可以对其进行迭代。

12.5 默认关联值

  • 另一个会碰到的事是枚举的关联值总是类型,但是我们却无法为这些类型指定默认值。

        enum Characters {
    
            case Mage(health: Int = 70, magic: Int = 100, strength: Int = 30)
            case Warrior(health: Int = 100, magic: Int = 0, strength: Int = 100)
            case Neophyte(health: Int = 50, magic: Int = 20, strength: Int = 80)
        }
    • 我们依然可以使用不同的值创建新的 case,但是角色的默认设置依然会被映射。

你可能感兴趣的:(iOS - Swift Enum 枚举)