Swift底层进阶--011:闭包

什么是闭包

维基百科中的解释:

  • 在计算机科学中,闭包(Closure),又称词法闭包(Lexical Closure)或函数闭包(function closures),是在支持头等函数的编程语言中实现词法绑定的一种技术。
  • 闭包在实现上是一个结构体,它存储了一个函数(通常是其入口地址)和一个关联的环境(相当于一个符号查找表)。
func test() {
    print("test")
}

上述代码,test是⼀个全局函数,也是⼀种特殊的闭包,只不过当前全局函数并不捕获值

func makeIncrementer() -> () -> Int {
    var runningTotal = 12
    func incrementer() -> Int {
        runningTotal += 1
        return runningTotal
    }
    return incrementer
}

上述代码,incrementer称之为内嵌函数,同时从上层函数makeIncrementer中捕获变量runningTotal

{(age: Int) -> Int in
    return age
}

上述代码,称之为闭包表达式,是⼀个匿名函数,可以从上下⽂中捕获变量和常量

闭包表达式

闭包表达式是Swift语法,可以更简洁的传达信息,具有以下特性:

  • 利⽤上下⽂推断参数和返回值类型
  • 单表达式可以隐⼠返回,既省略return关键字
  • 参数名称可简写,⽐如$0
  • 尾随闭包表达式

闭包表达式书写格式:

{(param) -> ReturnType in
   //函数体
}
  • 作⽤域,也就是⼤括号
  • 参数和返回值
  • 函数体,也就是in之后的代码

Swift中闭包即可以当做变量,也可以当做参数传递

var closure: (Int) -> Int = {(age: Int) in
   return age
}

func test(param : (Int) -> Int){
   print(param(10))
}

test(param: closure)

可以将闭包声明⼀个可选类型,需要在参数和返回值外整体加()?

//错误写法
//var closure: (Int) -> Int?
//closure = nil

//正确写法
var closure: ((Int) -> Int)?
closure = nil

注释中的错误写法,相当于仅返回值是可选类型

可以通过let关键字将闭包声明为⼀个常量,⼀旦赋值后不可改变

let closure: (Int) -> Int

closure = {(age: Int) in
   return age
}
尾随闭包

把闭包表达式作为函数的最后⼀个参数。如果当前闭包表达式很⻓,可以通过尾随闭包的书写⽅式来提⾼代码的可读性

func test(_ a: Int, _ b: Int, _ c: Int, by: (_ item1: Int, _ item2: Int, _ item3: Int) -> Bool) -> Bool{
   return  by(a, b, c)
}

//常规写法
test(10, 20, 30, by: {(_ item1: Int, _ item2: Int, _ item3: Int) -> Bool in
    return (item1 + item2 < item3)
})

//尾随闭包写法
test(10, 20, 30){(_ item1: Int, _ item2: Int, _ item3: Int) -> Bool in
   return (item1 + item2 < item3)
}

尾随闭包写法,⼀眼看上去就知道是函数的调⽤。后⾯是⼀个闭包表达式,{}放在函数外⾯

var array = [1, 2, 3]

//常规写法
array.sort{(item1 : Int, item2: Int) -> Bool in return item1 < item2 }

//省略参数类型,根据上下文自动推断
array.sort(by: {(item1, item2) -> Bool in return item1 < item2 })

//省略返回值类型,根据上下文自动推断
array.sort(by: {(item1, item2) in return item1 < item2 })

//单表达式可省略return关键字
array.sort{(item1, item2) in item1 < item2 }

//参数简写,通过$0、$1、$2...按顺序替代对应位置的参数
array.sort{ return $0 < $1 }

//单表达式可省略return关键字
array.sort{ $0 < $1 }

//使用高阶函数,指定排序规则
array.sort(by: <)

上述代码,展示了闭包表达式的好处:

  • 使用尾随闭包,{}放在函数外⾯,提⾼可读性
  • 利⽤上下⽂推断参数和返回值类型
  • 单表达式可以隐⼠返回,可省略return关键字
  • 参数简写,通过$0、$1、$2...按顺序替代对应位置的参数
  • 最后的代码并不是尾随闭包的写法,使用高阶函数指定排序规则,可以只传入<,这种方式by:不能省略
