Swift 最佳實踐之 Enum

語言: CN / TW / HK

Swift 作為現代、高效、安全的編程語言,其背後有很多高級特性為之支撐。

『 Swift 最佳實踐 』系列對常用的語言特性逐個進行介紹,助力寫出更簡潔、更優雅的 Swift 代碼,快速實現從 OC 到 Swift 的轉變。

該系列內容主要包括:

  • Optional
  • Enum
  • Closure
  • Protocol
  • Generic
  • Property Wrapper
  • Structured Concurrent
  • Result builder
  • Error Handle
  • Advanced Collections (Asyncsequeue/OptionSet/Lazy)
  • Expressible by Literal
  • Pattern Matching
  • Metatypes(.self/.Type/.Protocol)

ps. 本系列不是入門級語法教程,需要有一定的 Swift 基礎

本文是系列文章的第二篇,介紹 Enum,內容主要包括 Swift Enum 高級特性以及典型應用場景。

Swift 賦以 Enum 非常強大的能力,C-like Enum 與之不可同日而語。

充分利用 Enum 特性寫出更優雅、更安全的代碼。

Enum 特性


首先,簡要羅列一下 Swift Enum 具備的特性:

Value

C-like Enum 中每個 case 都關聯一個整型數值,而 Swift Enum 更靈活:

  • 默認,case 不關聯任何數值

  • 可以提供類似 C-like Enum 那樣的數值 (Raw Values), 但類型更豐富,可以是 Int、Float、String、Character

    ``` enum Direction: String {

    case east // 等價於 case east = "east", case west case south case north = "n" } ```

    如上,對於 String 類型的 Raw Value,若沒指定值,默認為 case name

    對於 Int/Float 類型,默認值從 0 開始,依次 +1

    通過 rawValue 屬性可以獲取 case 對應的 Raw Value

    let direction = Direction.east let value = direction.rawValue // "east"

    對於 Raw-Values Enum,編譯器會自動生成初始化方法:

    ``` // 由於並不是所有輸入的 rawValue 都能找到匹配的 case // 故,這是個 failable initializer,在沒有匹配的 case 時,返回 nil init?(rawValue: RawValueType)

    let direction = Direction.init(rawValue: "east") ```

  • 還可以為 case 指定任意類型的關聯值 (Associated-Values)

    ``` enum UGCContent {

    case text(String) case image(Image, description: String?) case audio(Audio, autoPlay: Bool) case video(Video, autoPlay: Bool) }

    let text = UGCContent.text("Hello world!") let video = UGCContent.video(Video.init(), autoplay: true) ```

    還可以為關聯值提供默認值:

    ``` enum UGCContent {

    case text(String = "Hello world!") case image(Image, description: String?) case audio(Audio, autoPlay: Bool = true) case video(Video, autoPlay: Bool = true) }

    let text = UGCContent.text() let content = UGCContent.video(Video.init()) ```

    如下,即可以通過 if-case-let,也可以通過 switch-case-let 匹配 enum 並提取關聯值:

    ``` if case let .video(video, autoplay) = content { print(video, autoplay) }

    switch content { case let .text(text): print(text) case let .image(image, description): print(image, description) case let .audio(audio, autoPlay): print(audio, autoPlay) case let .video(video, autoPlay): print(video, autoPlay) } ```

First-class type

Swift Enum 作為「 First-class type 」,有許多傳統上只有 Class 才具備的特性:

  • 可以有計算屬性 (computed properties),當然了存儲屬性是不能有的,如:

    ``` enum UGCContent {

    var description: String { switch self { case let .text(text): return text case let .image(, description): return description ?? "image" case let .audio(, autoPlay): return "audio, autoPlay: (autoPlay)" case let .video(_, autoPlay): return "video, autoPlay: (autoPlay)" } } }

    let content = UGCContent.video(Video.init()) print(content.description)
    ```

  • 可以有實例方法/靜態方法

  • 可以有初始化方法,如:

    ``` enum UGCContent {

    init(_ text: String) { self = .text(text) } }

    let text = UGCContent.init("Hi!") ```

  • 可以有擴展 (extension),也可以實現協議,如:

    ``` extension UGCContent: Equatable {

    static func == (lhs: Self, rhs: Self) -> Bool { return false } } ```

