swift 进阶知识点

本文的知识点会比较散,是基础语法之外的一些进阶内容,如果有写的不妥的地方,欢迎评论区指正~

Optional

可选值是通过枚举实现的:

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

Never & Void & nil

  • 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,那么该项也就不存在了。

Closure

闭包 = 函数 + 捕获的其局部变量外的变量。如果将闭包作为函数参数进行传递,有如下简写方式:

例如[1, 2, 3].map { $0 * 2 } // [2, 4, 6]

  1. 如果编译器可以从上下文中推断出类型的话,你就不需要指明它了。在我们的例子中,从数组元素的类型可以推断出传递给 map的函数接受 Int 作为参数,从闭包内的乘法结果的类型可以推断出闭包返回的也是 Int
  2. 如果闭包表达式的主体部分只包括一个单一的表达式的话,它将自动返回这个表达式的结果,你可以不写 return
  3. Swift 会自动为函数的参数提供简写形式,$0 代表第一个参数,$1 代表第二个参数,以此类推。
  4. 如果函数的最后一个参数是闭包表达式的话,你可以将这个闭包表达式移到函数调用的圆括号的外部,这样的尾随闭包语法 (trailing closure syntax) 在多行的闭包表达式中表现非常好。
  5. 最后,如果一个函数除了闭包表达式外没有别的参数,那么调用的时候在方法名后面的圆括号也可以一并省略。

自动闭包

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 的标记),后面的元素是在闭包外部的实际变量或常量。

Custom operator

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参数,你只能传递左值,因为右值是不能被修改的。当你在普通的函数或者方法中使用inout时,需要显式地将它们传入:即在每个左值前面加上&符号。

// ++ 自增运算符
postfix func ++(x: inout Int) {
x += 1
}

需要注意几点:

  • 不能够让这个inout参数逃逸,只能在函数返回前修改
  • 就像对待普通的参数一样,Swift 还是会复制传入的 inout 参数,但当函数返回时,会用这些参数的值覆盖原来的值。也就是说,即使在函数中对一个 inout 参数做多次修改,但对调用者来说只会注意到一次修改的发生,也就是在用新的值覆盖原有值的时候。同理,即使函数完全没有对 inout 参数做任何的修改,调用者也还是会注意到一次修改 (willSetdidSet 这两个观察者方法都会被调用)。

Subscripts

[]操作符进行扩展,可以操作类、结构体、枚举等类型的属性。

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

Dynamic member lookup

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

KeyPath

Setting properties

键路径表达式以一个反斜杠开头,比如 \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 member lookup

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

Keypaths as functions

KeyPath 可以当作函数使用:

let titles = [tutorial].map(\.title)

NSObjectobserve(_: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)
    }
}

Property Wrappers

@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 }
  }
}

Protocol

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
  }
}

Opaque return types

函数的返回值可以是遵循某些协议的非固定类型:

// return 的必须要是同一类型数据
func makeValueRandomly() -> some FixedWidthInteger {
  if Bool.random() {
    return Int(42)
  }
else {
    return Int(24)
  }
}

有两点需要注意:

  • 不能把返回值作为函数参数
  • 不能够约束返回值的associatedtype,比如some Collection<Int>

Type Erasure

在 Swift 中,由于它是一个强类型语言,每一个类、结构体或者枚举都有自己所特有的类型。然而,往往在实际编程中,我们可能需要去抹除这些特定的类型信息,将不同的类型以一种统一的方式去处理。这就是所谓的 “类型擦除”。

类型擦除主要用在两个方面:

  • 将拥有相同行为或特性,但是类型不同的实例,包装成一种通用的类型。比如 Array、Set、Dictionary 都遵循 Collection 协议,我们可以把它们统一的看作是 AnyCollection。
  • 在泛型编程中,更好的隐藏类型的具体细节,提供更加清晰和方便的公共接口。

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 允许的。所以才有了类型擦除(或许只是其中一个原因)。

额外扯一下AnyAny (或其他的泛型类型)并不是直接存储在数组中的,而是经过了一层封装。在 Array 的底层实现中,所有的元素都会被看作 AnyObject 类型的实例并存储在堆中。这就意味着这个 Array<Any> 是一个引用语义类型的集合,且每个元素实际上并不位于连续的内存空间中 – 相反,每个元素是一个指向堆中存储空间的引用。

当我们说 “数组中的每个 Any 元素在内存中的大小都是一样的” 时,指的是每个元素在数组中占用的内存大小一样 – 它们都是指针,指向存储在堆中的实例。无论这个实例是 Int 还是 String 或者其他的类型。

当然,如果数组元素是协议,是地址引用。

Class & Struct

改变结构体的可变类型的值实际上是重新给结构体变量赋值,拷贝结构体时,如果结构体中有类变量,那么拷贝的是该对象的引用,即不同的结构体可能有着对同一个对象的引用。

weak & unowned 引用

weak 应用作用于可选类型的对象,unowned引用不要求对象可选,但是要确保“被引用者”的生命周期比“引用者”要长。