捕获值
func makeIncrementer() -> () -> Int {
    var runningTotal = 10
    
    func incrementer() -> Int {
        runningTotal += 1
        return runningTotal
    }
    return incrementer
}

print("makeIncrementer:\(makeIncrementer()())")
print("makeIncrementer:\(makeIncrementer()())")
print("makeIncrementer:\(makeIncrementer()())")
print("--------------------")

let makeInc = makeIncrementer()

print("makeInc:\(makeInc())")
print("makeInc:\(makeInc())")
print("makeInc:\(makeInc())")

//输出以下内容:
//makeIncrementer:11
//makeIncrementer:11
//makeIncrementer:11
//--------------------
//makeInc:11
//makeInc:12
//makeInc:13

上述代码中,使用makeIncrementer()()调用,每次都重新分配runningTotal变量并进行+1,所以输出结果都是11。但使用makeInc()调用,内嵌函数incrementer内部捕获了runningTotal变量,所以输出结果是11、12、13

通过SIL代码,分析makeIncrementer函数是如何捕获变量的

将上述代码生成SIL文件:swiftc -emit-sil main.swift | xcrun swift-demangle

makeIncrementer

alloc_box官方文档说明:在堆上分配一块内存空间,存储了metadatarefCount、当前的value

alloc_box

通过断点查看汇编代码,确实调⽤了swift_allocObject⽅法

汇编代码

  • 捕获变量的本质:就是在堆上开辟内存空间,将当前的变量存储到里面
  • 闭包的本质:就是当前的内嵌函数,加上捕获的变量或者常量
  • ⼀个闭包能够从上下⽂捕获已被定义的常量和变量。即使定义这些常量和变量的原作⽤域已经不存在, 闭包仍能够在其函数体内引⽤和修改这些值
  • 每次修改捕获值的时候,修改的是堆区中的value
  • 每次重新执⾏当前函数的时候,都会重新创建内存空间
闭包是引⽤类型

makeIncrementer函数赋值给变量,这时变量⾥⾯存储的是什么?是函数地址吗?

这⾥通过断点查看汇编代码,无法看出makeInc变量存储的是什么

汇编代码

这时可以把SIL代码再降⼀级,通过IR来观察数据的构成

IR语法
  • 数组
[<elementnumber> x <elementtype>]
//example
alloca [24 x i8], align 8 //24个i8都是0
  • 结构体
%swift.refcounted = type { %swift.type*, i64 }
//表示形式
%T = type {<type list>} //这种和C语⾔的结构体类似
  • 指针类型
<type> *
//example
i64* //64位的整形
  • getelementptr指令
    LLVM中获取数组和结构体的成员,通过getelementptr指令:
<result> = getelementptr <ty>, <ty>* <ptrval>{, [inrange] <ty> <id x>}*
<result> = getelementptr inbounds <ty>, <ty>* <ptrval>{, [inrange] <ty> <idx>}*

创建test.c文件,运行一个来自LLVM官⽹的getelementptr指令的案例

struct munger_struct {
   int f1;
   int f2;
};

void munge(struct munger_struct *P) {
   P[0].f1 = P[1].f1 + P[2].f2;
}

struct munger_struct array[3];

//int main(int argc, const char * argv[]) {
//
//    munge(array);
//    return 0;
//}

将上述代码生成IR文件:clang -S -fobjc-arc -emit-llvm test.c

IR

结合以下代码理解⼀下

int array[4] = {1, 2, 3, 4};
int a = array[0];

其中int a = array[0]这句对应LLVM代码应该是这样的:

a = getelementptr inbounds [4 x i32], [4 x i32]* array, i64 0, i6 4 0

结合下面这张图,可以看到第⼀个0,使⽤基本类型[4 * i32],因此返回的指针前进 0 * 16字节,也就是当前数组的⾸地址。第⼆个index,使⽤的基本类型是 i32,返回的指针前进0字节,也就是当前数组的第⼀个元素。返回的指针类型为i32 *

  • 第⼀个索引不会改变返回指针的类型,也就是说ptrval前⾯的*对应什么类型,就返回什么类型
  • 第⼀个索引的偏移量是由第⼀个索引的值和第⼀个ty指定的基本类型共同确定的
  • 后⾯的索引是在数组或结构体内进⾏索引
  • 每增加⼀个索引,就会使该索引使⽤的基本类型和返回指针的类型去掉⼀层

