Swift 5.1 温故而知新笔记系列之第二天

1.结构体

在 Swift 标准库中,绝大多数的公开类型都是结构体,而枚举和类只占很小一部分
比如BoolIntDoubleStringArrayDictionary等都是结构体

struct Point {
	var x: Int = 0
	var y: Int = 0
	var origin: Bool = false
}
var point = Point(x: 10, y:10, origin:false)
print(MemoryLayout.size) // 17
print(MemoryLayout.stride) // 24
print(MemoryLayout.alignment) // 8

所有结构体都有一个编译器自动生成可传入成员值的初始化器(initializer,初始化方法,构造器,构造方法)
自动生成的初始化器会保证存储属性都被赋值

2.类

类的定义和结构体类似,但编译器并没有为类自动生成可以传入成员值的初始化器

class Point {
     
	var x: Int = 0
	var y: Int = 0
}
let p1 = Point()
let p2 = Point(x: 10, y: 20) // 报错
let p3 = Point(x: 10)  // 报错
let p4 = Point(y: 20)  // 报错

如果类的所有成员都在定义的时候指定了初始值,编译器会为类生成无参的初始化器 p成员的

class Point {
     
    var x: Int = 0
    var y: Int = 0
}
var p = Point()

class Point {
     
    var x: Int
    var y: Int
    init() {
     
        x = 0
        y = 0
    }
}
var p = Point()

类和结构体的本质区别

  • 1.结构体是值类型,类是引用类型
struct Size {
    var width = 3
    var height = 4
}

class Point {
    var x: Int = 1
    var y: Int = 2
    init(x: Int, y: Int) {
        self.x = x
        self.y = y
    }
}

func test() {
    // 0x00007ffeefbff048
    var point = Point(x: 100, y: 200)
    
    // 0x00007ffeefbff030
    // 03 00 00 00 00 00 00 00
    // 04 00 00 00 00 00 00 00
    var size = Size()

    withUnsafePointer(to: &point) { ptr in
        print("point \(ptr)")
    }

    withUnsafePointer(to: &size) { ptr in
        print("size \(ptr)")
    }

    // 引用类型,指针都占用8
    print(MemoryLayout.size(ofValue: point)) // 8
    print(MemoryLayout.stride(ofValue: point)) // 8
    print(MemoryLayout.alignment(ofValue: point)) // 8
    
    // 0x0000000102800150
    // 68 77 00 00 01 00 00 00  0x10007768
    // 02 00 00 00 00 00 00 00  0x00000002
    // 64 00 00 00 00 00 00 00  0x00000064
    // C8 00 00 00 00 00 00 00  0x000000C8
    print(Unmanaged.passUnretained(point).toOpaque())
    
}
test()

Swift 5.1 温故而知新笔记系列之第二天_第1张图片
可以看到,我们是在函数体内,栈空间创建的,如果是在外部,那就会在全局区,如果是在class里面创建,就会跟随class在堆空间,这边就举个例子。

  • 2.结构体值类型给var、let或者给函数传参,是直接深copy了一份
//全局变量,程序运行过程中只有一份内存 内存地址不变 
struct Space {
     
    var x: Int
    var y: Int
}

var space1 = Space(x: 10, y: 20)
var space2 = space1

space2.x = 100
space2.y = 200

//withUnsafePointer(to: &space1) {  ptr in
//    print("space1 \(ptr)")
//}
//
//withUnsafePointer(to: &space2) {  ptr in
//    print("space2 \(ptr)")
//}

这边用全局变量来做例子,你也可以放到函数体局部变量来做例子。
先看全局变量上面最简单代码的赋值汇编

0x1000013ab <+11>:  movl   $0xa, %eax			// 10 给到 eax  = rax
0x1000013b0 <+16>:  movl   %edi, -0x7c(%rbp)
0x1000013b3 <+19>:  movq   %rax, %rdi			// rax 给到 rdi = 10
0x1000013b6 <+22>:  movl   $0x14, %eax			// 20 给到 eax  = rax
0x1000013bb <+27>:  movq   %rsi, -0x88(%rbp)
0x1000013c2 <+34>:  movq   %rax, %rsi			// rax 给到 rsi = 20
0x1000013c5 <+37>:  callq  0x100001740               ; Swift01.Space.init(x: Swift.Int, y: Swift.Int) -> Swift01.Space at main.swift:16