在对象中,Swift 运行时使用另外一个引用计数来追踪 unowned 引用。当对象没有任何强引用的时候,会释放所有资源 (例如,对其他对象的引用)。然而,只要对象还有 unowned 引用存在,其自身所占用的内存就不会被回收。这块内存会被标记为无效,有时也称作僵尸内存 (zombie memory)。被标记为僵尸内存之后,只要我们尝试访问这个 unowned 引用,就会发生一个运行时错误。

相比弱引用,unowned 引用的开销也小一点,通过它访问属性或调用方法的速度会快一点点。

写时复制

写时复制的意思是,在结构体中的数据,一开始是在多个变量之间共享的:只有在其中一个变量修改了它的数据时,才会产生对数据的复制操作。集合类型(Array、Dictionary、Set、String)都是用写时复制实现的。

如果对结构体的类对象实现写时复制,可以利用isKnownUniquelyReferenced函数,检查一个引用类型的实例是否只有一个所有者。返回false时对引用的对象进行深拷贝,当引用唯一时没有必要进行复制。

Enum

if caseguard 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的关键词,这样就有在枚举没有穷尽时产生一个warningwitch must be exhaustive

提示和窍门

  • 尽量避免使用嵌套 switch 语句。可以使用元组一次性匹配多个值。
  • 避免用 nonesome 来命名成员。因为在模式匹配的上下文中,它们可能与 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" // Poké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 + 👦构成的。

NSStringString在内存中的视图编码是不同的,在二者之间桥接时会消耗一定的时间和空间。假设通过String初始化了NSMutableAttributedString,获取其string属性时实际上是从NSString属性转化而来的,频繁的该操作会消耗大量时间,但是如果as NSString就能避免这种桥接,这是Swift内部的优化。

字符串索引

做字符串拼接时,字符的数量不等于两个字符串的字符总和,存在相邻字符拼接成新的字位簇的情况。

当使用字符串的索引访问字符时,需要使用String.Index,而不是普通的整数索引。字符和标量并不是一一对应的关系,就算知道给定字符串中第 n 个字符的位置,也并不会对计算这个字符之前有多少个 Unicode 标量有任何帮助。

随机访问字符串并不是O(1)的时间复杂度,查找第n个字符要检查内存中该字符之前所有的字符。而且无法通过下标替换字符,只能用replaceSubrange

字符串类型 Stringindices 属性是一个包含所有字位簇的索引集合。这些索引表示了在字符串中每个字符的开头位置,不论这个字符是由单个 Unicode 标量还是多个 Unicode 标量组成的。

子字符串 SubString 会一直持有整个原始字符串。如果有一个巨大的字符串,它的一个只表示单个字符的子字符串将会在内存中持有整个字符串。即使当原字符串的生命周期本应该结束时,只要子字符串还存在,这部分内存就无法释放。长期存储子字符串实际上会造成内存泄漏,由于原字符串还必须被持有在内存中,但是它们却不能再被访问。

将一个 String 转为 SubString 最快的方式是用不指定任何边界的范围操作符:str[...]

Uninode 标量

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

Result Builders

@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

ConcurrencySwift新引入的一套实现并发的机制。它包括了一系列工具和语言特性,使得在 Swift 中进行异步编程变得更容易。

async/await

Swift中的async/await语法允许你书写看上去像是同步代码的异步代码。一个函数可以被声明为async,这意味着你可以在函数体内使用await关键字。当你 await 一个异步操作的时候,你的函数会被挂起,然后在异步操作结束后再继续执行,从而让其它代码可以在这个异步操作运行时继续执行。

Task/TaskGroup

Swift中的Task是一种可以并发运行的独立工作的抽象,一个Task可以在后台执行一个异步操作。你可以创建一个新的Task来执行一个async闭包。而TaskGroup则是一种可以并发运行许多Task的容器。

AsyncSequence

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

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

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 具有以下安全性保证:

  1. 值类型(如 Int,String,Array,等):由于值类型在传递时总是被复制,所以每个使用值的任务都有自己的独立副本,这就消除了数据竞争的可能性。因此,Swift 的许多值类型默认就遵循了 Sendable 协议。

  2. 引用类型(比如类和闭包):在默认情况下,引用类型是不遵从 Sendable 协议的,因为它们可能有可变状态,如果在无同步措施的情况下在多个任务间共享,可能引发数据竞争。然而,如果一个引用类型是只读的,或者内部状态的访问已经做了同步处理(例如,使用了锁或采用了 actor 模型),那么该类型就可以遵循 Sendable 协议。

  3. 自定义的符合 Sendable 的类型:可以使用 @Sendable 属性标记自己的自定义类型或函数以表明它们是 Sendable 的。但使用前需要确保其在并发环境下是安全的。

因此,只有遵守 Sendable 协议的数据才能在并发环境中安全地传递,这是因为 Sendable 的设计目标就是保证数据在并发传递过程中的安全性,避免可能的数据竞争。

  • 21
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值