Swift-進階 09:閉包(一)使用&捕獲原理

Swift 進階之路 文章匯總

本文主要分析閉包以及閉包捕獲變量的原理

閉包

閉包是一個捕獲了全局上下文的常量或者變量的函數,通俗來講,閉包可以是常量也可以是函數

  • 【全局函數是一種特殊的閉包】:定義一個全局函數,只是當前的全局函數并不捕獲值
func test(){
    print("test")
}
  • 【函數閉包】:下面的函數是一個閉包,函數中的incrementer是一個內嵌函數,可以從makeIncrementer中捕獲變量runningTotal
func makeIncrementer() -> () -> Int{
    var runningTotal = 10
    //內嵌函數,也是一個閉包
    func incrementer() -> Int{
        runningTotal += 1
        return runningTotal
    }
    return incrementer
}
  • 【閉包表達式 / 匿名函數】:下面是一個閉包表達式,即一個匿名函數,而且是從上下文中捕獲變量和常量
//閉包表達式
{ (param) -> ReturnType in
    //方法體
}

使用閉包的好處

  • 1、利用上下文推斷參數和返回值類型

  • 2、單表達式可以隱式返回,即省略return關鍵字

  • 3、參數名稱的簡寫,例如 $0表示第一個參數

  • 4、尾隨閉包表達式

閉包表達式

OC與swift的對比

  • OC中的Block其實是一個匿名函數,需要具備以下特點:

    • 1、作用域 {}

    • 2、參數和返回值

    • 3、函數體(in)之后的代碼

  • swift中的閉包,可以當做變量,也可以當做參數傳遞

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

閉包表達式的使用方式

  • 【可選類型的閉包表達式】1、將閉包表達式聲明成一個可選類型
//聲明一個可選類型的閉包
<!--錯誤寫法-->
var clourse: (Int) -> Int?
clourse = nil

<!--正確寫法-->
var clourse: ((Int) -> Int)?
clourse = nil
  • 【閉包常量】2、通過let將閉包聲明成一個常量(即一旦賦值之后就不能更改
//2、通過let將閉包聲明為一個常量,即一旦賦值后就不能改變了
let clourse: (Int) -> Int
clourse = {(age: Int) in
    return age
}
//報錯:Immutable value 'clourse' may only be initialized once
clourse = {(age: Int) in
    return age
}
修改閉包常量報錯
  • 【閉包參數】3、將閉包作為 函數的參數
//3、將閉包作為函數的參數
func test(param: () -> Int){
    print(param())
}
var age = 10
test { () -> Int in
    age += 1
    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, item2, item3) -> Bool in
    return (item1 + item2 < item3)
}
  • 我們平常使用的array.sorted其實就是一個尾隨閉包,且這個函數就只有一個參數,如下所示
//array.sorted就是一個尾隨閉包
var array = [1, 2, 3]
//1、完整寫法
array.sorted { (item1: Int, item2: Int) -> Bool in return item1 < item2}
//2、省略參數類型:通過array中的參數推斷類型
array.sorted { (item1, item2) -> Bool in return item1 < item2}
//3、省略參數類型 + 返回值類型:通過return推斷返回值類型
array.sorted { (item1, item2) in return item1 < item2}
//4、省略參數類型 + 返回值類型 + return關鍵字:單表達式可以隱士表達,即省略return關鍵字
array.sorted { (item1, item2) in item1 < item2}
//5、參數名稱簡寫
array.sorted {return $0 < $1}
//6、參數名稱簡寫 + 省略return關鍵字
array.sorted {$0 < $1}
//7、最簡:直接傳比較符號
array.sorted (by: <)

捕獲一個變量

下面代碼的打印結果是什么?

func makeIncrementer() -> () -> Int{
    var runningTotal = 10
    //內嵌函數,也是一個閉包
    func incrementer() -> Int{
        runningTotal += 1
        return runningTotal
    }
    return incrementer
}
let makeInc = makeIncrementer()
print(makeInc())
print(makeInc())
print(makeInc())