si 进去callq对应的init函数


Swift01`Space.init(x:y:):
->  0x100001740 <+0>:  pushq  %rbp
    0x100001741 <+1>:  movq   %rsp, %rbp
    0x100001744 <+4>:  movq   %rdi, %rax		rdi = 10 给到 rax
    0x100001747 <+7>:  movq   %rsi, %rdx		rsi = 20 给到 rdx
    0x10000174a <+10>: popq   %rbp
    0x10000174b <+11>: retq  


finish 跳出

0x1000013ca <+42>:  leaq   0xe47(%rip), %rsi         0x100002a8f + 0xe47 = 0x100002218 	space1的内存地址
0x1000013d3 <+51>:  movq   %rax, 0xe3e(%rip)         rax = 10 赋值给右边内存地址 0x1000013da + 0xe3e = 0x100002218  space1的内存地址
0x1000013da <+58>:  movq   %rdx, 0xe3f(%rip)         rdx = 20 赋值给右边内存地址 0x1000013e1 + 0xe3f = 0x100002220  
0x1000013e1 <+65>:  movq   %rsi, %rdi

var space1 = Space(x: 10, y: 20)
到这里,space1对象初始化后,内存赋值完毕

0x1000013f2 <+82>:  movq   0xe1f(%rip), %rax         0x1000013f9 + 0xe1f = 0x100002218  把space1里面的前8个字节 (q类型) 10给到rax 
0x1000013f9 <+89>:  movq   %rax, 0xe28(%rip)         0x100001400 + 0xe28 = 0x100002228	然后把rax的值给到 0x100002228	    space2的内存地址
0x100001400 <+96>:  movq   0xe19(%rip), %rax         0x100001407 + 0xe19 = 0x100002220  把space1里面的后8个字节 (q类型) 20给到rax 
0x100001407 <+103>: movq   %rax, 0xe22(%rip)         0x10000140e + 0xe22 = 0x100002230  然后把rax的值给到 0x100002230
0x10000140e <+110>: leaq   -0x18(%rbp), %rdi

到这里 var space2 = space1 赋值操作的汇编完成

0x100001430 <+144>: callq  0x100001d1c               ; symbol stub for: swift_beginAccess
0x100001435 <+149>: movq   $0x64, 0xde8(%rip)        // 100 赋值给 0x100001440 + 0xde8 = 0x100002228  space2的内存地址的 (q类型) 前八个字节
0x100001440 <+160>: leaq   -0x30(%rbp), %rdi
0x100001444 <+164>: callq  0x100001d28               ; symbol stub for: swift_endAccess
0x100001449 <+169>: leaq   0xdd8(%rip), %rax         
0x100001450 <+176>: xorl   %r8d, %r8d
0x100001450 <+176>: xorl   %r8d, %r8d
0x100001453 <+179>: movl   %r8d, %ecx				
0x100001456 <+182>: movq   %rax, %rdi
0x100001459 <+185>: leaq   -0x48(%rbp), %rsi
0x10000145d <+189>: movl   $0x21, %edx
0x100001462 <+194>: callq  0x100001d1c               ; symbol stub for: swift_beginAccess
0x100001467 <+199>: movq   $0xc8, 0xdbe(%rip)        // 200 赋值给 0x100001472 + 0xdbe = 0x100002230  space2的内存地址的 (q类型) 后八个字节
0x100001472 <+210>: leaq   -0x48(%rbp), %rdi

space2.x = 100
space2.y = 200
到这里 space2的赋值操作完成

0x100002228 地址二进制如下:

64 00 00 00 00 00 00 00 
C8 00 00 00 00 00 00 00

可以简单看下这个局部变量对应的汇编,很简单

0x1000017b3 <+19>: movl   $0xa, %edi   		// 10赋值给edi = rdi
0x1000017b8 <+24>: movl   $0x14, %esi		// 20赋值给esi = rsi
0x1000017bd <+29>: callq  0x100001790		// si 进去  rdi 赋值给rax  rsi 赋值给rdx	               
0x1000017c2 <+34>: movq   %rax, -0x10(%rbp)	
0x1000017c6 <+38>: movq   %rdx, -0x8(%rbp)
0x1000017ca <+42>: movq   %rax, -0x20(%rbp)
0x1000017ce <+46>: movq   %rdx, -0x18(%rbp)
0x1000017d2 <+50>: movq   $0x64, -0x20(%rbp)
0x1000017da <+58>: movq   $0xc8, -0x18(%rbp)

寄存器经验:
如果类似0xde8(%rip)一般是全局区
如果类似-0x20(%rbp)一般是局部变量

3.值类型的赋值操作

var s1 = "Jack"
var s2 = s1
s2.append("_Rose")
print(s1) // Jack
print(s2) // Jack_Rose

var a1 = [1, 2, 3]
var a2 = a1
a2.append(4)
a1[0] = 2
print(a1) // [2, 2, 3]
print(a2) // [1, 2, 3, 4]

var d1 = ["max" : 10, "min" : 2]
var d2 = d1
d1["other"] = 7
d2["max"] = 12
print(d1) // ["other": 7, "max": 10, "min": 2]
print(d2) // ["max": 12, "min": 2]
  • 在Swift标准库中,为了提升性能,String、Array、Dictionary、Set采取了Copy On Write的技术,比如仅当有“写”操作时,才会真正执行拷贝操作
  • 对于标准库值类型的赋值操作,Swift 能确保最佳性能,所有没必要为了保证最佳性能来避免赋值

引用类型

这个简单,就是我们普遍认为的内存引用,举个例子看下汇编

func test1(){
     
    class People {
     
        var width: Int
        var height: Int
        init(width: Int, height: Int) {
     
            self.width = width
            self.height = height
        }

    }
    
    var p1 = People(width: 10, height: 20)
    
    // register read rax
    // 98 22 00 00 01 00 00 00  ->meta info
    // 02 00 00 00 00 00 00 00  ->ref
    // 0A 00 00 00 00 00 00 00  ->10
    // 14 00 00 00 00 00 00 00  ->20
    // 0x1000010e0 <+64>:  movq   %rax, -0x10(%rbp)
    
     
    
    // 0x1000010e7 <+71>:  movq   %rax, -0x60(%rbp)
    var p2 = p1
    
    //  0x100001127 <+135>: movq   -0x60(%rbp), %rax
    //  0x10000112b <+139>: movq   $0xb, 0x10(%rax)
    // rax是对象的地址,0x10是16个字节 也就是 0A 00 00 00 00 00 00 00 赋值11
    p2.width = 11
    // 0x100001162 <+194>: movq   -0x60(%rbp), %rax
    // 0x100001166 <+198>: movq   $0xc, 0x18(%rax)
    // rax是对象的地址,0x18是24个字节 也就是 14 00 00 00 00 00 00 00  赋值12
    p2.height = 12
}

test1()

根据上述的汇编分析,也可以看到

1.内存地址格式为 0x4bdc(%rip),一般是全局变量,全局区(数据段)
2.内存地址格式为 -0x78(%rbp),一般是局部变量,栈空间
3.内存地址格式为 0x10(%rax),一般是堆空间   

4.闭包表达式

var clouse = {
     
	(v1: Int, v2: Int) -> Int in 
	return v1 + v2
}
clouse()

{
     
	(参数列表) -> 返回值类型 in
	函数体代码
}

func exec(v1: Int, v2: Int, fn:(Int, Int) -> Int) {
     
    print(fn(v1, v2))
}


exec(v1: 10, v2: 20) {
     
    (v1: Int, v2: Int) -> Int in
    return v1 + v2
}

exec(v1: 10, v2: 20) {
     
    v1, v2 in
    return v1 - v2
}

exec(v1: 10, v2: 20) {
     
    v1, v2 in v1 * v2
}


exec(v1: 10, v2: 20) {
      $0 + $1}

exec(v1: 100, v2: 200, fn:+)

5.闭包

一个函数和它所捕获的变量/常量环境组合在一起

  • 一般指定义在函数内部的函数
  • 一般它捕获的是外层函数的局部变量、常量
  • 返回出去的函数 + 捕获堆空间的东西

5.1 概述

实例一

//typealias FNC = (Int) -> Int
//
//func getFn() -> FNC {
     
//    var num = 0
//    return {
     
//        (i: Int) -> Int in
//        num += i
//        return num
//    }
//}
//
//var x = getFn()
//print(x(1))
//print(x(2))
//print(x(3))
typealias KJFunc = (Int) -> Int

func getFn() -> KJFunc {
     
    var num = 1
    func plus(_ i: Int) -> Int{
     
        num += i
        return num
    }
    return plus
}

var fn = getFn()

print(fn(1))
print(fn(2))
print(fn(3))
print(fn(4))
print(fn(5))

这个闭包应该很简单,是个人都知道结果,但是有没有想过,var num这个是局部变量,在getFn函数之后,栈空间就被回收了,那么除了全局静态区变量,就只可能被分配到堆空间了。
我们在两个return的时候打断点,看下汇编

Swift01`getFn():
    0x1000018f0 <+0>:  pushq  %rbp
    0x1000018f1 <+1>:  movq   %rsp, %rbp
    0x1000018f4 <+4>:  subq   $0x20, %rsp
    0x1000018f8 <+8>:  leaq   0x7f9(%rip), %rdi
    0x1000018ff <+15>: movl   $0x18, %esi
    0x100001904 <+20>: movl   $0x7, %edx
    0x100001909 <+25>: callq  0x100001e1c               ; symbol stub for: swift_allocObject
