专注、坚持

Swift 拾遗 - Swift Tips

2021.04.16 by kingcos

Preface

《Swift 拾遗》是一个关于 Swift 的新文章专辑,这个系列的文章将不会涉及基本语法的层面,而是尝试从底层「拾」起之前所忽视的内容。那么作为起始篇,随着整个系列的进行,其中「遗」漏的基本使用内容将在本文中得到补充。

Contents

Void

按照 Swift 标准库的定义,Void 即空元组 ()

public typealias Void = ()

函数重载(Overload)

Swift 中的函数重载有一些「坑」,因此尽量不要产生命名歧义性(比如结合默认参数值等用法)。

@discardableResult

Swift 是一门要求很严格的语言,当函数的返回值未被使用到时,编译器就会提示相关的警告。我们可以使用 @discardableResult 将函数声明为可丢弃结果,即可告知编译器不产生警告:

import Foundation

func foo() -> String {
    return "kingcos.me"
}

@discardableResult
func bar() -> String {
    return "kingcos.me"
}

foo() // WARNING: Result of call to 'foo()' is unused

// 当然也可以赋值到占位符 _ 以避免警告
_ = foo()

bar()

swiftc

2

swiftc 是 Swift 编译器(前端),位于 Xcode 中 Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/ 目录,其本质是同目录下 swift 的符号链接:

-rwxr-xr-x  1 root  wheel  95341344 Jul  2 14:37 swift
lrwxr-xr-x  1 root  wheel         5 Jul 18 00:20 swiftc -> swift
  • 生成语法树:swiftc -dump-ast demo.swift
  • 生成最简洁的 SIL 代码:swiftc -emit-sil demo.swift
  • 生成 LLVM IR 代码:swiftc -emit-ir demo.swift -o demo.ll
  • 生成汇编代码:swiftc -emit-assembly demo.swift -o demo.s

1

隐式原始值

当 Swift 中的枚举原始值(Raw Value)类型为 IntString 时,其将获得默认值,即隐式原始值(Implicitly Assigned Raw Values):

enum IntFoo: Int {
    case first
    case second = 200
    case third
    case fourth = 400
    case fifth
}

print(IntFoo.first.rawValue)  // 0
print(IntFoo.second.rawValue) // 200
print(IntFoo.third.rawValue)  // 201
print(IntFoo.fourth.rawValue) // 400
print(IntFoo.fifth.rawValue)  // 401

enum StrFoo: String {
    case first
    case second = "2"
    case third
}

print(StrFoo.first.rawValue)  // first
print(StrFoo.second.rawValue) // 2
print(StrFoo.third.rawValue)  // third

递归枚举

递归枚举(Recursive Enumeration)指的是枚举的关联值(Associated Value)类型使用到了该枚举类型本身,此时需要 indirect 修饰:

indirect enum IndirectBar {
    case first(Int)
    case second(IndirectBar)
}

enum Bar {
    case first(Int)
    indirect case second(Bar)
}

let bar1 = IndirectBar.second(.second(.second(.first(0))))
let bar2 = Bar.second(.second(.second(.first(0))))

print(bar1) // second(demo.IndirectBar.second(demo.IndirectBar.second(demo.IndirectBar.first(0))))
print(bar2) // second(demo.Bar.second(demo.Bar.second(demo.Bar.first(0))))

MemoryLayout

不同于 Obj-C/C++,Swift 变得更加注重安全且无法直接使用 C/C++ 中的 API,这使得我们剖析 Swift 底层时有些「不踏实」。而 MemoryLayout 正是来帮助我们描述类型的内存布局,提供诸如大小、步进、以及对齐的信息:

// 支持泛型

// size:实际占用的空间大小
print(MemoryLayout<Int>.size)      // 8
// stride:最终分配的空间大小
print(MemoryLayout<Int>.stride)    // 8
// alignment:内存对齐参数(最终分配的空间大小即其倍数)
print(MemoryLayout<Int>.alignment) // 8

var intFoo = 100 // 16 * 6 + 4 => 0x0000000000000064
var intBar = 18  // 16 * 1 + 2 => 0x0000000000000012