GEP作⽤于结构体时,其索引⼀定要是常量。GEP指令只是返回⼀个偏移后的指针,并没有访问内存

了解IR语法后,回到makeIncrementer案例,通过IR代码,查看makeInc变量存储的是什么

将上述代码生成IR文件:swiftc -emit-ir main.swift | xcrun swift-demangle

swift.refcounted

找到main.makeIncrementer()函数

makeIncrementer

bitcast在进⾏指针类型转换的过程中,sourceType的位⼤⼩和destType必须相同。如果源类型是指针,则⽬标类型也必须是相同⼤⼩的指针,转换就好像该值已存储到内存中并作为destType类型读取⼀样。类似Swift中的unsafeBitCast

定义FuntionData结构体,仿照IRswift.function进行代码还原

struct FuntionData<T>{
   var ptr: UnsafeRawPointer
   var captureValue: UnsafePointer<T>
}
  • ptr:内嵌函数地址
  • captureValue:捕获值地址

定义HeapObject结构体,相当于swift.refcounted

struct HeapObject{
   var type: UnsafeRawPointer
   var refCount1: UInt32
   var refCount2: UInt32
}

定义Box结构体,相当于swift.full_boxmetadata

struct Box<T> {
   var refCounted: HeapObject
   var value: T
}
  • refCounted:HeapObject
  • value:捕获值

定义完所有结构体,在使用过程中,makeInc无法直接绑定为FuntionData<Box<Int>>类型,因为编译器无法推断出具体类型

编译报错

定义VoidIntFun结构体,用结构体将函数包裹一层,并将函数设为结构体的第一个属性,目的是利用结构体地址就是首元素地址的特性

struct VoidIntFun {
   var f: () ->Int
}

声明makeInc结构体,传入makeIncrementer函数,获取VoidIntFunc类型指针ptr,再将ptr绑定为FunctionData<Box<Int>>类型,最终返回指针

var makeInc = VoidIntFun(f: makeIncrementer())

let ptr = UnsafeMutablePointer<VoidIntFun>.allocate(capacity: 1)
ptr.initialize(to: makeInc)

let ctx = ptr.withMemoryRebound(to: FuntionData<Box<Int>>.self, capacity: >1) {
   $0.pointee
}

print("incrementer内嵌函数地址:\(ctx.ptr)")
print("runningTotal值:\(ctx.captureValue.pointee.value)")

//输出以下内容:
//incrementer内嵌函数地址:0x0000000100005800
//runningTotal值:10

搜索符号:nmMach-O路径】| grep【地址】。在终端搜索地址0000000100005800,记住没有前面的0x

nm /Users/x/Library/Developer/Xcode/DerivedData/LGSwiftTest-ditbotpgxmdgkigjpenpcpgbyiwt/Build/Products/Debug/LGSwiftTest | grep 0000000100005800

输出incrementer内嵌函数的符号

0000000100005800 t _$s11LGSwiftTest15makeIncrementerSiycyF11incrementerL_SiyFTA

还原符号名称:xcrun swift-demangle【符号】,在终端还原符号名称,记住没有前面的_$

xcrun swift-demangle s11LGSwiftTest15makeIncrementerSiycyF11incrementerL_SiyFTA

输出incrementer内嵌函数

$s11LGSwiftTest15makeIncrementerSiycyF11incrementerL_SiyFTA ---> partial apply forwarder for incrementer #1 () -> Swift.Int in LGSwiftTest.makeIncrementer() -> () -> Swift.Int

如果捕获的是多个值,内存布局是什么样子的?
修改案例,让makeIncrementer函数接收一个Int类型参数

func makeIncrementer(forIncrement amount: Int) -> () -> Int {
   var runningTotal = 5
   func incrementer() -> Int {
       runningTotal += amount
       return runningTotal
   }
   return incrementer
}