->  0x10000190e <+30>: movq   %rax, %rdx
    0x100001911 <+33>: addq   $0x10, %rdx
    0x100001915 <+37>: movq   %rdx, %rsi
    0x100001918 <+40>: movq   $0x1, 0x10(%rax)
    0x100001920 <+48>: movq   %rax, %rdi
    0x100001923 <+51>: movq   %rax, -0x8(%rbp)
    0x100001927 <+55>: movq   %rdx, -0x10(%rbp)

可以看到,闭包是分配了堆空间的swift_allocObject,之前 有提到过,callq函数返回值是存储在rax寄存器中,看下内存

(lldb) register read rax
     rax = 0x000000010067fc80
     
(lldb) x/5xg 0x000000010067fc80
0x10067fc80: 0x00000001000020f8 0x0000000000000002
0x10067fc90: 0x000000010067000f 0x0000000000000000
0x10067fca0: 0x0000000000000000

读出rax内存地址,然后打印该地址下5组数据,可以猜测前32个字节,有点像对象的内存分布,但是可以看到0x10067fc90: 0x000000010067000f是脏数据,因为还没有被初始化,断点继续往下0x100001918 <+40>: movq $0x1, 0x10(%rax),执行完这句,内存就被初始化为1了

(lldb) x/5xg 0x000000010067fc80
0x10067fc80: 0x00000001000020f8 0x0000000000000002
0x10067fc90: 0x0000000000000001 0x0000000000000000
0x10067fca0: 0x0000000100075380