print(MemoryLayout.size(ofValue: intFoo))      // 8
print(MemoryLayout.stride(ofValue: intFoo))    // 8
print(MemoryLayout.alignment(ofValue: intFoo)) // 8

// 打印值类型变量的内存地址
withUnsafePointer(to: &intFoo) { print("\($0)") } // 0x0000000100003088

struct StructFoo {
    var a = 1 // 0 + 1 => 0x0000000000000001
    var b = 1 // 0 + 1 => 0x0000000000000001
}

var foo = StructFoo()
// 打印结构体类型变量的内存地址
withUnsafeMutablePointer(to: &foo) { print("\($0)") } // 0x0000000100003098

// x86_64 / arm64 小端:高位字节存放在高位地址
// eg. 0x0000000000000064,64 为低位,00 为高位,内存地址从左至右依次升高,64 低位字节存放在低位地址即开头
// (lldb) memory read 0x0000000100003088
// 0x100003088: 64 00 00 00 00 00 00 00 12 00 00 00 00 00 00 00  d...............
// 0x100003098: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
// (lldb) memory read 0x0000000100003098
// 0x100003098: 01 00 00 00 00 00 00 00 01 00 00 00 00 00 00 00  ................
// 0x1000030a8: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................

main

Swift 项目中没有显式的 main 函数作为整个程序的入口,但当我们断点在 main.swift 中时,Debug Navigator 中仍将展示 0 main,此时打开汇编也会在第一行展示demo`main:。需要注意的是,声明在 main.swift 的变量将类似全局变量,存储在内存的全局区(数据段),可以直接在其它文件中使用:

// main.swift

var gloA = 100

withUnsafePointer(to: &gloA) { print("\($0)") } // 0x00000001000031c0

下标(Subscript)

下标是指可以在类、结构体、或枚举中定义 subscript 方法,即可使相应实例获得通过下标访问的能力:

class Foo {
    var a = 10
    var b = 20

    subscript(index: Int) -> Int { // 省略 `func` 关键字
        get {                      // 通过 subscript 获取,类似计算属性
            switch index {
            case 0:
                return a
            case 1:
                return b
            default:
                fatalError("Index out of range.")
            }
        }

        set {                      // 通过 subscript 设置,类似计算属性
            switch index {
            case 0:
                a = newValue
            case 1:
                b = newValue
            default:
                fatalError("Index out of range.")
            }
        }
    }
}

var foo = Foo()

foo[0] = 100
foo[1] = 200

print(foo[0]) // 100
print(foo[1]) // 200

当不实现 set 时,将变成只读下标,即只可以通过下标访问而不能更改。下标支持设置显式的参数标签,以及支持同时多个下标索引;也支持针对于类型的下标,只需要在声明前添加 staticclass 关键字修饰即可(但需要注意由于 static 修饰的方法将不能被子类重写,下标也同理):

class Bar {
    var colmunA = [1, 2]
    var columnB = [10, 20]

    static var c = 30
    static var d = 40

    subscript(i index: Int, row: Int) -> Int { // 只读下标 & 多下标索引
        get {
            switch index {
            case 0:
                return colmunA[row]
            case 1:
                return columnB[row]
            default:
                fatalError("Index out of range.")
            }
        }
    }

    static subscript(index: Int) -> Int {
        switch index {
        case 0:
            return c
        case 1:
            return d
        default:
            fatalError("Index out of range.")
        }
    }
}

print(Bar()[i: 0, 1]) // 10
print(Bar[0])         // 30

当为只读下标时,通过下标获取的类型根据是结构体还是类的情况有些许不同:

class ClassFoo {
    var a = 10
}

struct StructBar {
    var b = 20
}

class BazByClass {
    var foo = ClassFoo()

    subscript(index: Int) -> ClassFoo {
        get { foo }
    }
}

class BazByStruct {
    var bar = StructBar()

    subscript(index: Int) -> StructBar {
        get { bar }
    }
}

var baz1 = BazByClass()
baz1[0].a = 100
// baz1[0] = ClassFoo() // ERROR: Cannot assign through subscript: subscript is get-only
print(baz1[0].a)

var baz2 = BazByStruct()
// baz2[0].b = 200 // ERROR: Cannot assign to property: subscript is get-only
print(baz2[0].b)

当只读下标获取的类型为类时(引用类型),只读只针对于指向堆空间实例的地址,即无法设置指向为新的实例,但仍可以读写堆的内存空间;而当只读下标获取的类型为结构体时(值类型),将无法通过下标设置结构体变量的值,那么想要修改就要通过 set 实现:

class BazByStruct2 {
    var bar = StructBar()

    subscript(index: Int) -> StructBar {
        get { bar }
        set {
            bar = newValue
        }
    }
}

var baz3 = BazByStruct2()
withUnsafeMutablePointer(to: &baz3.bar) { print("\($0)") } // 0x0000000100523ac0
baz3[0].b = 200
withUnsafeMutablePointer(to: &baz3.bar) { print("\($0)") } // 0x0000000100523ac0
print(baz3[0].b) // 200

static 与 class

staticclass 均可以在 class 类型中修饰方法或属性,即类型方法或类型属性。但两者的不同是,static 修饰的方法或属性子类将不能重写:

class Foo {
    static var staticProp = 1
    static func staticFunc() { }

    class var classProp: Int { 1 }
    class func classFunc() { }
}

class SubFoo: Foo {
    // static 修饰的属性 / 方法不可被重写
    // ERROR: Cannot override static property
//    static var staticProp: Int {
//        get { super.staticProp }
//        set { super.staticProp = newValue }
//    }

    // ERROR: Cannot override static method
//    static func staticFunc() { }
    
    // class 修饰的属性 / 方法可被重写
    override class var classProp: Int {
        get { super.staticProp }
        set { super.staticProp = newValue + 1 }
    }
    
    override static func classFunc() {}
}

class SubSubFoo: SubFoo {
    // 二次重写时使用 class 修饰,子类仍可重写
    override class var classProp: Int {
        get { super.staticProp }
        set { super.staticProp = newValue + 2 }
    }

    // 二次重写时使用 static 修饰,子类将不可重写
    // ERROR: Cannot override static method
//    override class func classFunc() {}
}

Any 与 AnyObject

Swift 中的 Any 类型可以代表包括结构体、枚举、类、函数等任意类型;AnyObject 类型仅可以代表任意 class 类型。

struct Foo {
    var foo = 1
}

class Bar {
    var bar = 2
}

class AnotherBar {
    var bar = 3
}

func baz() {}

var any: Any = 10
any = 3.14
any = Foo()
any = Bar()
any = baz

var anyObject: AnyObject = Bar()
anyObject = AnotherBar()
// ERROR: Value of type 'Foo' expected to be an instance of a class or class-constrained type in assignment
// anyObject = Foo()

当 Swift 的协议遵守 AnyObject 时,该协议将只能被类遵守:

protocol ProtocolA {
    func a()
}

protocol ProtocolB: AnyObject {
    func b()
}

// 协议遵守 class 时也将只能被类遵守
protocol ProtocolC: class {
    func c()
}

struct Foo: ProtocolA {
    func a() {}
}

// class Bar: ProtocolA, ProtocolB, ProtocolC {
class Bar: ProtocolA & ProtocolB & ProtocolC {
    func a() {}
    func b() {}
    func c() {}
}

T.self、T.Type、Self 与 type(of:)

T 为某一类型,T.self 是元类型(Metatype)的指针,类型为 T.Type,元类型中存储了类型本身的信息:

class Foo { }
struct Bar { }

var selfInt = Int.self           // Int.Type
var selfFoo: Foo.Type = Foo.self // Foo.Type
var selfBar: Bar.Type = Bar.self // Bar.Type

var intA = intSelf.init(5)       // 5: Int

AnyClassAnyObject.Type 的类型别名,即任意 class 类型的元类型指针:

// Swift / Misc
public typealias AnyClass = AnyObject.Type

我们可以尝试将其转换为不同的元类型指针,用以构造:

class Foo {
    required init(_ str: String) { }
}

func initAnyClass(_ ac: AnyClass) -> AnyObject {
    if let ac = ac as? Foo.Type {
        return ac.init("ac") // 编译器已经替我们尝试约束 Foo 所必须具备(required)的 init 方法
    }
    
    fatalError()
}

print(initAnyClass(Foo.self)) // demo.Foo

Self 代表当前所在的类型本身(类似于 Obj-C 中的 instancetype),可定义为函数的返回值:

protocol Protocol {
    // 在协议中,Self 可作为函数参数使用
    func function(_ param: Self)
}

class Foo: Protocol {
    func foo() {}

    // 必须声明为 required,防止子类声明其它构造方法导致 init() 丢失后无法被调用
    required init() { }

    func foo() -> Self {
        // type(of value: T) -> Metatype :返回参数的类型
        // 即使子类也可获取类型,并初始化
        return type(of: self).init()
    }

    func function(_ param: Foo) { }
}

class SubFoo: Foo { }

struct Bar {
    func bar() -> Self {
        return type(of: self).init()
    }
}

type(of:) 在 Swift 中看似被认为是函数,带有参数与返回值,但从现有汇编中可以得出其本质上并非函数调用:

class Foo {
    func foo() {}
}

var f = Foo()
var fType = type(of: f)  // Foo.Type;BREAKPOINT 🔴
// Foo.self 为指向元类型的指针,即类型信息的地址,因此与 type(of:) 返回值相同
print(Foo.self == fType) // true

运行至断点处并查看汇编:

demo`main:
    0x100002ad0 <+0>:   pushq  %rbp
    0x100002ad1 <+1>:   movq   %rsp, %rbp
    0x100002ad4 <+4>:   pushq  %r13
    0x100002ad6 <+6>:   subq   $0x58, %rsp
    0x100002ada <+10>:  xorl   %eax, %eax
    0x100002adc <+12>:  movl   %eax, %ecx
    0x100002ade <+14>:  movl   %edi, -0x24(%rbp)
    0x100002ae1 <+17>:  movq   %rcx, %rdi
    0x100002ae4 <+20>:  movq   %rsi, -0x30(%rbp)
    0x100002ae8 <+24>:  callq  0x100002b80               ; type metadata accessor for demo.Foo at <compiler-generated>
    0x100002aed <+29>:  movq   %rax, %r13
    0x100002af0 <+32>:  movq   %rdx, -0x38(%rbp)
    0x100002af4 <+36>:  callq  0x100002c20               ; demo.Foo.__allocating_init() -> demo.Foo at main.swift:3
    ; 将 rip(0x100002b00)+ 0x57b8(0x1000082B8)存储在 rcx 寄存器中(全局变量 f)
    0x100002af9 <+41>:  leaq   0x57b8(%rip), %rcx        ; demo.f : demo.Foo
    0x100002b00 <+48>:  xorl   %r8d, %r8d
    0x100002b03 <+51>:  movl   %r8d, %edx
    0x100002b06 <+54>:  movq   %rax, 0x57ab(%rip)        ; demo.f : demo.Foo