将上述代码生成IR文件:swiftc -emit-sil main.swift | xcrun swift-demangle

IR

通过lldb查看内存

lldb

案例:捕获多个值

捕获makeIncrementer函数的入参amount,以及函数内部的runningTotalvalstr三个不同数据类型变量

struct HeapObject{
    var type: UnsafeRawPointer
    var refCount1: UInt32
    var refCount2: UInt32
}

struct FuntionData<T>{
    var ptr: UnsafeRawPointer
    var captureValue: UnsafePointer<T>
}

struct Box<T1,T2,T3,T4> {
    var refCounted: HeapObject
    var valueBox: UnsafePointer<ValueBox<T1,T2,T3>>
    var value: T4
}

struct ValueBox<T1,T2,T3> {
    var obj1: ValueBoxObj<T1>
    var obj2: ValueBoxObj<T2>
    var obj3: ValueBoxObj<T3>
}

struct ValueBoxObj<T> {
    var refCounted: HeapObject
    var value: T
    var type: UnsafeRawPointer
}

struct VoidIntFun {
    var f: () ->Int
}

func makeIncrementer(forIncrement amount: Int) -> () -> Int {
    var runningTotal: Int = 3
    var val: Double = 5.5
    var str: String = "Zang"
    
    func incrementer() -> Int {

        runningTotal += amount
        val += Double(amount)
        str += String(amount)

        return runningTotal
    }
    return incrementer
}

var makeInc = VoidIntFun(f: makeIncrementer(forIncrement: 10))

let ptr = UnsafeMutablePointer<VoidIntFun>.allocate(capacity: 1)
ptr.initialize(to: makeInc)

let ctx = ptr.withMemoryRebound(to: FuntionData<Box<Int,Double,String,Int>>.self, capacity: 1) {
    $0.pointee
}

print("内嵌函数地址:\(ctx.ptr)")
print("amount值:\(ctx.captureValue.pointee.value)")
print("runningTotal值:\(ctx.captureValue.pointee.valueBox.pointee.obj1.value)")
print("val值:\(ctx.captureValue.pointee.valueBox.pointee.obj2.value)")
print("str值:\(ctx.captureValue.pointee.valueBox.pointee.obj3.value)")

//输出以下内容:
//内嵌函数地址:0x00000001000033f0
//amount值:10
//runningTotal值:3
//val值:5.5
//str值:Zang
  • 捕获值的原理:在堆上开辟内存空间,将捕获的值放到这个内存空间里
  • 修改捕获值的时候,去堆上把变量拿出来,修改的就是堆空间里的值
  • 闭包是一个引用类型(地址传递),底层结构是结构体,包含函数地址和捕获变量的值
函数也是引用类型
func makeIncrementer(inc: Int) -> Int {
   var runningTotal = 10
   return runningTotal + inc
}

var makeInc = makeIncrementer

将上述代码生成IR文件:swiftc -emit-ir main.swift | xcrun swift-demangle

IR

struct FuntionData{
    var ptr: UnsafeRawPointer
    var captureValue: UnsafeRawPointer?
}

struct VoidIntFun {
    var f: (Int) -> Int
}

func makeIncrementer(inc: Int) -> Int {
   var runningTotal = 10
   return runningTotal + inc
}

var makeInc = VoidIntFun(f: makeIncrementer)

let ptr = UnsafeMutablePointer<VoidIntFun>.allocate(capacity: 1)
ptr.initialize(to: makeInc)

let ctx = ptr.withMemoryRebound(to: FuntionData.self, capacity: 1){$0.pointee}

print("函数地址:\(ctx.ptr)")
print("捕获值:\(ctx.captureValue)")

//输出以下内容:
//函数地址:0x0000000100002070
//捕获值:nil

搜索符号:nmMach-O路径】| grep【地址】。在终端搜索地址0000000100002070,记住没有前面的0x

nm /Users/x/Library/Developer/Xcode/DerivedData/LGSwiftTest-aqvqasiqlxeaiodejtwafdmetstw/Build/Products/Debug/LGSwiftTest | grep 0000000100002070

输出makeIncrementer内嵌函数的符号

