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 协议中的:initformUnionformIntersectionformSymmetricDifference 办法。

    在 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 的数

Array_Map.png

其实,真正需求履行 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-LazySequence-LazyMapSequence.png

能够看到,其履行流程与一般的 array-map 有很大的区别:

  • ArrayDictionary 等 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-SomeVaules.png

  • LazySequence,其间的 mapfilter 等操作是「懒」履行

    A sequence containing the same elements as a Base sequence, but on which some operations such as map and filter are implemented lazily.

    — LazySequence – Apple Developer Documentation

    @frozen struct LazySequence<Base> where Base : Sequence
    

    关于 LazySequence 本身咱们不用太重视,很少直接用它,一般都是经过 Array、Dictionary 的 lazy 特点直接获取

  • LazySequence 类型的实例做 map 操作返回的是 LazyMapSequence 类型的成果

    LazyMapSequence.png

    能够简单的理解为,LazyMapSequence 存储的是 map-closure,而不是 map 履行后的成果:

    Array-LazySequence-LazyMapSequence.png

运用

能够看到,LazySequence 与一般的 Collection 在履行流程上有很大的差异,在正常开发中应防止运用,除非有性能问题。

另外,只有在取部分成果的场景下才有意义,如 firstfirst(where:)prefix 等。

在 GitHub – BetterCodable 中做 Lossless Decode 时用到 lazy:

Codable-LosslessValueCodable.png

LosslessDefaultStrategy.png

看一个有意思的问题:

找出最小的 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.png

如上图:

  • 不同类型的 Range 都完成了 RangeExpression 协议

    其间最重要的办法便是 contains,判别某个值是否在指定区间内

  • 不仅 Int 能够用于表明 range,一切完成了 Comparable 协议的类型都能够

    如,DoubleString (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...inforEach

    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"]
    

    需求留意的 prefixValuesprefixToVaulessuffixValuesmiddleValues 的类型是 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 的数据

    ArraySlice.png

由于 ArrayArraySlice 同享数据:

  • 不建议长时间持有 ArraySlice (不要作为 classstruct 的特点,只应作为局部变量运用),由于 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])

    middlevalues-index.png

    middlevalues-outofbounds.png

    因此,对 slice 做下标相关操作时需分外慎重,相关操作应该基于 ArraySlice 供给的 startIndexendIndex

    slice 的有用取值区间为:startIndex..<endIndex

    middlevalues-startindex.png

Substring


StringSubstring 的联系十分类似于 ArrayArraySlice

// 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 associated RawValue type without losing the value of the original RawRepresentable type. Using the raw value of a conforming type streamlines interoperation with Objective-C and legacy APIs and simplifies conformance to other protocols, such as EquatableComparable, and Hashable.

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 (IntString …) 间能够「 无损互转 」

    • 经过 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 中 IntStringArrayDictionary 都是 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 后,就能够用相应的「字面量」赋值

没错,IntStringArrayDictionary 别离完成了上述 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