Swift 最佳实践之 Advanced Collections
Swift 作为现代、高效、安全的编程语言,其背后有很多高级特性为之支撑。
『 Swift 最佳实践 』系列对常用的语言特性逐个进行介绍,助力写出更简洁、更高雅的 Swift 代码,快速完成从 OC 到 Swift 的转变。
该系列内容首要包括:
- Optional
- Enum
- Closure
- Protocol
- Generics
- Property Wrapper
- Error Handling
- Advanced Collections
- Pattern Matching
- Metatypes(.self/.Type/.Protocol)
- High Performance
ps. 本系列不是入门级语法教程,需求有一定的 Swift 基础
本文是系列文章的第八篇,首要介绍 Swift 中一些高级调集类型,如:OptionSet、LazySequence、Range、ArraySlice、Substring 等。
Overview
充分利用 OptionSet、LazySequence、Range、ArraySlice、Substring 等调集的特性能够写出更高雅、高效的代码。
进一步了解其背后的完成机制对代码健壮性也十分重要。
本文将对这些调集的常用特性以及背后完成机制打开详细介绍。
OptionSet
任务:规划一个办法给 View 设置圆角,要求能够经过参数指定哪些角需求设成圆角(LeftTop/RightTop/LeftBottom/RightBottom)
接口该怎样规划?
关键是需求设成圆角的「角」怎样传递?
enum?好像不合适
此正是 OptionSet
的用武之地
// 👇
public struct RectCorner: OptionSet {
public let rawValue: Int
public init(rawValue: Int) {
self.rawValue = rawValue
}
// 👇
public static let leftTop = RectCorner(rawValue: 1 << 0)
public static let rightTop = RectCorner(rawValue: 1 << 1)
public static let leftBottom = RectCorner(rawValue: 1 << 2)
public static let rightBottom = RectCorner(rawValue: 1 << 3)
public static let all: RectCorner = [.leftTop, .rightTop, .leftBottom, .rightBottom]
}
extension UIView {
func corner(_ corners: RectCorner, radius: CGFloat) -> UIView {
if corners.contains(.leftTop) {
// ...
} else if corners.contains(.rightTop) {
// ...
} else if corners.contains(.leftBottom) {
// ...
} else if corners.contains(.rightBottom) {
// ...
}
}
}
view.corner([.leftTop, .rightBottom], radius: 5)
-
OptionSet
是个协议public protocol OptionSet : RawRepresentable, SetAlgebra { associatedtype Element = Self init(rawValue: Self.RawValue) }
SetAlgebra
中界说了一切跟Set
相关的操作:public protocol SetAlgebra<Element> : Equatable, ExpressibleByArrayLiteral { associatedtype Element init() func contains(_ member: Self.Element) -> Bool func union(_ other: Self) -> Self func intersection(_ other: Self) -> Self func symmetricDifference(_ other: Self) -> Self mutating func insert(_ newMember: Self.Element) -> (inserted: Bool, memberAfterInsert: Self.Element) mutating func remove(_ member: Self.Element) -> Self.Element? mutating func update(with newMember: Self.Element) -> Self.Element? mutating func formUnion(_ other: Self) mutating func formIntersection(_ other: Self) // ... }
-
其间的
rawValue
一般为Int
(FixedWidthInteger
)如果
rawValue
不是FixedWidthInteger
,则需求手动完成SetAlgebra
协议中的:init
、formUnion
、formIntersection
、formSymmetricDifference
办法。在 OptionSet extension 中有条件地(Self.RawValue: FixedWidthInteger)完成了它们:
extension OptionSet where Self.RawValue : FixedWidthInteger { @inlinable public init() @inlinable public mutating func formUnion(_ other: Self) @inlinable public mutating func formIntersection(_ other: Self) @inlinable public mutating func formSymmetricDifference(_ other: Self) }
-
将一切 option 别离界说为:
static let
-
能够经过
[]
的方式界说 option 的组合,如:[.leftTop, .rightTop]
enum vs. OptionSet
- enum — 用于表明一组「互斥」联系
- OptionSet — 用于表明「组合」联系
需求留意的是,「互斥」、「组合」与运用场景相关,如上用于表明 4 个方位的「LeftTop、RightTop、LeftBottom、RightBottom」:
- 在
func corner(_ corners: RectCorner, radius: CGFloat) -> UIView
场景下便是组合联系,运用OptionSet
界说之 - 如在表明某个物体当前所在方位时,运用
enum
,由于同一个物体不可能一起坐落 2 个不同的方位上
NS_OPTIONS
OC 下的 NS_OPTIONS
在 Swift 下会转成 OptionSet
,如:
typedef NS_OPTIONS(NSInteger, Direction) {
DirectionNone = 0,
DirectionLeft = 1 << 0,
DirectionRight = 1 << 1,
DirectionTop = 1 << 2,
DirectionBottom = 1 << 3
};
在 Swift 下:
public struct Direction : OptionSet, @unchecked Sendable {
public init(rawValue: Int)
public static var left: Direction { get }
public static var right: Direction { get }
public static var top: Direction { get }
public static var bottom: Direction { get }
}
运用
JSONEncoder
Swift 规范库中操控 JSON 解码输出格局的 OutputFormatting
:
open class JSONEncoder {
/// The formatting of the output JSON data.
public struct OutputFormatting : OptionSet, Sendable {
public let rawValue: UInt
public init(rawValue: UInt)
public static let prettyPrinted: JSONEncoder.OutputFormatting
public static let sortedKeys: JSONEncoder.OutputFormatting
public static let withoutEscapingSlashes: JSONEncoder.OutputFormatting
}
}
UIView
UIView 相关的很多从 NS_OPTIONS
转化过来的:
public struct AutoresizingMask : OptionSet, @unchecked Sendable {
public init(rawValue: UInt)
public static var flexibleLeftMargin: UIView.AutoresizingMask { get }
public static var flexibleWidth: UIView.AutoresizingMask { get }
public static var flexibleRightMargin: UIView.AutoresizingMask { get }
public static var flexibleTopMargin: UIView.AutoresizingMask { get }
public static var flexibleHeight: UIView.AutoresizingMask { get }
public static var flexibleBottomMargin: UIView.AutoresizingMask { get }
}
SwiftUI
SwiftUI layout 时描述「边」:
@frozen public enum Edge : Int8, CaseIterable {
/// An efficient set of `Edge`s.
@frozen public struct Set : OptionSet {
public let rawValue: Int8
public init(rawValue: Int8)
public static let top: Edge.Set
public static let leading: Edge.Set
public static let bottom: Edge.Set
public static let trailing: Edge.Set
public static let all: Edge.Set
public static let horizontal: Edge.Set
public static let vertical: Edge.Set
}
}
Moya
GitHub – Moya 中用于表明网络恳求的哪些部分将打到 log 里:
public extension NetworkLoggerPlugin.Configuration {
struct LogOptions: OptionSet {
public let rawValue: Int
public init(rawValue: Int) { self.rawValue = rawValue }
/// The request's method will be logged.
public static let requestMethod: LogOptions = LogOptions(rawValue: 1 << 0)
/// The request's body will be logged.
public static let requestBody: LogOptions = LogOptions(rawValue: 1 << 1)
/// The request's headers will be logged.
public static let requestHeaders: LogOptions = LogOptions(rawValue: 1 << 2)
/// The request will be logged in the cURL format.
public static let formatRequestAscURL: LogOptions = LogOptions(rawValue: 1 << 3)
/// The body of a response that is a success will be logged.
public static let successResponseBody: LogOptions = LogOptions(rawValue: 1 << 4)
/// The body of a response that is an error will be logged.
public static let errorResponseBody: LogOptions = LogOptions(rawValue: 1 << 5)
//Aggregate options
/// Only basic components will be logged.
public static let `default`: LogOptions = [requestMethod, requestHeaders]
/// All components will be logged.
public static let verbose: LogOptions = [requestMethod, requestHeaders, requestBody,
successResponseBody, errorResponseBody]
}
}
Alamofire
GitHub – Alamofire 中将下载好的文件移到指定方位时履行的操作:
- 要不要创建中间目录
- 要不要删去老的文件
public class DownloadRequest: Request {
/// A set of options to be executed prior to moving a downloaded file from the temporary `URL` to the destination
/// `URL`.
public struct Options: OptionSet {
/// Specifies that intermediate directories for the destination URL should be created.
public static let createIntermediateDirectories = Options(rawValue: 1 << 0)
/// Specifies that any previous file at the destination `URL` should be removed.
public static let removePreviousFile = Options(rawValue: 1 << 1)
public let rawValue: Int
public init(rawValue: Int) {
self.rawValue = rawValue
}
}
}
LazySequence
let someValues = ["1", "3", "8", "5", "8", "10", "7", "6"]
let _ =
someValues.map {
print("In map: ", $0)
return Int($0) ?? 0 // 👈, 若此处操作十分耗时⚡️
}.first {
print("In first-where:", $0)
return $0 > 3
}
如上代码,先将 String --> Int
,再找到第一个大于 3
的数
整个履行进程:
In map: 1
In map: 3
In map: 8
In map: 5
In map: 8
In map: 10
In map: 7
In map: 6
In first-where: 1
In first-where: 3
In first-where: 8
- 先将整个 [String] –> [Int]
- 在成果 [Int] 中找到第一个大于 3 的数
其实,真正需求履行 map(String --> Int)
操作的就前 3 个数:”1″、”3″、”8″,由于 “8” 满足要求。
如果 map
操作十分耗时❓
有没有优化措施,减少无谓的 map
操作?
当然有了:
// 👇
let _ = someValues.lazy.map {
print("In lazy map:", $0)
return Int($0) ?? 0
}.first {
print("In lazy first-where:", $0)
return $0 > 3
}
其履行流程:
In lazy map: 1
In lazy first-where: 1
In lazy map: 3
In lazy first-where: 3
In lazy map: 8
In lazy first-where: 8
能够看到,其履行流程与一般的 array-map 有很大的区别:
-
Array
、Dictionary
等 Collections 都供给了核算特点lazy
,将其转成LazySequence
@frozen public struct Array<Element> { /// A sequence containing the same elements as this sequence, /// but on which some operations, such as `map` and `filter`, are /// implemented lazily. @inlinable public var lazy: LazySequence<Array<Element>> { get } }
@frozen public struct Dictionary<Key, Value> where Key : Hashable { /// A sequence containing the same elements as this sequence, /// but on which some operations, such as `map` and `filter`, are /// implemented lazily. @inlinable public var lazy: LazySequence<Dictionary<Key, Value>> { get } }
-
LazySequence,其间的
map
、filter
等操作是「懒」履行A sequence containing the same elements as a
Base
sequence, but on which some operations such asmap
andfilter
are implemented lazily.— LazySequence – Apple Developer Documentation
@frozen struct LazySequence<Base> where Base : Sequence
关于
LazySequence
本身咱们不用太重视,很少直接用它,一般都是经过 Array、Dictionary 的lazy
特点直接获取 -
对
LazySequence
类型的实例做map
操作返回的是LazyMapSequence
类型的成果能够简单的理解为,
LazyMapSequence
存储的是map-closure
,而不是map
履行后的成果:
运用
能够看到,LazySequence 与一般的 Collection 在履行流程上有很大的差异,在正常开发中应防止运用,除非有性能问题。
另外,只有在取部分成果的场景下才有意义,如 first
、first(where:)
、prefix
等。
在 GitHub – BetterCodable 中做 Lossless Decode 时用到 lazy:
看一个有意思的问题:
找出最小的 5 个数:
- 它们都大于 100
- 它们的立方根都为整数
老实巴交:
var results = [Int]()
var curValue = 1
while curValue * curValue * curValue <= 100 {
curValue += 1
}
(1...5).forEach { _ in
results.append(curValue * curValue * curValue)
curValue += 1
}
小聪明:
let lazyResults = (1...)
.lazy
.map { $0 * $0 * $0 }
.filter({ $0 > 100} )
let results = Array(lazyResults.prefix(5))
Range
Swift Range 供给了十分强壮的能力,充分利用其特性能够写出十分高雅的代码。
Swift Range 有三类:
- Closed Range —
a...b
- Half-open Range —
a..<b
- One-sided Range —
a...
、...b
、..<b
如上图:
-
不同类型的 Range 都完成了
RangeExpression
协议其间最重要的办法便是
contains
,判别某个值是否在指定区间内 -
不仅
Int
能够用于表明 range,一切完成了Comparable
协议的类型都能够如,
Double
、String
(1.0...14.0
、"a"..."z"
)
运用
-
有用取值区间校验
如:ph 值有用取值区间为 0.0~14.0
func isVaildPH(_ value: Double) -> Bool { (0.0...14.0).contains(value) // value >= 0.0 && value <= 14.0 }
校验 http status code:
func isSuccessHTTPStatusCode(_ statusCode: Int) -> Bool { (200..<300).contains(statusCode) }
-
for...in
、forEach
for index in 1..<7 { // ... } (1...31).forEach { i in // ... }
-
switch
switch score { case 0..<60: print("Failed.") case 60..<85: print("OK.") default: print("Good!") }
-
Slicing collections
var someValues = ["1", "3", "8", "5", "8", "10", "7", "6"] var prefixValues = someValues[...3] // ["1", "3", "8", "5"] var prefixToVaules = someValues[..<3] // ["1", "3", "8"] var suffixValues = someValues[4...] // ["8", "10", "7", "6"] var middleValues = someValues[1...5] // ["3", "8", "5", "8", "10"]
需求留意的
prefixValues
、prefixToVaules
、suffixValues
、middleValues
的类型是ArraySlice
而非Array
ArraySlice
如上,对 Array
做切片得到的成果类型不是 Array
而是 ArraySlice
:
// A slice of an Array, ContiguousArray, or ArraySlice instance.
//
@frozen public struct ArraySlice<Element> {}
-
ArraySlice
具有与Array
相同的接口,故能用Array
的当地都能用ArraySlice
也便是在运用时不必过多介意详细类型是
Array
仍是ArraySlice
-
由
Array
生成ArraySlice
时,并没有产生内存的 alloc、copy 等操作,ArraySlice
彻底同享Array
的数据
由于 Array
与 ArraySlice
同享数据:
-
不建议长时间持有
ArraySlice
(不要作为class
、struct
的特点,只应作为局部变量运用),由于 slice 会强持有整个 array,可能会呈现内存问题关于需求长时间持有的,应将 slice 转成 array:
let newStorage = Array(middleValues)
Long-term storage of
ArraySlice
instances is discouraged. A slice holds a reference to the entire storage of a larger array, not just to the portion it presents, even after the original array’s lifetime ends. Long-term storage of a slice may therefore prolong the lifetime of elements that are no longer otherwise accessible, which can appear to be memory and object leakage.— ArraySlice – Apple Developer Documentation
-
slice 与 array 同享相同的 index,slice 开始 index 纷歧定是 0 💥⚡️
如下,
middleValues
index 从 1 开始,而不是 0(
var middleValues = someValues[1...5]
)因此,对
slice
做下标相关操作时需分外慎重,相关操作应该基于ArraySlice
供给的startIndex
、endIndex
:slice
的有用取值区间为:startIndex..<endIndex
Substring
String
与 Substring
的联系十分类似于 Array
与 ArraySlice
:
// When you create a slice of a string, a `Substring` instance is the result.
//
@frozen public struct Substring : Sendable {}
- 同享存储
- 同享 Index
-
Substring
具有与String
相同的接口
let greeting = "Hello, world!"
let index = greeting.firstIndex(of: ",") ?? greeting.endIndex // 5
// beginning、ending is instance of `Substring`
//
let beginning = greeting[..<index] // "Hello"
let ending = greeting[greeting.index(after: index)...] // " world!"
let wIndex = ending.firstIndex(of: "w") ?? ending.endIndex // 7
// Convert the result to a String for long-term storage.
let newString = String(beginning)
RawRepresentable
public protocol RawRepresentable<RawValue> {
associatedtype RawValue
init?(rawValue: Self.RawValue)
var rawValue: Self.RawValue { get }
}
With a
RawRepresentable
type, you can switch back and forth between a custom type and an associatedRawValue
type without losing the value of the originalRawRepresentable
type. Using the raw value of a conforming type streamlines interoperation with Objective-C and legacy APIs and simplifies conformance to other protocols, such asEquatable
,Comparable
, andHashable
.The
RawRepresentable
protocol is seen mainly in two categories of types: enumerations with raw value types and option sets.— RawRepresentable – Apple Developer Documentation
-
RawRepresentable
表明自界说类型与 Raw value (Int
、String
…) 间能够「 无损互转 」- 经过
init?(rawValue: Self.RawValue)
能够将 Raw value 转成自界说类型目标 - 经过
rawValue
特点能够获取自界说目标对应的 raw value
- 经过
-
2 个首要运用场景:带 raw value 的 enum、OptionSet
- 关于 Raw value enum 详见 Swift 最佳实践之 Enum
Expressible by Literal
var someInt: Int = 1
var someString: String = "Hello"
var someArray: Array<String> = ["Hello", "world!"]
var someDictionary: Dictionary<String, Int> = ["Key": 1]
咱们有感觉到这些赋值古怪吗?🤔
在 Swift 中 Int
、String
、Array
、Dictionary
都是 struct
,如:
@frozen public struct Int
其初始化不该该是要调用相应的 init
办法吗?
var someInt: Int = Int(1)
var someString: String = String("Hello")
var someArray: Array<String> = Array(["Hello", "world!"])
var someDictionary: Dictionary<String, Int> = Dictionary(dictionaryLiteral: ("Key", 1))
怎样直接给它们赋值了相关的字面量 (Literal)❓
Swift 为了简化初始化赋值进程,供给了一系列的协议用于「字面量赋值」:
public protocol ExpressibleByIntegerLiteral {
associatedtype IntegerLiteralType : _ExpressibleByBuiltinIntegerLiteral
init(integerLiteral value: Self.IntegerLiteralType)
}
public protocol ExpressibleByStringLiteral : ExpressibleByExtendedGraphemeClusterLiteral {
associatedtype StringLiteralType : _ExpressibleByBuiltinStringLiteral
init(stringLiteral value: Self.StringLiteralType)
}
// ExpressibleByArrayLiteral
// ExpressibleByDictionaryLiteral
// ExpressibleByBooleanLiteral
// ExpressibleByNilLiteral
完成了相关 Protocol 后,就能够用相应的「字面量」赋值
没错,Int
、String
、Array
、Dictionary
别离完成了上述 Protocol
在实际开发中,咱们也能够按需完成这些协议,如:
extension Date: ExpressibleByStringLiteral {
public init(stringLiteral value: String) {
let dateFormatter = ISO8601DateFormatter()
dateFormatter.formatOptions = [.withFullDate]
self = dateFormatter.date(from: value) ?? Date.now
}
}
// 👇
let date: Date = "2023-05-28"
print(date) // 2023-05-28 00:00:00 +0000
如上,咱们扩展了 Date
,并完成了 ExpressibleByStringLiteral
从此,能够直接将 String 类型的「字面量」赋给 Date
!
OptionSet 类型之所以能够用 Array 方式的初始化,也是由于其完成了 ExpressibleByArrayLiteral 协议,如:
public static let all: RectCorner = [.leftTop, .rightTop, .leftBottom, .rightBottom]
参考资料
OptionSet – Apple Developer Documentation
ArraySlice – Apple Developer Documentation
String – Apple Developer Documentation
LazySequence – Apple Developer Documentation
How and when to use Lazy Collections in Swift – SwiftLee
www.objc.io/blog/2018/0…
Expressible literals in Swift explained by 3 useful examples – SwiftLee
How OptionSet works inside the Swift Compiler