继续断到闭包内的return里面的断点
Swift 5.1 温故而知新笔记系列之第二天_第2张图片
再次读取对象的地址,可以看到第十六个字节被赋值了2,一次类推,可以看到闭包捕获的变量,变量本身是局部变量,栈空间的,被捕获到了堆空间,而且内存分布看上去和对象基本一致,前八个字节元数据,8-16是引用计数,在后面就存储捕获的变量,那么又个问题,怎么知道捕获的变量实际分配了多少内存呢。
先简单看下普通类实例化的时候,init函数之后,实际分配了多少内存

示例二

class Test {
     
    var test1: Int = 0
    var test2: Int = 0
}
var test = Test()
0x1000013d4 <+36>: callq  0x100001810               ; Swift01.Test.__allocating_init() -> Swift01.Test at main.swift:38

si进去

Swift 5.1 温故而知新笔记系列之第二天_第3张图片
可以看到alloc的时候,movl $0x20, %esi分配了32个字节的内存

那么我们再回过头来看闭包的汇编
Swift 5.1 温故而知新笔记系列之第二天_第4张图片
可以看到,发送了24个字节的指令去申请内存,实际上根据架构,实际上获取到了32个字节,因此上面x/5xg打印出来的内存地址,其实只要x/4xg就是完整的闭包捕获的对象了。

小结:

  • 1.闭包就是定义在函数内部的函数,捕获外层函数的局部变量/常量
  • 2.内存捕获到堆空间,捕获的局部变量、常量就是对象的成员,组成的闭包函数,就是类内部定义的方法
  • 3.捕获的变量原本在栈空间,出了作用域被回收,copy了值到堆空间变成对象供函数使用

阶段一概述:
从汇编来看
1.变量捕获会在堆空间开辟内存
2.例子中开辟的内存是32个字节,前16个字节类似类的数据结构,后16个字节开始存放捕获的变量值

5.2 详述

1.先来看一下简单的函数内存分布