<!--打印結果-->
11
12
13

打印結果如下,從結果中可以看出,每次的結構都是在上次函數執行的基礎上累加的,但是我們所知的runningTotal是一個臨時變量,按理說每次進入函數都是10,這里為什么會每次累加呢? 主要原因:內嵌函數捕獲了runningTotal,不再是單純的一個變量了

  • 如果是下面這種方式調用呢?
print(makeIncrementer()())
print(makeIncrementer()())
print(makeIncrementer()())

<!--打印結果-->
11
11
11

為什么這種方式每次打印的結果就是同一個呢?

1、SIL分析

將上述代碼通過SIL分析:

  • 1、通過alloc_box申請了一個堆上的引用計數,并將引用計數地址給了RunningTotal,將變量存儲到堆上
  • 2、通過project_box從堆上取出變量
  • 3、將取出的變量交給閉包進行調用
    捕獲一個變量的SIL分析

    結論:所以,捕獲值的本質是 將變量存儲到堆上

2、斷點驗證

  • 也可以通過斷點來驗證,在makeIncrementer方法內部調用了swift_allocObject方法
    捕獲一個變量的斷點分析

總結

  • 一個閉包能夠從上下文捕獲已經定義的常量和變量,即使這些定義的常量和變量的原作用域不存在,閉包仍然能夠在其函數體內引用和修改這些值

  • 當每次修改捕獲值時,修改的是堆區中的value值

  • 當每次重新執行當前函數時,都會重新創建內存空間

所以上面的案例中我們知道:

  • makeInc是用于存儲makeIncrementer函數調用的全局變量,所以每次都需要依賴上一次的結果

  • 而直接調用函數時,相當于每次都新建一個堆內存,所以每次的結果都是不關聯的,即每次結果都是一致的

閉包是引用類型

這里還要一個疑問,makeInc存儲的到底是什么?個人猜測存儲的是runningTotal的堆區地址,下面我們通過分析來驗證是否如此

但是此時我們發現,通過SIL并沒有辦法分析出什么,那么可以將SIL降一級,通過IR代碼來觀察數據的構成

在分析之前,首先來了解下IR的基本語法

IR基本語法

  • 通過以下命令將代碼轉換為IR文件
swiftc -emit-ir 文件名 > ./main.ll && code main.ll

例如:
- cd 文件所在路徑
- swiftc -emit-ir main.swift > ./main.ll && open main.ll
  • 數組
/*
- elementnumber 數組中存放數據的數量
- elementtype 數組中存放數據的類型
*/
[<elementnumber> x <elementtype>]

<!--舉例-->
/*
24個i8都是0
- iN:表示多少位的整型,即8位的整型 - 1字節
*/
alloca [24 x i8], align 8
  • 結構體
/*
- T:結構體名稱
- <type list> :列表,即結構體的成員列表
*/
//和C語言的結構體類似
%T = type {<type list>}


<!--舉例-->
/*
- swift.refcounted:結構體名稱
- %swift.type*:swift.type指針類型
- i64:64位整型 - 8字節
*/
%swift.refcounted = type { %swift.type*, i64}
  • 指針類型
<type> *

<!--舉例-->
//64位的整型 - 8字節
i64*
  • getelementptr指令
    在LLVM中獲取數組和結構體的成員時通過getelementptr,語法規則如下:
<result> = getelementptr <ty>, <ty>* <ptrval>{, [inrange] <ty> <id x>}*

<result> = getelementptr inbounds <ty>, <ty>* <ptrval>{, [inrange] <ty> <idx>}*

<!--舉例-->
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;
}

通過下面的命令將c/c++編譯成IR

clang -S -emit-llvm 文件名 > ./main.ll && code main.ll