0000000100002070 T _$s11LGSwiftTest15makeIncrementer3incS2i_tF

还原符号名称:xcrun swift-demangle【符号】,在终端还原符号名称,记住没有前面的_$

xcrun swift-demangle s11LGSwiftTest15makeIncrementer3incS2i_tF

输出makeIncrementer内嵌函数

$s11LGSwiftTest15makeIncrementer3incS2i_tF ---> LGSwiftTest.makeIncrementer(inc: Swift.Int) -> Swift.Int

函数的本质:函数是引用类型,底层结构是结构体{函数地址,null}。结构体只有函数地址,捕获值为nil

逃逸闭包

逃逸闭包的定义:当闭包作为⼀个实际参数传递给⼀个函数时,并且在函数返回后调⽤,我们就说这个闭包逃逸了。当我们声明⼀个接收闭包作为形式参数的函数时,可以在形式参数前写@escaping来明确闭包是允许逃逸的

案例1:

将闭包赋值给变量complitionHandler,存储后进行调用,如果⽤@escaping修饰闭包后,在逃逸闭包中应该显示引用self

class LGTeacher {
    var complitionHandler: ((Int)->Void)?

    func makeIncrementer(amount: Int,  handler: @escaping (Int) -> Void){
        var runningTotal = 0
        runningTotal += amount

        self.complitionHandler = handler
    }

    func doSomething(){
        self.makeIncrementer(amount: 10) {
            print($0)
        }
    }

    deinit {
        print("LGTeaher deinit")
    }
}

var t = LGTeacher()
t.doSomething()
t.complitionHandler?(10)

//输出以下内容:
//10

complitionHandlerLGTeachermakeIncrementer⽅法调⽤完成之后才会调⽤,此时闭包的⽣命周期⽐当前⽅法⽣命周期更⻓,需要将闭包声明成逃逸闭包。此案例没有打印LGTeaher deinit,说明内部存在循环引用

案例2:

使用DispatchQueue.global().asyncAfter延迟调用

class LGTeacher {

    func makeIncrementer(amount: Int,  handler: @escaping (Int) -> Void){
        var runningTotal = 0
        runningTotal += amount

        DispatchQueue.global().asyncAfter(wallDeadline: .now() + 0.1) {
            handler(runningTotal)
        }
    }

    func doSomething(){
        self.makeIncrementer(amount: 10) {
            print($0)
        }
    }

    deinit {
        print("LGTeaher deinit")
    }
}

var t = LGTeacher()
t.doSomething()

//输出以下内容:
//LGTeaher deinit
//10

makeIncrementer⽅法的执⾏,不会等待闭包执⾏完再执⾏,⽽是直接返回。如果闭包的⽣命周期⽐⽅法更⻓,需要将闭包声明成逃逸闭包

上述两个案例,如果不使用@escaping修饰,编译报错

编译报错

  • 逃逸闭包的使用,一般都是案例中存储、延迟这两种情况。逃逸闭包非常消耗资源,且逃逸闭包可能造成循环引用,需慎用
  • 逃逸闭包中应该显示引用self,表示这里有可能产生循环引用,起到提示作用,没什么其他目的
非逃逸闭包

Swift 3.0之后,系统默认闭包为非逃逸闭包,参数被@nonescaping修饰,可以通过SIL代码查看

func test(by: () -> Void) {
   by()
}

将上述代码生成SIL文件:swiftc -emit-sil main.swift | xcrun swift-demangle

@nonescaping

  • 生命周期与函数一致,闭包只能在函数体内执行
  • 函数执行完后,闭包表达式消失
  • 非逃逸闭包不会产生循环引用
  • 非逃逸闭包编译器会做优化,省略掉一些内存管理调用
  • 非逃逸闭包上下文保存栈上,而不是堆上(官方文档看到的,没有验证出来)
自动闭包
func debugOutPrint(_ condition: Bool , _ message: String){
    if condition {
        print("lg_debug:\(message)")
    }
}

debugOutPrint(true, "Application Error Occured")

//输出以下内容:
//lg_debug:Application Error Occured

上述代码,只有在conditontrue的时候,才会打印传入的错误信息,也就意味着false的时候当前条件不会执⾏

