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<NodeType> {
enum LinkNode<NodeType> {
case end(NodeType)
indirect case link(NodeType, next: LinkNode)
}
func sum(rootNode: LinkNode<Int>) -> 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 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<Wrapped> : 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<Success, Failure> 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.
(Documentation-errorhandling)
正如 Apple 官方文档所言,界说过错模型是 Enum 的典型运用场景之一,如上节说到的
EncodingError
、DecodingError
。将不同的过错类型界说为 case,过错相关信息以相关值的形式附加在相应 case 上。
在闻名网络库 Alamofire 中也有很好的运用,如:
public enum AFError: Error { /// The underlying reason the `.multipartEncodingFailed` error occurred. public enum MultipartEncodingFailureReason { /// The `fileURL` provided for reading an encodable body part isn't a file `URL`. case bodyPartURLInvalid(url: URL) /// The filename of the `fileURL` provided has either an empty `lastPathComponent` or `pathExtension. 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 界说了命名空间
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 中:
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<String, Error> { // 等价于 loadedData.map { UIState.loaded($0) } ?? .empty loadedData.map(UIState.loaded) ?? .empty } // loadedTmp 本质上便是个函数:(String) -> UIState<String, Error> let loadedTmp = UIState<String, Error>.loaded)
小结
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
www.swiftbysundell.com/articles/po…
appventure.me/guides/adva…