<!--舉例-->
clang -S -emit-llvm ${SRCROOT}/06-EnumTestC/main.c > ./main.ll && code main.ll
IR代碼分析
  • 第一個索引:%struct.munger_struct* %13, i32 0 等價于 第一個索引類型 + 第一個索引值 ==》 共同決定 第一個索引的偏移量
  • 第二個索引:i32 0

再結合圖來理解

int main(int argc, const char * argv[]) { 
    int array[4] = {1, 2, 3, 4}; 
    int a = array[0];
    return 0;
}
其中int a = array[0];這句對應的LLVM代碼應該是這樣的:
/*
- [4 x i32]* array:數組首地址
- 第一個0:相對于數組自身的偏移,即偏移0字節 0 * 4字節
- 第二個0:相對于數組元素的偏移,即結構體第一個成員變量 0 * 4字節
*/
a = getelementptr inbounds [4 x i32], [4 x i32]* array, i64 0, i64 0
  • 可以看到其中的第一個0,使用基本類型[4 x i32],因此返回的指針前進0 * 16字節,即當前數組首地址
  • 第二個index,使用基本類型 i32,返回的指針前進0字節,即當前數組的第一個元素,返回的指針類型是 i32*
    IR代碼-指針類型圖示

總結

  • 第一個索引不會改變返回的指針的類型,即ptrval前面的*對應什么類型,返回的就是什么類型

  • 第一個索引的偏移量是由第一個索引的值第一個ty指定的基本類型共同確定的

  • 后面的索引是在數組或者結構體內進行索引

  • 每增加一個索引,就會使得該索引使用基本類型和返回的指針類型去掉一層(例如 [4 x i32] 去掉一層是 i32)

IR分析

分析IR代碼

  • 查看makeIncrementer方法
    • 1、首先通過swift_allocObject創建swift.refcounted結構體
    • 2、然后將swift.refcounted轉換為<{ %swift.refcounted, [8 x i8] }>*結構體(即Box)
    • 3、取出結構體中index等于1的成員變量,存儲到[8 x i8]*連續的內存空間中
    • 4、將內嵌函數的地址存儲到i8即void地址中
    • 5、最后返回一個結構體
makeIncrementer函數-IR分析1

其結構體定義如下


makeIncrementer函數-結構體定義

仿寫

通過上述的分析,仿寫其內部的結構體,然后構造一個函數的結構體,將makeInc的地址綁定到結構體中

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

//函數返回值結構體
//BoxType 是一個泛型,最終是由傳入的Box決定的
struct FunctionData<BoxType>{
    //內嵌函數地址
    var ptr: UnsafeRawPointer
    var captureValue: UnsafePointer<BoxType>
}

//捕獲值的結構體
struct Box<T> {
    var refCounted: HeapObject
    var value: T
}

//封裝閉包的結構體,目的是為了使返回值不受影響
struct VoidIntFun {
    var f: () ->Int
}

//下面代碼的打印結果是什么?
func makeIncrementer() -> () -> Int{
    var runningTotal = 10
    //內嵌函數,也是一個閉包
    func incrementer() -> Int{
        runningTotal += 1
        return runningTotal
    }
    return incrementer
}
let makeInc = VoidIntFun(f: makeIncrementer())

let ptr = UnsafeMutablePointer<VoidIntFun>.allocate(capacity: 1)
//初始化的內存空間
ptr.initialize(to: makeInc)
//將ptr重新綁定內存
let ctx = ptr.withMemoryRebound(to: FunctionData<Box<Int>>.self, capacity: 1) {
     $0.pointee
}
print(ctx.ptr)
print(ctx.captureValue.pointee)