如果当前的字符串,需要在某个业务逻辑功能中获取,一般会这样写:

func debugOutPrint(_ condition: Bool , _ message: String){
    if condition {
        print("lg_debug:\(message)")
    }
}

func doSomething() -> String{
    //do something and get error message
    return "NetWork Error Occured"
}

debugOutPrint(true, doSomething())

//输出以下内容:
//lg_debug:NetWork Error Occured

这种写法会有⼀个问题,那就是当前的conditon,⽆论是true还是falsedoSomething⽅法都会被执⾏。如果doSomething⽅法内部是耗时的任务操作,那么这⾥就造成了资源浪费

这时会想到把当前参数修改成⼀个闭包,以此来解决资源浪费的问题

func debugOutPrint(_ condition: Bool , _ message: () -> String){
    if condition {
        print("lg_debug:\(message())")
    }
}

func doSomething() -> String{
    //do something and get error message
    return "NetWork Error Occured"
}

debugOutPrint(true, doSomething)

//输出以下内容:
//lg_debug:NetWork Error Occured

这样的修改,虽然能满⾜仅在符合条件的情况下才执行doSomething⽅法,但新问题⼜随之⽽来了。这⾥是⼀个闭包,如果输出的信息是一个需要从外界传⼊的String,那又该怎么办呢?直接给方法传入String是会编译报错的

编译报错

这种情况可以使⽤@autoclosure关键字,将当前的表达式声明成⼀个⾃动闭包。⾃动闭包不接收任何参数,返回值是当前内部表达式的值。所以实际上我们传⼊的String就是放在⼀个闭包表达式中,在调⽤的时候返回

func debugOutPrint(_ condition: Bool , _ message: @autoclosure () -> String){
    if condition {
        print("lg_debug:\(message())")
    }
}

debugOutPrint(true, "Application Error Occured")

//输出以下内容:
//lg_debug:Application Error Occured

使用@autoclosure声明⾃动闭包,相当于将传⼊的String使用闭包进行包裹,然后又在闭包中将String直接进行return。就像下面的代码一样:

{
   return "Application Error Occured"
}
  • ⾃动闭包不接收任何参数
  • 返回值是内部表达式的值,在调⽤的时候返回

使用@autoclosure声明⾃动闭包,也可以直接传入函数,且该函数在未满足条件时,不会被执行

func debugOutPrint(_ condition: Bool , _ message: @autoclosure () -> String){
    
    print("condition当前为:\(condition)")
    
    if condition {
        print("lg_debug:\(message())")
    }
    
    print("----------\n")
}

func doSomething() -> String{
    
    print("执行了doSomething函数")
    return "NetWork Error Occured"
}

debugOutPrint(true, doSomething())
debugOutPrint(false, doSomething())

//输出以下内容:
//condition当前为:true
//执行了doSomething函数
//lg_debug:NetWork Error Occured
//----------
//
//condition当前为:false
//----------
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,711评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,079评论 3 387
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,194评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,089评论 1 286
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,197评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,306评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,338评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,119评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,541评论 1 306
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,846评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,014评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,694评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,322评论 3 318
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,026评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,257评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,863评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,895评论 2 351

推荐阅读更多精彩内容

  • 前言:本篇文章的目的,在于记录学习过程,敦促自己,方便查看。练习工具:Playground学习网站: swift5...
    麦穗0615阅读 373评论 0 0
  • swift进阶 学习大纲[https://www.jianshu.com/p/0fc67b373540] 在 sw...
    markhetao阅读 867评论 0 3
  • Swift 中的闭包是自包含的函数代码块,可以在代码中被传递和使用。类似于OC中的Block以及其他函数的匿名函数...
    乔克_叔叔阅读 522评论 1 3
  • 86.复合 Cases 共享相同代码块的多个switch 分支 分支可以合并, 写在分支后用逗号分开。如果任何模式...
    无沣阅读 1,356评论 1 5
  • 前情提要 Swift的闭包和OC的Block是一回事,是一种特殊的函数-带有自动变量的匿名函数。 分别从语法和原理...
    Jacob6666阅读 408评论 0 0