->  0x100002b0d <+61>:  movq   %rcx, %rdi
    0x100002b10 <+64>:  leaq   -0x20(%rbp), %rsi
    0x100002b14 <+68>:  movl   $0x20, %eax
    0x100002b19 <+73>:  movq   %rdx, -0x40(%rbp)
    0x100002b1d <+77>:  movq   %rax, %rdx
    0x100002b20 <+80>:  movq   -0x40(%rbp), %rcx
    0x100002b24 <+84>:  callq  0x100003d12               ; symbol stub for: swift_beginAccess
    ; 移动 rip(0x100002b30)+ 0x5788(0x1000082B8,全局变量 f,即 Foo 类型对象的指针)至 rax 寄存器
    0x100002b29 <+89>:  movq   0x5788(%rip), %rax        ; demo.f : demo.Foo
    0x100002b30 <+96>:  movq   %rax, %rcx
    0x100002b33 <+99>:  movq   %rcx, %rdi
    ; 移动 rax 寄存器内容至 rbp - 0x48
    0x100002b36 <+102>: movq   %rax, -0x48(%rbp)
    0x100002b3a <+106>: callq  0x100003d2a               ; symbol stub for: swift_retain
    0x100002b3f <+111>: leaq   -0x20(%rbp), %rdi
    0x100002b43 <+115>: movq   %rax, -0x50(%rbp)
    0x100002b47 <+119>: callq  0x100003d1e               ; symbol stub for: swift_endAccess
    ; 移动 rbp - 0x48 内容至 rax 寄存器
    0x100002b4c <+124>: movq   -0x48(%rbp), %rax
    ; 取 rax 寄存器内容所指向的内存空间的内容(前八个字节,即类型信息)至 rcx 寄存器
    0x100002b50 <+128>: movq   (%rax), %rcx
    0x100002b53 <+131>: movq   %rax, %rdi
    ; 移动 rcx 寄存器内容至 rbp - 0x58
    0x100002b56 <+134>: movq   %rcx, -0x58(%rbp)
    0x100002b5a <+138>: callq  0x100003d24               ; symbol stub for: swift_release
    0x100002b5f <+143>: xorl   %eax, %eax
    ; 移动 rbp - 0x58 内容至 rcx 寄存器
    0x100002b61 <+145>: movq   -0x58(%rbp), %rcx
    ; 移动 rcx 寄存器内容(前八个字节)至 rip + 0x5754(全局变量 fType)
    0x100002b65 <+149>: movq   %rcx, 0x5754(%rip)        ; demo.fType : demo.Foo.Type
    0x100002b6c <+156>: addq   $0x58, %rsp
    0x100002b70 <+160>: popq   %r13
    0x100002b72 <+162>: popq   %rbp
    0x100002b73 <+163>: retq

