本文的知识点会比较散,是基础语法之外的一些进阶内容,如果有写的不妥的地方,欢迎评论区指正~
可选值是通过枚举实现的:
enum Optional<Wrapped> {
case none
case some(Wrapped)
对于Optional<Wrapped>
类型的值可以通过switch来处理,比如case .some(Wrapped)
,简写为case let Wrapped?
,当然解包可选值最简单的方法莫过于用if/guard let
的语法。
当需要可选值的时候,传入非可选值会隐式转换成可选值。
while let
当条件返回nil
时终止循环:
let array = [1, 2, 3]
var iterator = array.makeIterator()
while let i = iterator.next() {
print(i, terminator: " ")
} // 1 2 3
还有一种for...where
的用法,表示满足条件才会执行循环:
for i in 0..<10 where i % 2 == 0 {
print(i, terminator: " ")
} // 0 2 4 6 8
let stringNumbers = ["1", "2", "three"]
let maybeInts = stringNumbers.map { Int($0) } // [Optional(1), Optional(2), nil]
如果用 maybeInts.makeIterator().next()
遍历元素,其实每次返回的都是Int??
,相当于解两次包。
如果想筛选出数组中不为nil
的元素,可以用:
for case let i? in maybeInts {
// i 将是 Int 值,而不是 Int?
print(i, terminator: " ")
}
// 1 2
public enum Never { }
:无法构建Never
类型的值,绝对不会返回。guard
语句的else
路径必须退出当前域或者调用一个不会返回的函数,如fatalError()
。public typealias Void = ()
:不返回任何值。nil
表示不存在。var dictWithNils: [String: Int?] = [
"one": 1,
"two": 2,
"none": nil
]
如果dictWithNils["two"] = nil
,则字典会删除该项。如果想让"two"
对应的值为nil
,可以dictWithNils["two"] = .some(nil)
、dictWithNils["two"]? = nil
。
这两种赋值的前提都是先保障字典key
对应的可选值’存在’,然后给对应的值Int?
赋值,而如果直接给dictWithNils["two"]
赋值,赋的只是最外层字典本身的可选值,一旦这个值为nil
,那么该项也就不存在了。
闭包 = 函数 + 捕获的其局部变量外的变量。如果将闭包作为函数参数进行传递,有如下简写方式:
例如[1, 2, 3].map { $0 * 2 } // [2, 4, 6]
map
的函数接受 Int
作为参数,从闭包内的乘法结果的类型可以推断出闭包返回的也是 Int
return
。trailing closure syntax
) 在多行的闭包表达式中表现非常好。在 Swift
中定义一个和 &&
操作符具有相同功能的 and
函数:
func and(_ l: Bool, _ r: () -> Bool) -> Bool {
guard l else { return false }
return r()
}
我们可以使用 @autoclosure
标注来告诉编译器它应该将一个特定的参数用闭包表达式包装起来。
func and(_ l: Bool, _ r: @autoclosure () -> Bool) -> Bool {
guard l else { return false }
return r()
}
过度使用自动闭包可能会让你的代码难以理解,使用时的上下文和函数名应该清晰地指出实际求值会被推迟。
一个被保存在某个地方 (比如一个属性中) 等待稍后再调用的闭包就叫做逃逸闭包。相对的,永远不会离开一个函数的局部作用域的闭包就是非逃逸闭包。闭包参数默认是非逃逸的。如果你想要保存一个闭包稍后再用,你需要将闭包参数标记为 @escaping
。
对于那些使用闭包作为参数的函数,如果闭包被封装到像是元组或者可选值等类型的话,这个闭包参数也是逃逸的。因为在这种情况下闭包不是直接参数,它将自动变为逃逸闭包。这样的结果是,你不能写出一个函数,使它接受的函数参数同时满足可选值和非逃逸。很多情况下,你可以通过为闭包提供一个默认值来避免可选值。如果这样做行不通的话,可以通过重载函数,提供一个包含可选值 (逃逸) 的函数,以及一个不是可选值,非逃逸的函数来绕过这个限制:
// 如果用 nil 参数 (或者一个可选值类型的变量) 来调用函数,将使用可选值变种,而如果使用闭包字面量的调用将使用非逃逸和非可选值的重载方法
func transform(_ input: Int, with f: ((Int) -> Int)?) -> Int {
print("使用可选值重载")
guard let f = f else { return input }
return f(input)
}
func transform(_ input: Int, with f: (Int) -> Int) -> Int {
print("使用非可选值重载")
return f(input)
}
闭包可以捕获并存储其定义上下文中任何常量和变量的引用。
闭包的生命周期从它们被创建和赋值开始,一直持续到它们被销毁(即,它们已经没有任何引用指向它们)。
关于闭包与引用循环(Reference cycles)的关系,首先需要了解闭包是引用类型,不是值类型。当一个闭包被赋值给一个变量,常量或者属性,实际上被赋值的是对该闭包的引用,而不是闭包的副本。
例如,如果一个类实例有个属性指向一个闭包,并且这个闭包又捕获(也就是引用)了这个类的实例,那么就会形成一个引用循环。
通过捕获列表可以避免循环引用:
counter = 0
g = {[c = counter] in print(c)}
counter = 1
g() // 0
捕获列表位于闭包的开始部分,包含在方括号([])中,每项由一对由等号分隔的元素组成,前面的元素是引用的名称(可能带有 weak 或 unowned 的标记),后面的元素是在闭包外部的实际变量或常量。
infix operator
用来自定义一个中缀运算符。中缀运算符是处于两个操作数之间的运算符,比如加法运算符。
// 自定义运算符**表示取幂运算(即lhs的rhs次幂),并且其优先级与乘法运算相同(MultiplicationPrecedence)。
infix operator **: MultiplicationPrecedence // 使用已有的优先级group
func **(base: Int, power: Int) -> Int {
precondition(power >= 2)
var result = base
for _ in 2...power {
result *= base
}
return result
}
注意:Swift 中定义的运算符必须要有一个优先级组(precedence group)。上述代码中使用了已有的乘法优先级,你也可以自定义优先级:
precedencegroup PowerPrecedence {
associativity: right // 2 ** 3 ** 2 从右向右结合
higherThan: MultiplicationPrecedence
}
infix operator **: PowerPrecedence
let result = 2 * 3 ** 2
就会首先计算 3 ** 2
。
如果想让 **
支持更过的类型,可以考虑范型:
func **<T: BinaryInteger>(base: T, power: Int) -> T {
precondition(power >= 2)
var result = base
for _ in 2...power {
result *= base
}
return result
}
如果想在函数体内改变参数的值,可利用inout
关键词。对于inout
参数,你只能传递左值,因为右值是不能被修改的。当你在普通的函数或者方法中使用inout
时,需要显式地将它们传入:即在每个左值前面加上&
符号。
// ++ 自增运算符
postfix func ++(x: inout Int) {
x += 1
}
需要注意几点:
inout
参数逃逸,只能在函数返回前修改Swift
还是会复制传入的 inout
参数,但当函数返回时,会用这些参数的值覆盖原来的值。也就是说,即使在函数中对一个 inout
参数做多次修改,但对调用者来说只会注意到一次修改的发生,也就是在用新的值覆盖原有值的时候。同理,即使函数完全没有对 inout
参数做任何的修改,调用者也还是会注意到一次修改 (willSet
和 didSet
这两个观察者方法都会被调用)。对[]
操作符进行扩展,可以操作类、结构体、枚举等类型的属性。
class Person {
let name: String
let age: Int
init(name: String, age: Int) {
self.name = name
self.age = age
}
}
extension Person {
subscript(key: String) -> String? {
switch key {
case "name": return name
case "age": return "\(age)"
default: return nil
}
}
}
let me = Person(name: "Cosmin", age: 36)
me["name"]
me["age"]
// Subscript parameters
subscript(key key: String) -> String? {
// original code
}
me[key: "name"]
me[key: "age"]
把Subscripts
的使用转变为.
操作符,做法如下:
// 1
@dynamicMemberLookup
class Instrument {
let brand: String
let year: Int
private let details: [String: String]
init(brand: String, year: Int, details: [String: String]) {
self.brand = brand
self.year = year
self.details = details
}
// 2 可添加 class 前缀等价于 static
subscript(dynamicMember key: String) -> String {
switch key {
case "info": return "\(brand) made in \(year)."
default: return details[key] ?? ""
}
} }
键路径表达式以一个反斜杠开头,比如 \String.count
。反斜杠是为了将键路径和同名的类型属性区分开来。类型推断对键路径也是有效的,在上下文中如果编译器可以推断出类型的话,你可以将类型名省略,只留下 \.count
。”
class Tutorial {
let title: String
let details: (type: String, category: String)
init(title: String,
details: (type: String, category: String)) {
self.title = title
self.details = details
}
}
// 通过 keyPath 可以直接获取和修改属性值
let tutorial = Tutorial(
title: "Object Oriented Programming in Swift",
details: (type: "Swift",
category: "iOS")
)
let title = \Tutorial.title
let tutorialTitle = tutorial[keyPath: title]
KeyPath
可以和 MemberLookup
结合使用:
struct Point {
let x, y: Int
}
@dynamicMemberLookup
struct Circle {
let center: Point
let radius: Int
subscript(dynamicMember keyPath: KeyPath<Point, Int>) -> Int {
center[keyPath: keyPath]
}
}
let center = Point(x: 1, y: 2)
let circle = Circle(center: center, radius: 1)
// 应该是利用了类型推断
circle.x
circle.y
KeyPath
可以当作函数使用:
let titles = [tutorial].map(\.title)
NSObject
的observe(_:options:changeHandler:)
方法将会对一个 \键路径进行观察,并在属性发生变化的时候调用 handler
。不要忘记还需要将要观察的属性标记为 @objc dynamic
,否则 KVO
将不会工作。
键路径可以帮助我们在两个 NSObject
之间实现双向绑定,键路径可以让我们的代码更加泛用,而不必拘泥于某个特定的属性:
// 通过扩展 NSObjectProtocol 而不是 NSObject,我们可以使用 Self
extension NSObjectProtocol where Self: NSObject {
func observe<A, Other>(_ keyPath: KeyPath<Self, A>,
writeTo other: Other,
_ otherKeyPath: ReferenceWritableKeyPath<Other, A>)
-> NSKeyValueObservation
where A: Equatable, Other: NSObjectProtocol
{
// 注意options的取值,需要在新值变化时调用handler
return observe(keyPath, options: .new) { _, change in
guard let newValue = change.newValue,
other[keyPath: otherKeyPath] != newValue else {
return // prevent endless feedback loop
}
other[keyPath: otherKeyPath] = newValue
}
}
func bind<A, Other>(_ keyPath: ReferenceWritableKeyPath<Self,A>,
to other: Other,
_ otherKeyPath: ReferenceWritableKeyPath<Other,A>)
-> (NSKeyValueObservation, NSKeyValueObservation)
where A: Equatable, Other: NSObject
{
let one = observe(keyPath, writeTo: other, otherKeyPath)
let two = other.observe(otherKeyPath, writeTo: self, keyPath)
return (one,two)
}
}
@propertyWrapper
是 Swift 中的一个特性,用于自定义属性的获取和设置行为。
@propertyWrapper
struct ZeroTo<Value: Numeric & Comparable> {
private var value: Value
let upper: Value
init(wrappedValue: Value, upper: Value) {
value = wrappedValue
self.upper = upper
}
var wrappedValue: Value {
get { min(max(value, 0), upper) }
set { value = newValue }
}
// 可以通过 $ 获取该值, 没必要和 wrappedValue 是一个类型
var projectedValue: Value { value }
}
// 当我们在函数或闭包的参数中使用属性包装器时,编译器将帮我们生成其他必要的初始化代码。当我们调用函数时,实际参数的值将用作 wrappedValue值。
func printValueV3(@ZeroTo(upper: 10) _ value: Double) {
print("The wrapped value is", value)
print("The projected value is", $value)
}
printValueV3(42) // 10
projectedValue
的使用示例如下:
@propertyWrapper
public struct ValidatedDate {
private var storage: Date? = nil
private(set) var formatter = DateFormatter()
public init(wrappedValue: String) {
self.formatter.dateFormat = "yyyy-mm-dd"
self.wrappedValue = wrappedValue
}
public var wrappedValue: String {
set {
self.storage = formatter.date(from: newValue)
}
get {
if let date = self.storage {
return formatter.string(from: date)
} else {
return "invalid"
}
}
}
public var projectedValue: DateFormatter {
get { formatter }
set { formatter = newValue }
}
}
extension protocol
不仅可以为 protocol
提供默认实现,而且可以新增属性、方法的定义和实现。
associatedtype
用于在协议定义中声明一个占位类型,也就是类型的别名。具体的类型在协议被具体类型(如:结构体、类或枚举)遵守时才会被确定:
protocol Container {
associatedtype Item
mutating func append(_ item: Item)
var count: Int { get }
subscript(i: Int) -> Item { get }
}
struct IntArray: Container {
// 在遵守协议的类型中指定 Item 实际上是 Int 类型
typealias Item = Int
var items = [Int]()
mutating func append(_ item: Int) {
items.append(item)
}
var count: Int {
return items.count
}
subscript(i: Int) -> Int {
return items[i]
}
}
由于协议只是定义了一套共用的接口,很难将实现协议的类型关联起来,所以associatedtype
也可以为协议绑定特定类型的数据。
protocol Product {}
protocol ProductionLine {
func produce() -> Product
}
protocol Factory {
var productionLines: [ProductionLine] {get}
}
extension Factory {
func produce() -> [Product] {
var items: [Product] = []
productionLines.forEach { items.append($0.produce()) }
print("Finished Production")
print("-------------------")
return items
} }
创建一个工厂,里面的产品线可以是任意的,但是我想让一个特定的工厂只生产特定的产品,就可以采用associatedtype
或者generics
:
protocol Product {
init() }
protocol ProductionLine {
associatedtype ProductType
func produce() -> ProductType
}
protocol Factory {
associatedtype ProductType
func produce() -> [ProductType]
}
struct GenericProductionLine<P: Product>: ProductionLine {
func produce() -> P {
P()
}
}
struct GenericFactory<P: Product>: Factory {
var productionLines: [GenericProductionLine<P>] = []
func produce() -> [P] {
var newItems: [P] = []
productionLines.forEach { newItems.append($0.produce()) }
print("Finished Production")
print("-------------------")
return newItems
}
}
函数的返回值可以是遵循某些协议的非固定类型:
// return 的必须要是同一类型数据
func makeValueRandomly() -> some FixedWidthInteger {
if Bool.random() {
return Int(42)
}
else {
return Int(24)
}
}
有两点需要注意:
associatedtype
,比如some Collection<Int>
在 Swift 中,由于它是一个强类型语言,每一个类、结构体或者枚举都有自己所特有的类型。然而,往往在实际编程中,我们可能需要去抹除这些特定的类型信息,将不同的类型以一种统一的方式去处理。这就是所谓的 “类型擦除”。
类型擦除主要用在两个方面:
以 AnyCollection
的实现为例:
// 私有协议,抓住了 Collection 的公共接口
private protocol _AnyCollectionBox {
associatedtype Element
// 定义的其他需要实现的方法和属性
}
// 私有类,实现了私有协议,并持有一个 Collection
private class _CollectionBox<Base : Collection> : _AnyCollectionBox {
var _base: Base
init(_ base: Base) {
_base = base
}
// 实现了其他的方法和属性
}
public struct AnyCollection<Element> : Collection {
// 中间抽象,可以指向任何一个 Collection
internal var _box: _AnyCollectionBox<Element>
// 公共的构造方法,接收一个 Collection
public init<C : Collection>(_ collection: C) where C.Element == Element {
// 将接收到的 Collection 封装到 box 中
_box = _CollectionBox(collection)
}
// 其他 Collection 协议的方法和属性
}
可以通过以下代码把承载不同元素的容器放到同一个数组里:
let array = Array(1...10)
let set = Set(1...10)
let reversedArray = array.reversed()
let arrayCollections = [array, Array(set), Array(reversedArray)]
let collections = [AnyCollection(array),
AnyCollection(set),
AnyCollection(array.reversed())]
我对类型擦除的理解就是,对于内部有associatedtype
的协议而言,不能把他们放到一个数组里,编译器无法确定数组中的每个元素的具体类型,这是不被 Swift 允许的。所以才有了类型擦除(或许只是其中一个原因)。
额外扯一下Any
,Any
(或其他的泛型类型)并不是直接存储在数组中的,而是经过了一层封装。在 Array
的底层实现中,所有的元素都会被看作 AnyObject
类型的实例并存储在堆中。这就意味着这个 Array<Any>
是一个引用语义类型的集合,且每个元素实际上并不位于连续的内存空间中 – 相反,每个元素是一个指向堆中存储空间的引用。
当我们说 “数组中的每个 Any
元素在内存中的大小都是一样的” 时,指的是每个元素在数组中占用的内存大小一样 – 它们都是指针,指向存储在堆中的实例。无论这个实例是 Int
还是 String
或者其他的类型。
当然,如果数组元素是协议,是地址引用。
改变结构体的可变类型的值实际上是重新给结构体变量赋值,拷贝结构体时,如果结构体中有类变量,那么拷贝的是该对象的引用,即不同的结构体可能有着对同一个对象的引用。
weak
应用作用于可选类型的对象,unowned
引用不要求对象可选,但是要确保“被引用者”的生命周期比“引用者”要长。
在对象中,Swift
运行时使用另外一个引用计数来追踪 unowned
引用。当对象没有任何强引用的时候,会释放所有资源 (例如,对其他对象的引用)。然而,只要对象还有 unowned
引用存在,其自身所占用的内存就不会被回收。这块内存会被标记为无效,有时也称作僵尸内存 (zombie memory)
。被标记为僵尸内存之后,只要我们尝试访问这个 unowned
引用,就会发生一个运行时错误。
相比弱引用,unowned
引用的开销也小一点,通过它访问属性或调用方法的速度会快一点点。
写时复制的意思是,在结构体中的数据,一开始是在多个变量之间共享的:只有在其中一个变量修改了它的数据时,才会产生对数据的复制操作。集合类型(Array、Dictionary、Set、String
)都是用写时复制实现的。
如果对结构体的类对象实现写时复制,可以利用isKnownUniquelyReferenced
函数,检查一个引用类型的实例是否只有一个所有者。返回false
时对引用的对象进行深拷贝,当引用唯一时没有必要进行复制。
if case
和 guard case
判断枚举类型是否匹配的同时,可以利用模式匹配定义枚举中包含的值,避免了 switch
匹配。
实现一个单向链表:
enum List<Element> {
case end
indirect case node(Element, next: List<Element>)
/// 把一个含有值 `x` 的节点添加到链表的头部。
/// 然后返回整个链表。
func cons(_ x: Element) -> List {
return .node(x, next: self)
}
}
indirect
告诉编译器把 node
成员表示为一个引用,从而使递归起作用。枚举作为值类型是不能包含自身的,因为如果允许这样的话,在计算类型大小的时候,就会创建一个无限递归。编译器必须能够为每种类型确定一个固定且有限的尺寸。将需要递归的成员作为一个引用是可以解决这个问题的,因为引用类型在其中增加了一个间接层;并且编译器知道任何引用的存储大小总是为 8
个字节 (在一个 64
位的系统上)。当然,如果子节点是数组的话,就不需要indirect
了,因为数组内部使用一个引用类型作为存储,已经提供了所需的间接层。
让链表实现 ExpressibleByArrayLiteral
协议,使其能够使用数组字面量来初始化一个链表。在具体的实现中,首先反转作为输入的数组 (因为链表是从结尾开始构建的),然后从 .end
节点开始,使用 reduce
将元素逐个添加到链表中:
extension List: ExpressibleByArrayLiteral {
public init(arrayLiteral elements: Element...) {
// reduce 方法接受一个初始的聚合值和一个闭包,每次迭代将当前聚合值和数组中的一个元素作为输入,并返回一个新的聚合值。
self = elements.reversed().reduce(.end) { partialList, element in
partialList.cons(element)
}
}
}
let list2: List = [3,2,1]
/*
node(3, next: List<Swift.Int>.node(2, next: List<Swift.Int>.node(1,
next: List<Swift.Int>.end)))
*/
这个链表类型还有一个有趣的特性:它的可持久化。节点都是不可变的 - 一旦创建,你就无法修改它了。添加一个元素到链表中时并不会复制链表;它只是给你一个新的节点,这个节点会链接到现有列表的头部。
假如在某个版本中switch
对一个枚举类型的值穷尽了,但是很有可能在后续版本该枚举类型又增加了,为了防止之前的版本崩溃,我们一般都会加上default
的分支。这样做在编译和运行时虽然是没问题的,但是之前的代码就感知不到枚举类型的改动,如果想获取IDE
的提示,可以在default
加上@unknown
的关键词,这样就有在枚举没有穷尽时产生一个warning
:witch must be exhaustive
。
none
或 some
来命名成员。因为在模式匹配的上下文中,它们可能与 Optional
的成员发生冲突。(backtick)
。如果你使用某些关键字来作为成员名字的话 (例如,default
),类型检查器会因为无法解析代码而产生错误。你可以用反引号(``)把名字括起来使用它。(AssocValue) -> Enum
的函数。Swift
没有内置的命名空间。但我们可以用枚举来“模拟”命名空间。由于类型定义是可以嵌套的,因此外部类型可以充当其包含的所有声明的命名空间。Character
是人类阅读时理解的单个字符,也称为扩展字位簇(extended grapheme cluster)
,但它可能由多个Unicode
标量(Unicode scalars)
组成,原因在于字符集的庞大与扩张。由于Unicode
是变长编码,一个Unicode
标量可能由多个编码单元(code units)
组成,对于UTF-8
而言,会使用 1~4
个字节编码标量,所以其编码单元为UInt8
;对于UTF-16
而言,会使用 2/4
个字节编码标量,所以其编码单元为UInt16
。
// String - UTF-8; NSString - UTF-16
// 调用 count 时,结果都为 7
let single = "Pok\u{00E9}mon" // Pokémon
let double = "Poke\u{0301}mon" // Poke?mon
single.unicodeScalars.count // 7
double.unicodeScalars.count // 8
single == double // true
// 比较编码单元速度会更快
single.utf8.elementsEqual(double.utf8) // false
let nssingle = single as NSString
nssingle.length // 7
let nsdouble = double as NSString
nsdouble.length // 8
// 在 UTF-16 编码单元的层面上进行比较,可以用 NSString.compare(_:) 比较组合之后的字面量
nssingle == nsdouble // false
简单的颜文字一般由两个Unicode
标量组成,复杂点的颜文字可以通过一个标量值为 U+200D
的不可见零宽连接字符(zero-width joiner,ZWJ)
连接简单颜文字而成。例如👨?👩?👧?👦
是由👨 + ZWJ + 👩 + ZWJ + 👧 + ZWJ + 👦
构成的。
NSString
和String
在内存中的视图编码是不同的,在二者之间桥接时会消耗一定的时间和空间。假设通过String
初始化了NSMutableAttributedString
,获取其string
属性时实际上是从NSString
属性转化而来的,频繁的该操作会消耗大量时间,但是如果as NSString
就能避免这种桥接,这是Swift
内部的优化。
做字符串拼接时,字符的数量不等于两个字符串的字符总和,存在相邻字符拼接成新的字位簇的情况。
当使用字符串的索引访问字符时,需要使用String.Index
,而不是普通的整数索引。字符和标量并不是一一对应的关系,就算知道给定字符串中第 n
个字符的位置,也并不会对计算这个字符之前有多少个 Unicode 标量
有任何帮助。
随机访问字符串并不是O(1)
的时间复杂度,查找第n
个字符要检查内存中该字符之前所有的字符。而且无法通过下标替换字符,只能用replaceSubrange
。
字符串类型 String
的 indices
属性是一个包含所有字位簇的索引集合。这些索引表示了在字符串中每个字符的开头位置,不论这个字符是由单个 Unicode 标量
还是多个 Unicode 标量
组成的。
子字符串 SubString
会一直持有整个原始字符串。如果有一个巨大的字符串,它的一个只表示单个字符的子字符串将会在内存中持有整个字符串。即使当原字符串的生命周期本应该结束时,只要子字符串还存在,这部分内存就无法释放。长期存储子字符串实际上会造成内存泄漏,由于原字符串还必须被持有在内存中,但是它们却不能再被访问。
将一个 String
转为 SubString
最快的方式是用不指定任何边界的范围操作符:str[...]
Unicode.Scalar.Properties
有着丰富的属性列表。
现在列出字符串中每一个标量的编码点、名称和一般分类只需要对字符串做一点格式化就行了:
"I’m a 👩🏽?🚒.".unicodeScalars.map { scalar -> String in
let codePoint = "U+\(String(scalar.value, radix: 16, uppercase: true))"
let name = scalar.properties.name ?? "(no name)"
return "\(codePoint): \(name) – \(scalar.properties.generalCategory)"
}.joined(separator: "\n")
@resultBuilder
是 Swift 5.4 引入的一个新特性,主要用于简化构建和处理复杂对象的代码。在 SwiftUI 框架和 Swift 的函数式 API 中已经有它的身影。它可以将一组值经过特定的转换和计算构建为一个结果对象,有点类似于声明式编程的概念。
例如,我们需要创建一个HTML网页,网页包含head标签和body标签,每个标签又可能包含若干子标签。如果没有@resultBuilder
,我们需要创建每个标签,设置属性,再将这个标签添加到其父标签下,代码会显得繁琐。
struct HTMLTag: HTMLRepresentable {
var tag: String
var children: [HTMLRepresentable] = []
init(tag: String) {
self.tag = tag
}
init(@HTMLBuilder _ content: () -> HTMLRepresentable...) {
self.tag = "div"
self.children = content()
}
init(tag: String, @HTMLBuilder _ content: () -> HTMLRepresentable...) {
self.tag = tag
self.children = content()
}
}
@resultBuilder
struct HTMLBuilder {
static func buildBlock(_ components: HTMLRepresentable...) -> HTMLRepresentable {
let tag = HTMLTag(tag: "html")
components.forEach { tag.add(child: $0) }
return tag
}
}
func buildPage(@HTMLBuilder content: () -> HTMLRepresentable) -> String {
let tag = content()
return tag.htmlString
}
let page = buildPage {
HTMLTag(tag: "head")
HTMLTag(tag: "body") {
HTMLTag(tag: "h1") { "Hello, world!" }
}
}
print(page)
@HTMLBuilder
可以定义一个或多个如下方法:
buildExpression
: 处理单个表达式的结果。这通常是处理基本类型,例如 HTML 标签的字符串,或者自定义类型例如 HTMLTag。
buildBlock
: 把 buildExpression 的所有结果合并成一个结果。例如将多个 HTMLTag 合并为一个。
buildOptional
: 处理可选表达式(只有 if 一个分支)。例如你有一个返回 HTMLTag? 类型的表达式,这个方法定义了如何处理 nil 。
buildEither
: 处理条件语句(if/else 分支、switch语句)。例如有一个条件语句,根据不同的条件返回不同的 HTMLTag。
buildArray
: 处理数组表达式,例如有一个返回 [HTMLTag] 的表达式,这个方法定义了如何处理整个数组。通常是 for…each,每次循环生成一个结果,遍历完成之后将结果收集到一个数组中进行处理。
Concurrency
是Swift
新引入的一套实现并发的机制。它包括了一系列工具和语言特性,使得在 Swift 中进行异步编程变得更容易。
Swift中的async/await语法允许你书写看上去像是同步代码的异步代码。一个函数可以被声明为async,这意味着你可以在函数体内使用await关键字。当你 await 一个异步操作的时候,你的函数会被挂起,然后在异步操作结束后再继续执行,从而让其它代码可以在这个异步操作运行时继续执行。
Swift中的Task是一种可以并发运行的独立工作的抽象,一个Task可以在后台执行一个异步操作。你可以创建一个新的Task来执行一个async闭包。而TaskGroup则是一种可以并发运行许多Task的容器。
AsyncSequence是一种异步生成和处理元素序列的方法。它类似于同步的Sequence协议,但其元素是异步产生的。例如,你可以创建一个从文件中逐行读取内容的AsyncSequence,每次循环时,它只会读取和处理一个行,而不需要将整个文件加载到内存中。
func findTitle(url: URL) async throws -> String? {
for try await line in url.lines {
if line.contains("<title>") {
return line.trimmingCharacters(in: .whitespaces)
}
}
return nil
}
对于没有执行顺序的异步代码,可以聚合到一起利用多个线程执行:
func findTitlesParallel(first: URL, second: URL) async throws ->
(String?,
String?) {
async let title1 = findTitle(url: first) // 1
async let title2 = findTitle(url: second) // 2
let titles = try await [title1, title2] // 3
return (titles[0], titles[1]) // 4
}
Actor是一种保证并发数据安全的模型,它通过串行化在它自身状态上执行的所有操作来提供数据安全性。Actors可以和其它Actors进行通信并对其它Actor进行异步操作,但不能直接访问其它Actors的状态。
Swift Actor 中并没有显式地使用锁来保护状态,而是通过内在设计来保证只有一个任务可以同时访问 Actor 的内部状态。这种设计更为友好,因为它避免了通常与加锁和解锁相关的竞态条件和死锁问题。
实际上,Actor 模型通过进入队列等待的方式来处理任务。当一个任务想要访问 Actor 的状态或成员函数时,这个任务将会被放入一个队列中,当其他任务完成后,此任务会按照在队列中的顺序被执行。这由编译器和运行时系统来管理,对开发者来说是透明的。
因此,也可以将 Actor 看作一个自动处理任务队列和确保数据安全的类,保证了在内部的数据访问和修改是安全的,
简单来说,尽管没有显式地锁定 Actor 中的状态,但通过这种内建的排队和任务管理机制,Actor 还是确保了其状态的访问足够安全,一个时刻只有一个任务在访问或修改状态。
actor Counter {
private var value = 0
func increment() {
value += 1
print(value)
}
}
// 使用 Actor
let counter = Counter()
Task {
// 需要使用 await 关键字来调用 Actor 中的方法
await counter.increment()
}
Actor 可以实现协议,如果协议的方法要求是串行的,但是属性访问默认都是异步的,此时可以添加nonisolated
关键词作actor isolation
。
extension Counter: CustomStringConvertible {
nonisolated var description: String {
"\(value)"
}
}
Sendable 协议是 Swift 中保证数据在并发环境下安全传递的一种手段。要理解这个问题,我们需要明白在并发环境下可能出现的一些问题。
当我们在说“传递数据的安全性”时,我们主要是想要避免数据非预期的并发修改。考虑这样的情况:你有两个并发执行的任务A和B。任务A创建了一个数据并送往任务B,当B使用这个数据的同时,A更改了这个数据。这样,B在使用的可能并非是你想要的数据,也许在B看到的数据和A发生变更之间存在微秒级的时间差,就可能导致数据异常或者程序错误。
即使在外部通过 Mutex(互斥锁)或者 Actor 来对数据进行保护,这样的问题仍然有可能发生。这是因为 Mutex 或 Actor 主要是保护数据的读者(接收者)和写者(发送者)之间的同步,而非保护同一份数据的多份拷贝。也就是说,即使是 Actor 并不能阻止一份数据在其他地方被修改。
为了解决这个问题,我们就要求传递的数据要是 Sendable 协议。如果一个类型符合 Sendable 协议,那么它的实例就可以安全地在多个并发执行的任务中共享。Swift 通过保证值类型在复制时拥有唯一的数据,和限制引用类型在遵守 Sendable 时是只读或者线程安全的,来防止这类问题。
举个例子,如果你有一个遵守 Sendable 的结构体,当你在任务A中创建这个结构体的实例并发送给任务B,实际传递给任务B的是这个实例的一份新的复制品,因此任务A中的原始数据修改并不会影响任务B的副本。
这就是为什么数据需要符合 Sendable 协议。这为我们在并发环境下处理数据提供了额外的安全性保障。
具体来说,Sendable 具有以下安全性保证:
值类型(如 Int,String,Array,等):由于值类型在传递时总是被复制,所以每个使用值的任务都有自己的独立副本,这就消除了数据竞争的可能性。因此,Swift 的许多值类型默认就遵循了 Sendable 协议。
引用类型(比如类和闭包):在默认情况下,引用类型是不遵从 Sendable 协议的,因为它们可能有可变状态,如果在无同步措施的情况下在多个任务间共享,可能引发数据竞争。然而,如果一个引用类型是只读的,或者内部状态的访问已经做了同步处理(例如,使用了锁或采用了 actor 模型),那么该类型就可以遵循 Sendable 协议。
自定义的符合 Sendable 的类型:可以使用 @Sendable 属性标记自己的自定义类型或函数以表明它们是 Sendable 的。但使用前需要确保其在并发环境下是安全的。
因此,只有遵守 Sendable 协议的数据才能在并发环境中安全地传递,这是因为 Sendable 的设计目标就是保证数据在并发传递过程中的安全性,避免可能的数据竞争。