Swift 最佳實踐之 Enum
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 Valuelet 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
case end(NodeType) indirect case link(NodeType, next: LinkNode) }
func sum(rootNode: LinkNode
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 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
協議:
// 由於 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,如
EncodingError
、DecodingError
:``` 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.
正如 Apple 官方文檔所言,定義錯誤模型是 Enum 的典型應用場景之一,如上節提到的
EncodingError
、DecodingError
。將不同的錯誤類型定義為 case,錯誤相關信息以關聯值的形式附加在相應 case 上。
在著名網絡庫 Alamofire 中也有很好的應用,如:
`` public enum AFError: Error { /// The underlying reason the
.multipartEncodingFailederror occurred. public enum MultipartEncodingFailureReason { /// The
fileURLprovided for reading an encodable body part isn't a file
URL. case bodyPartURLInvalid(url: URL) /// The filename of the
fileURLprovided has either an empty
lastPathComponentor
pathExtension. case bodyPartFilenameInvalid(in: URL) /// The file at thefileURL
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 定義了命名空間
Publishers
、Subscribers
:如上,通過命名空間
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: "https://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 (
zen
、userProfile
、userRepositories
) 封裝在 enumGitHub
中。最終通過
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 可以當作函數用
一般用於函數式
map
、flatMap
,如 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) ```
小結
Swift Enum 功能非常強大,具備很多傳統上只有 Class 才有的特性。
關聯值進一步極大豐富了 Enum 的使用場景。
對於一組具有相關性的 「值」、「狀態」、「操作」等,都可以用 Enum 封裝。
鼓勵優先考慮使用 Enum。
參考資料
GitHub - Moya/Moya: Network abstraction layer written in Swift
GitHub - Alamofire/Alamofire: Elegant HTTP Networking in Swift
https://www.swiftbysundell.com/articles/powerful-ways-to-use-swift-enums/
https://appventure.me/guides/advanced_practical_enum_examples/introduction.html
- Swift 最佳實踐之 Closure
- Swift 最佳實踐之 Optional
- Swift 最佳實踐之 Enum
- 深入淺出 Flutter Framework 之自定義渲染型 Widget
- 深入淺出 Flutter Framework 之 RenderObject
- Swift 新併發框架之 Task
- Swift 新併發框架之 actor
- Swift 新併發框架之 Sendable
- Swift 新併發框架之 async/await
- Swift Protocol 背後的故事(上)
- 『碼』出高質量
- 深入淺出 Flutter Framework 之 PipelineOwner
- 深入淺出 Flutter Framework 之 Layer
- 深入淺出 Flutter Framework 之 PaintingContext
- 深入淺出 Flutter Framework 之 Element