CustomStringConvertible

public protocol CustomStringConvertible {
    var description: String { get }
}

public protocol LosslessStringConvertible : CustomStringConvertible {
    init?(_ description: String)
}

public protocol CustomDebugStringConvertible {
    var debugDescription: String { get }
}

CustomStringConvertible 协议中只有一个 description 属性,类似 Obj-C 中 NSObjectdescription 方法,Swift 中的类型可以通过实现该协议在需要打印对象的时候进行自定义表示。LosslessStringConvertible 协议继承自 CustomStringConvertible,提供了从表示到对象的可失败构造方法。CustomDebugStringConvertible 则提供了更多 Debug 下常用的对象打印,如 debugPrint 与 LLDB 的 po 命令:

// Swift / Misc

struct Foo: CustomStringConvertible,
            LosslessStringConvertible,
            CustomDebugStringConvertible {
    var foo: String
    
    init?(_ description: String) {
        foo = description
    }
    
    var description: String {
        "Foo - \(foo)"
    }
    
    var debugDescription: String {
        "Debug - Foo - \(foo)"
    }
}

let f = Foo("foo")!

print(f)      // Foo - foo,BREAKPOINT 🔴
debugPrint(f) // Debug - Foo - foo

// LLDB:
// (lldb) po f
// ▿ Debug - Foo - foo
//   - foo : "foo"
// (lldb) p f
// (demo.Foo) $R4 = (foo = "foo") 

默认情况

当未实现 CustomStringConvertible 协议时,printdebugPrintpo 均可正常使用,表现如下;当仅实现 CustomStringConvertible 协议时,debugPrintpo 将默认以实现的 description 为准,表现如下:

struct Bar {
    var bar: String
}

let bar = Bar(bar: "bar")

print(bar)      // Bar(bar: "bar")
debugPrint(bar) // demo.Bar(bar: "bar")

struct Baz: CustomStringConvertible {
    var baz: String
    
    var description: String {
        "Baz - \(baz)"
    }
}

let baz = Baz(baz: "baz")

print(baz)      // Baz - baz,BREAKPOINT 🔴
debugPrint(baz) // Baz - baz

// LLDB:
// (lldb) po bar
// ▿ Bar
//   - bar : "bar"
// (lldb) po baz
// ▿ Baz - baz
//   - baz : "baz"

断言(assert)

我们常在 Debug 模式下使用断言来保证代码的健壮性。默认情况下,Swift 中的断言仅在 Debug 模式下生效,但我们也可以通过编译选项配置:

Other Swift Flags Description
-assert-config Release 断言失效
-assert-config Debug 断言生效