Recursive Enum

Enum 關聯值類型可以是其枚舉自身,稱其為遞歸枚舉 (Recursive Enum)。

如下,定義了 Enum 類型的鏈表接點 LinkNode,包含 2 個 case:

  • end,表示尾部節點
  • link,其關聯值 next 的類型為 LinkNode

``` // 關鍵詞 indirect 也可以放在 enum 前面 // indirect enum LinkNode { enum LinkNode {

case end(NodeType) indirect case link(NodeType, next: LinkNode) }

func sum(rootNode: LinkNode) -> Int {

switch rootNode { case let .end(value): return value case let .link(value, next: next): return value + sum(rootNode: next) } }

let endNode = LinkNode.end(3) let childNode = LinkNode.link(9, next: endNode) let rootNode = LinkNode.link(5, next: childNode)

let sum = sum(rootNode: rootNode) ```

Iterating over Enum Cases

有時,我們希望遍歷 Enum 的所有 cases,或是獲取第一個 case,此時 CaseIterable 派上用場了:

``` public protocol CaseIterable {

/// A type that can represent a collection of all values of this type.
associatedtype AllCases : Collection = [Self] where Self == Self.AllCases.Element

/// A collection of all values of this type.
static var allCases: Self.AllCases { get }

} ```

可以看到,CaseIterable 協議有一個靜態屬性:allCases

對於沒有關聯值 (Associated-Values) 的枚舉,當聲明其遵守 CaseIterable 時,會自動合成 allCases 屬性:

``` enum Weekday: String, CaseIterable { case sunday, monday, tuesday, wednesday, thursday, friday, saturday

/ 自動合成的實現 static var allCases: Self.AllCases { [sunday, monday, tuesday, wednesday, thursday, friday, saturday] } / }

// sunday, monday, tuesday, wednesday, thursday, friday, saturday let weekdays = Weekday.allCases.map{ $0.rawValue }.joined(separator: ", ") ```

對於有關聯值的枚舉,不會自動合成 allCases,因為關聯值沒法確定

CaseIterable-Enum-Error.png

此時,需要手動實現 CaseIterable 協議:

``` enum UGCContent: CaseIterable {

case text(String = "ugc") case image(Image, description: String? = nil) case audio(Audio, autoPlay: Bool = false) case video(Video, autoplay: Bool = true)

static var allCases: [UGCContent] { [.text(), image(Image("")), .audio(Audio()), .video(Video())] } } ```

Equatable

沒有關聯值的枚舉,默認可以執行判等操作 (==),無需聲明遵守 Equatable 協議:

``` let sun = Weekday.sunday let mon = Weekday.monday

sum == Weekday.sunday // true sum == mon // false ```

對於有關聯值的枚舉,若需要執行判等操作,需顯式聲明遵守 Equatable 協議:

Enum-Equatable-Error.png // 由於 NodeType:Equatable // 故,系統會為 LinkNode 自動合成 static func == (lhs: Self, rhs: Self) -> Bool // 無需手寫 == 的實現,只需顯式聲明 LinkNode 遵守 Equatable 即可 enum LinkNode<NodeType: Equatable>: Equatable { case end(NodeType) indirect case link(NodeType, next: LinkNode) }

應用


關聯值可以説極大豐富了 Swift Enum 的使用場景,而 C-like Enum 限於只是個 Int 型值,只能用於一些簡單的狀態、分類等。

因此,我們需要轉變思維,善用 Swift Enum。對於一組相關的「值」、「狀態」、「操作」等等,都可以通過 Enum 封裝,附加信息用 Associated-Values 表示。

標準庫中的 Enum