<!--打印結果-->
0x0000000100002bc0
Box<Int>(refCounted: _7_Clourse.HeapObject(type: 0x0000000100004038, refCount1: 3, refCount2: 2), value: 10)
  • 終端命令查找0000000100002bc0(其中0x0000000100002bc0內嵌函數的地址
nm -p /Users/chenjialin/Library/Developer/Xcode/DerivedData/07、Clourse-bsccpnlhsrkbzkdglsojfgisewnx/Build/Products/Debug/07、Clourse | grep 0000000100002bc0

其中s10_7_Clourse15makeIncrementerSiycyF11incrementerL_SiyFTA是內嵌函數的地址對應的符號

內嵌函數對應的符號

結論:所以當我們var makeInc2 = makeIncrementer()使用時,相當于給makeInc2就是FunctionData結構體,其中關聯了內嵌函數地址,以及捕獲變量的地址,所以才能在上一個的基礎上進行累加

捕獲兩個變量的情況

上面的案例中,我們分析了閉包捕獲一個變量的情況,如果是將捕獲一個變量更改為捕獲兩個變量呢?如下所示修改makeIncrementer函數

func makeIncrementer(forIncrement amount: Int) -> () -> Int{
    var runningTotal = 0
    //內嵌函數,也是一個閉包
    func incrementer() -> Int{
        runningTotal += amount
        return runningTotal
    }
    return incrementer
}
  • 查看其IR代碼


    捕獲兩個變量的IR分析

內部結構仿寫

根據捕獲一個變量的仿寫,繼續仿寫捕獲兩個變量的情況

//2、閉包捕獲多個值的原理
struct HeapObject {
    var type: UnsafeRawPointer
    var refCount1: UInt32
    var refCount2: UInt32
}

//函數返回值結構體
//BoxType 是一個泛型,最終是由傳入的Box決定的
struct FunctionData<BoxType>{
    var ptr: UnsafeRawPointer//內嵌函數地址
    var captureValue: UnsafePointer<BoxType>
}

//捕獲值的結構體
struct Box<T> {
    var refCounted: HeapObject
    var value: T
}

//封裝閉包的結構體,目的是為了使返回值不受影響
struct VoidIntFun {
    var f: () ->Int
}

//下面代碼的打印結果是什么?
func makeIncrementer(forIncrement amount: Int) -> () -> Int{
    var runningTotal = 0
    //內嵌函數,也是一個閉包
    func incrementer() -> Int{
        runningTotal += amount
        return runningTotal
    }
    return incrementer
}
var makeInc = makeIncrementer(forIncrement: 10)
var f = VoidIntFun(f: makeInc)

let ptr = UnsafeMutablePointer<VoidIntFun>.allocate(capacity: 1)
//初始化的內存空間
ptr.initialize(to: f)
//將ptr重新綁定內存
let ctx = ptr.withMemoryRebound(to: FunctionData<Box<Int>>.self, capacity: 1) {
     $0.pointee
}
print(ctx.ptr)
print(ctx.captureValue)

<!--打印結果-->
0x0000000100002910
0x00000001040098e0
  • 通過終端命令查看第一個地址是否是內嵌函數的地址

    經過包裝的內嵌函數地址

    注:(函數必須使用VoidIntFun包裝下,否則轉換后的地址不是內嵌函數的地址),如下所示
    未經過包裝

  • 通過cat查看 第一個地址,即內嵌函數的地址

    cat查看內嵌函數地址

    • x/8g 第二個地址


      查看內嵌函數地址內存情況-1
    • 繼續查看內存情況


      查看內存情況-2

如果將runningTotal改成12呢?來驗證是否如我們猜想的一樣。事實證明,確實是存儲的runningTotal

修改后的再次驗證

所以,閉包捕獲兩個變量時,Box結構體內部發生了變化,修改后的仿寫代碼如下:

//2、閉包捕獲多個值的原理
struct HeapObject {
    var type: UnsafeRawPointer
    var refCount1: UInt32
    var refCount2: UInt32
}

//函數返回值結構體
//BoxType 是一個泛型,最終是由傳入的Box決定的
struct FunctionData<BoxType>{
    var ptr: UnsafeRawPointer//內嵌函數地址
    var captureValue: UnsafePointer<BoxType>
}

//捕獲值的結構體
struct Box<T> {
    var refCounted: HeapObject
    //valueBox用于存儲Box類型
    var valueBox: UnsafeRawPointer
    var value: T
}

//封裝閉包的結構體,目的是為了使返回值不受影響
struct VoidIntFun {
    var f: () ->Int
}

//下面代碼的打印結果是什么?
func makeIncrementer(forIncrement amount: Int) -> () -> Int{
    var runningTotal = 12
    //內嵌函數,也是一個閉包
    func incrementer() -> Int{
        runningTotal += amount
        return runningTotal
    }
    return incrementer
}

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

let ptr = UnsafeMutablePointer<VoidIntFun>.allocate(capacity: 1)
//初始化的內存空間
ptr.initialize(to: f)
//將ptr重新綁定內存
let ctx = ptr.withMemoryRebound(to: FunctionData<Box<Int, Int>>.self, capacity: 1) {
     $0.pointee
}
print(ctx.ptr)
print(ctx.captureValue.pointee)
print(ctx.captureValue.pointee.valueBox)

<!--打印結果-->
0x0000000100002b30
Box<Int>(refCounted: _7_Clourse.HeapObject(type: 0x0000000100004090, refCount1: 3, refCount2: 4), valueBox: 0x00000001006094a0, value: 10)
0x00000001006094a0

疑問:如果是捕獲3個變量呢?

  • 如下所示,是捕獲三個值的內存情況


    捕獲三個變量的內存分析
  • 通過IR文件發現,從返回值倒推

<!--返回值-->
ret { i8*, %swift.refcounted* } %15

<!--%15-->
%15 = insertvalue { i8*, %swift.refcounted* }
{ i8* bitcast (i64 (%swift.refcounted*)* @"$s4main15makeIncrementer12forIncrement7amount2SiycSi_SitF11incrementerL_SiyFTA" to i8*),
    %swift.refcounted* undef }, %swift.refcounted* %10, 1

<!--%10-->
//與捕獲兩個變量相比,區別在于 i64 32 變成了 i64 40
%10 = call noalias %swift.refcounted* @swift_allocObject(
%swift.type* getelementptr inbounds (%swift.full_boxmetadata, %swift.full_boxmetadata* @metadata.3, i32 0, i32 2),
i64 40, i64 7) #1

所以Box結構體改為

//捕獲值的結構體
struct Box<T> {
    var refCounted: HeapObject
    //這也是一個HeapObject
    var valueBox: UnsafeRawPointer
    var value1: T
    var value2: T
}

最終完整的仿寫代碼為

//3、捕獲3個值
struct HeapObject {
    var type: UnsafeRawPointer
    var refCount1: UInt32
    var refCount2: UInt32
}

//函數返回值結構體
//BoxType 是一個泛型,最終是由傳入的Box決定的
struct FunctionData<BoxType>{
    var ptr: UnsafeRawPointer//內嵌函數地址
    var captureValue: UnsafePointer<BoxType>
}

//捕獲值的結構體
struct Box<T> {
    var refCounted: HeapObject
    //valueBox用于存儲Box類型
    var valueBox: UnsafeRawPointer
    var value1: T
    var value2: T
    
}

//封裝閉包的結構體,目的是為了使返回值不受影響
struct VoidIntFun {
    var f: () ->Int
}
//下面代碼的打印結果是什么?
func makeIncrementer(forIncrement amount: Int, amount2: Int) -> () -> Int{
    var runningTotal = 1
    //內嵌函數,也是一個閉包
    func incrementer() -> Int{
        runningTotal += amount
        runningTotal += amount2
        return runningTotal
    }
    return incrementer
}
var makeInc = makeIncrementer(forIncrement: 10, amount2: 2)
var f = VoidIntFun(f: makeInc)

let ptr = UnsafeMutablePointer<VoidIntFun>.allocate(capacity: 1)
//初始化的內存空間
ptr.initialize(to: f)
//將ptr重新綁定內存
let ctx = ptr.withMemoryRebound(to: FunctionData<Box<Int>>.self, capacity: 1) {
     $0.pointee
}
print(ctx.ptr)
print(ctx.captureValue.pointee.value1)
print(ctx.captureValue.pointee.value2)

<!--打印結果-->
10
2

從打印結果可以看出,正好是傳入的兩個參數值

總結

  • 1、捕獲值原理:在堆上開辟內存空間,并將捕獲的值放到這個內存空間里

  • 2、修改捕獲值時:實質是修改堆空間的值

  • 3、閉包是一個引用類型(引用類型是地址傳遞),閉包的底層結構(是結構體:函數地址 + 捕獲變量的地址 == 閉包

  • 4、函數也是一個引用類型(本質是一個結構體,其中只保存了函數的地址),例如還是以makeIncrementer函數為例

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

var makeInc = makeIncrementer

分析其IR代碼,函數在傳遞過程中,傳遞的就是函數的地址

函數是引用類型分析-1

將仿寫的FunctionData進行修改

struct FunctionData{
    var ptr: UnsafeRawPointer//內嵌函數地址
    var captureValue: UnsafePointer<BoxType>
}

然后改版后的結構仿寫如下

//函數也是引用類型
struct FunctionData{
    //函數地址
    var ptr: UnsafeRawPointer
    var captureValue: UnsafeRawPointer?
}

//封裝閉包的結構體,目的是為了使返回值不受影響
struct VoidIntFun {
    var f: (Int) ->Int
}

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

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

let ptr = UnsafeMutablePointer<VoidIntFun>.allocate(capacity: 1)
//初始化的內存空間
ptr.initialize(to: f)
//將ptr重新綁定內存
let ctx = ptr.withMemoryRebound(to: FunctionData.self, capacity: 1) {
     $0.pointee
}

print(ctx.ptr)
print(ctx.captureValue)

<!--打印結果-->
0x0000000100003370
nil

通過cat命令查看該地址,地址就是makeIncrementer函數的地址

函數是引用類型分析-2

總結

  • 一個閉包能夠從上下文中捕獲已經定義的常量/變量,即使其作用域不存在了,閉包仍然能夠在其函數體內引用、修改

    • 1、每次修改捕獲值:本質修改的是堆區中的value值

    • 2、每次重新執行當前函數,會重新創建新的內存空間

  • 捕獲值原理:本質是在堆區開辟內存空間,并將捕獲值存儲到這個內存空間

  • 閉包是一個引用類型(本質是函數地址傳遞),底層結構為:閉包 = 函數地址 + 捕獲變量的地址

  • 函數也是引用類型(本質是結構體,其中保存了函數的地址)

?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,488評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,034評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,327評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,554評論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,337評論 6 404
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,883評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,975評論 3 439
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,114評論 0 286
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,625評論 1 332
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,555評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,737評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,244評論 5 355
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 43,973評論 3 345
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,362評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,615評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,343評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,699評論 2 370

推薦閱讀更多精彩內容

  • swift進階 學習大綱[http://www.lxweimin.com/p/0fc67b373540] 在 sw...
    markhetao閱讀 899評論 0 3
  • 前情提要 Swift的閉包和OC的Block是一回事,是一種特殊的函數-帶有自動變量的匿名函數。 分別從語法和原理...
    Jacob6666閱讀 424評論 0 0
  • 什么是閉包 維基百科中的解釋:在計算機科學中,閉包(Closure),又稱詞法閉包(Lexical Closure...
    帥駝駝閱讀 441評論 0 3
  • 閉包 閉包是自包含的函數代碼塊,可以在代碼中被傳遞和使用。Swift中的閉包與C和Objective-C中的代碼塊...
    浪的出名閱讀 730評論 0 1
  • 86.復合 Cases 共享相同代碼塊的多個switch 分支 分支可以合并, 寫在分支后用逗號分開。如果任何模式...
    無灃閱讀 1,400評論 1 5