func sum(v1: Int, v2: Int) -> Int{
     
    return v1 + v2
}
var fn = sum
print("1")

(lldb) p fn
() -> () $R0 = 0x0000000100000aa0 Swift01`Swift01.sum(v1: Swift.Int, v2: Swift.Int) -> Swift.Int at main.swift:11

展开的汇编如下

    0x1000009e3 <+19>:  leaq   0xb6(%rip), %rcx          ; Swift01.sum(v1: Swift.Int, v2: Swift.Int) -> Swift.Int at main.swift:11
    0x1000009ea <+26>:  movq   %rcx, 0x7a7(%rip)         ; Swift01.fn : (Swift.Int, Swift.Int) -> Swift.Int
    0x1000009f1 <+33>:  movq   $0x0, 0x7a4(%rip)         ; Swift01.fn : (Swift.Int, Swift.Int) -> Swift.Int + 4
    0x1000009fc <+44>:  movl   $0x1, %ecx

根据经验来看,0x10(%rip)这种就是全局变量,也就是这边的var fn,那么根据右侧的提示,可以看到leaq对应的地址赋值是 rip + 0xb6 = 0x100000AA0,和上面的po打印出来的吻合,而且下面两个movq分别是对 0x1000011A0``0x100001198进行8个字节的赋值,因此,函数分配给fn变量的内存是16个字节,前8个字节是函数地址,后八个字节是0,暂时没用到。

2.看一下没捕获变量的闭包(这种也不叫闭包)

typealias KJFunc = (Int) -> Int

func getFn() -> KJFunc {
     
//    var num = 0
    func plus(_ i: Int) -> Int{
     
//        num += i
//        return num
        return i
    }
    return plus
}

var fn = getFn()
print("1")

断点在getFn()

0x1000013c0 <+0>:    pushq  %rbp
    0x1000013c1 <+1>:    movq   %rsp, %rbp
    0x1000013c4 <+4>:    pushq  %r13
    0x1000013c6 <+6>:    subq   $0x218, %rsp              ; imm = 0x218 
    0x1000013cd <+13>:   movl   %edi, -0x84(%rbp)
    0x1000013d3 <+19>:   movq   %rsi, -0x90(%rbp)
    0x1000013da <+26>:   callq  0x100001aa0               ; Swift01.getFn() -> (Swift.Int) -> Swift.Int at main.swift:47
si 进去
Swift01`getFn():
->  0x100001aa0 <+0>:  pushq  %rbp
    0x100001aa1 <+1>:  movq   %rsp, %rbp
    0x100001aa4 <+4>:  leaq   0x15(%rip), %rax          ; plus #1 (Swift.Int) -> Swift.Int in Swift01.getFn() -> (Swift.Int) -> Swift.Int at main.swift:49
    0x100001aab <+11>: xorl   %ecx, %ecx
    0x100001aad <+13>: movl   %ecx, %edx
    0x100001aaf <+15>: popq   %rbp
    0x100001ab0 <+16>: retq   

可以看到函数plus赋值给了rax,xorl %ecx, %ecx清0操作,然后把0赋值给edx也就是rdx,然后看外部

0x1000013df <+31>:   movq   0xc32(%rip), %rsi         ; (void *)0x00007fff9abdeb18: type metadata for Any
    0x1000013e6 <+38>:   addq   $0x8, %rsi
    0x1000013ea <+42>:   movq   %rax, 0xdc7(%rip)         ; Swift01.fn : (Swift.Int) -> Swift.Int
    0x1000013f1 <+49>:   movq   %rdx, 0xdc8(%rip)         ; Swift01.fn : (Swift.Int) -> Swift.Int + 8
    0x1000013f8 <+56>:   movl   $0x1, %edi

这里可以看到把rax、rdx里面的值赋值给了对应的地址,

(lldb) p fn
() -> () $R4 = 0x0000000100001ac0 Swift01`plus #1 (Swift.Int) -> Swift.Int in Swift01.getFn() -> (Swift.Int) -> Swift.Int at main.swift:49
(lldb) register read rax
     rax = 0x0000000100001ac0  Swift01`plus #1 (Swift.Int) -> Swift.Int in Swift01.getFn() -> (Swift.Int) -> Swift.Int at main.swift:49

在没有捕获变量的情况下,和普通函数没什么区别,给外部变量前八个字节就是函数地址,后八个字节是0