Enum 在 Swift 標準庫中有大量應用,典型的如:

  • Optional,在 「 Swift 最佳實踐之 Enum 」中有詳細介紹,不再贅述

    ``` @frozen public enum Optional : ExpressibleByNilLiteral {

    /// The absence of a value.
    ///
    /// In code, the absence of a value is typically written using the `nil`
    /// literal rather than the explicit `.none` enumeration case.
    case none
    
    /// The presence of a value, stored as `Wrapped`.
    case some(Wrapped)
    

    } ```

  • Result,用於封裝結果,如網絡請求、方法返回值等

    ``` @frozen public enum Result where Failure : Error {

    /// A success, storing a `Success` value.
    case success(Success)
    
    /// A failure, storing a `Failure` value.
    case failure(Failure)
    

    } ```

  • Error Handle,如 EncodingErrorDecodingError

    ``` public enum DecodingError : Error {

    /// An indication that a value of the given type could not be decoded because
    /// it did not match the type of what was found in the encoded payload.
    ///
    /// As associated values, this case contains the attempted type and context
    /// for debugging.
    case typeMismatch(Any.Type, DecodingError.Context)
    
    /// An indication that a non-optional value of the given type was expected,
    /// but a null value was found.
    ///
    /// As associated values, this case contains the attempted type and context
    /// for debugging.
    case valueNotFound(Any.Type, DecodingError.Context)
    
    /// An indication that a keyed decoding container was asked for an entry for
    /// the given key, but did not contain one.
    ///
    /// As associated values, this case contains the attempted key and context
    /// for debugging.
    case keyNotFound(CodingKey, DecodingError.Context)
    
    /// An indication that the data is corrupted or otherwise invalid.
    ///
    /// As an associated value, this case contains the context for debugging.
    case dataCorrupted(DecodingError.Context)
    

    } ```

  • Never,是一個沒有 case 的枚舉,用於表示一個方法永遠不會正常返回

    /// The return type of functions that do not return normally, that is, a type /// with no values. /// /// Use `Never` as the return type when declaring a closure, function, or /// method that unconditionally throws an error, traps, or otherwise does /// not terminate. /// /// func crashAndBurn() -> Never { /// fatalError("Something very, very bad happened") /// } @frozen public enum Never { // ... }

實踐中的應用

  • Error Handle

    Swift enumerations are particularly well suited to modeling a group of related error conditions, with associated values allowing for additional information about the nature of an error to be communicated.

    (Documentation-errorhandling)

    正如 Apple 官方文檔所言,定義錯誤模型是 Enum 的典型應用場景之一,如上節提到的 EncodingErrorDecodingError

    將不同的錯誤類型定義為 case,錯誤相關信息以關聯值的形式附加在相應 case 上。

    在著名網絡庫 Alamofire 中也有很好的應用,如:

    `` public enum AFError: Error { /// The underlying reason the.multipartEncodingFailederror occurred. public enum MultipartEncodingFailureReason { /// ThefileURLprovided for reading an encodable body part isn't a fileURL. case bodyPartURLInvalid(url: URL) /// The filename of thefileURLprovided has either an emptylastPathComponentorpathExtension. case bodyPartFilenameInvalid(in: URL) /// The file at the fileURL provided was not reachable. case bodyPartFileNotReachable(at: URL)

        // ...
    }
    
    /// The underlying reason the `.parameterEncoderFailed` error occurred.
    public enum ParameterEncoderFailureReason {
        /// Possible missing components.
        public enum RequiredComponent {
            /// The `URL` was missing or unable to be extracted from the passed `URLRequest` or during encoding.
            case url
            /// The `HTTPMethod` could not be extracted from the passed `URLRequest`.
            case httpMethod(rawValue: String)
        }
    
        /// A `RequiredComponent` was missing during encoding.
        case missingRequiredComponent(RequiredComponent)
        /// The underlying encoder failed with the associated error.
        case encoderFailed(error: Error)
    }
    
    // ...
    

    } ```

  • 命名空間

    命名空間有助於提升代碼結構化,Swift 中命名空間是隱式的,即以模塊 (Module) 為邊界,不同的模塊屬於不同的命名空間,無法顯式定義命名空間 (沒有 namespace 關鍵詞)。

    我們可以通過 no-case Enum 創建自定義(偽)命名空間,實現更小粒度的代碼結構化

    為什麼是用 Enum,而不是 Struct 或 Class?

    原因在於,沒有 case 的 Enum 是無法實例化的

    而 Struct、Class 一定是可以實例化的

    如,Apple 的 Combine 庫用 Enum 定義了命名空間 PublishersSubscribers

    Namespace-Publishers.png Publishers-Namespace-First.png

    如上,通過命名空間 Publishers 將相關功能組織在一起,代碼更加結構化

    也不需要在每個具體類型前重複添加前綴/後綴,如:

    • MulticastPublisher --> Publishers.Multicast
    • SubscribeOnPublisher --> Publishers.SubscribeOn
    • FirstPublisher --> Publishers.First
  • 定義常量

    思想跟上面提到的命名空間是一樣的,可以將一組相關常量定義在一個 Enum 中,如:

    ``` class TestView: UIView { enum Dimension { static let left = 18.0 static let right = 18.0 static let top = 10 static let bottom = 10 }

    // ... }

    enum Math { static let π = 3.14159265358979323846264338327950288 static let e = 2.71828182845904523536028747135266249 static let u = 1.45136923488338105028396848589202744 } ```

  • API Endpoints

    App/Module 內網絡請求 (API) 模型也可以用 Enum 來定義,API 參數通過關聯值綁定

    著名網絡庫 Moya 就是基於這個思想:

    ``` public enum GitHub { case zen case userProfile(String) case userRepositories(String) }

    extension GitHub: TargetType { public var baseURL: URL { URL(string: "http://api.github.com")! } public var path: String { switch self { case .zen: return "/zen" case .userProfile(let name): return "/users/(name.urlEscaped)" case .userRepositories(let name): return "/users/(name.urlEscaped)/repos" } } public var method: Moya.Method { .get }

    public var task: Task {
        switch self {
        case .userRepositories:
            return .requestParameters(parameters: ["sort": "pushed"], encoding: URLEncoding.default)
        default:
            return .requestPlain
        }
    }
    

    }

    let provider = MoyaProvider() provider.request(.zen) { result in // ... } ```

    如上,將 GitHub 相關的 API (zenuserProfileuserRepositories) 封裝在 enum GitHub 中。

    最終通過 provider.request(.zen) 的方式發起請求。

    強烈建議大家讀一下 Moya 源碼 - UI State

可以將頁面各種狀態封裝在 Enum 中:

swift enum UIState<DataType, ErrorType> { case loading case empty case loaded(DataType) case failured(ErrorType) }

  • Associated-Values case 可以當作函數用

    一般用於函數式 mapflatMap,如 Array、Optional、Combine 等:

    ``` func uiState(_ loadedData: String?) -> UIState { // 等價於 loadedData.map { UIState.loaded($0) } ?? .empty loadedData.map(UIState.loaded) ?? .empty }

    // loadedTmp 本質上就是個函數:(String) -> UIState let loadedTmp = UIState.loaded) ```

    CaseAsFunction.png

小結

Swift Enum 功能非常強大,具備很多傳統上只有 Class 才有的特性。

關聯值進一步極大豐富了 Enum 的使用場景。

對於一組具有相關性的 「值」、「狀態」、「操作」等,都可以用 Enum 封裝。

鼓勵優先考慮使用 Enum。

參考資料

Documentation-enumerations

GitHub - Moya/Moya: Network abstraction layer written in Swift

GitHub - Alamofire/Alamofire: Elegant HTTP Networking in Swift

http://www.swiftbysundell.com/articles/powerful-ways-to-use-swift-enums/

http://appventure.me/guides/advanced_practical_enum_examples/introduction.html