3.看下完整的闭包是怎么样的

typealias KJFunc = (Int) -> Int

func getFn() -> KJFunc {
     
    var num = 0
    func plus(_ i: Int) -> Int{
     
        num += i
        return num
    }
    return plus
}

var fn = getFn()


print(fn(1))
print(fn(2))
print(fn(3))
print(fn(4))
print(fn(5))

0x01

先在return plus那打上断点,然后进入汇编

看到闭包的返回值,rax中存储着函数地址,rdx中存储着堆对象的地址

0x02

return plus后,回到外部调用,这边的函数地址存储在rax中,因此调用就会变成callq *%rax

callq上面,fn(1)的1存入rdi-0xb0(%rbp)追溯到上面的rcx(变量的后8个字节,堆空间地址)地址存入r13-0xa8(%rbp)追溯到上面的rax(变量的前8个字节),函数地址,所以调用callq的时候,参数1,参数self,也就是堆空间变量的地址,函数指针都有了,

  • rdi = 1
  • rax = 函数地址
  • r13 = 堆空间地址

0x03

si进去验证下

Swift01`partial apply for plus #1 (_:) in getFn():
->  0x100001a90 <+0>: pushq  %rbp
    0x100001a91 <+1>: movq   %rsp, %rbp
    0x100001a94 <+4>: movq   %r13, %rsi
    0x100001a97 <+7>: popq   %rbp
    0x100001a98 <+8>: jmp    0x100001930               ; plus #1 (Swift.Int) -> Swift.Int in Swift01.getFn() -> (Swift.Int) -> Swift.Int at main.swift:51

进入闭包函数地址,可以看到r13赋值给了rsi,然后jmp到 plus函数

  • rdi = 1
  • rax = 函数地址
  • rsi = 堆空间地址

0x04

plus函数

首先红色部分,把对应的rdx对象0x10后字节的数字 加上 rcx中存储的值,然后存储在rcx中。蓝色部分就是把存储在寄存器中计算好的值,写入rax对应的地址,可以看到rax对应的地址就是上面分析出来,堆对象地址后移16个字节所处的地址,写入寄存器中的值,完成一次fn(1)的调用

6.自动闭包

func getNumber() -> Int {
     
    let a = 100
    let b = 200
    return a + b
}
func getFirstPositive(_ v1: Int, _ v2: Int) -> Int {
     
    return v1 > 0 ? v1 : v2
}
print(getFirstPositive(10, 20)) // 10
print(getFirstPositive(-2, 20)) // 20
print(getFirstPositive(0, -4))  // -4

print(getFirstPositive(0, getNumber()))  // 300
print(getFirstPositive(10, getNumber())) // 10

正常写法,参数如果是函数的话,虽然有时候不需要,也会执行

func getNumber() -> Int {
     
    let a = 100
    let b = 200
    return a + b
}


func getFirstPositive(_ v1: Int, _ v2: () -> Int) -> Int {
     
    return v1 > 0 ? v1 : v2()
}

print(getFirstPositive(10, {
     
    let a = 1000
    let b = 2000
    print("111")
    return a + b
}))  // 10

print(getFirstPositive(0, {
     
    let a = 1000
    let b = 2000
    print("222")
    return a + b
})) // 222    3000


print(getFirstPositive(0, getNumber)) // 300
print(getFirstPositive(0, {
     100})) // 100
print(getFirstPositive(0){
     200}) // 200

这种写法就可以让函数晚点执行,但是如果是() -> Int类型,还有个@autoclosure让调用更加优雅一点


func getNumber() -> Int {
     
    let a = 100
    let b = 200
    print("333")
    return a + b
}
func getFirstPositive(_ v1: Int, _ v2: @autoclosure () -> Int) -> Int {
     
    return v1 > 0 ? v1 : v2()
}
print(getFirstPositive(100, getNumber()))
print(getFirstPositive(0, getNumber()))
print(getFirstPositive(0, 99))
  • @autoclosure会自动将 99 封装成闭包 { 99 }
  • @autoclosure只支持 () -> T格式的参数
  • @autoclosure 并非只支持最后一个参数
  • 空合运算符 ??使用了 @autoclosure 技术
  • @autoclosure@autoclosure 构成函数重载



第二天结束0.0

你可能感兴趣的:(基础知识,swift,ios,xcode,编程语言,函数闭包)