Swift advanced 08: closure & capture principle

Time:2021-11-27

closure

closurecanCapture and storageOf any constants and variables defined in their contextquote, this is the so-calledClose and wrap those constants and variablesTherefore, it is called“closure”Swift can handle everything for youCaptured memory managementOperation of.

Three forms of closure

  • [global function is a special closure]: a global function is a closure that has a name but does not capture any values
//Define a global function, but the current global function does not capture values
func test() {
    print("test")
}
  • [an embedded function is a closure that has a name and can capture values from its upper function]The following function is a closure, and theincrementerIt’s aNested Function , frommakeIncrementerCapture variables inrunningTotal
func makeIncrementer() -> () -> Int{
    var runningTotal = 10
    //An embedded function is also a closure
    func incrementer() -> Int{
        runningTotal += 1
        return runningTotal
    }
    return incrementer
}
  • [closure expression]: is an unnamed closure written in lightweight syntax that can capture constant or variable values in its context

Here is aClosure expression, i.e. oneAnonymous function, and yesCapture variables and constants from context

//The closure expression syntax has the following general forms:
{ (parameters) -> (return type) in
    statements
}

Benefits of using closures

  • 1. Inferring the types of formal parameters and return values by context;
  • 2. The closure of a single expression canImplicit return, that is, omit the return keyword;
  • 3. Shorthand the actual parameter name, for example$0Represents the first parameter;
  • 4、Trailing closureGrammar;

Closure expression

OC vs swift

  • In OCBlockIt’s actually aAnonymous function, the following features are required:

    • 1. Scope {}
    • 2. Parameters and return values
    • 3. Code after function body (in)
  • Closures in swift can be passed as variables or parameters

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

How closure expressions are used

  • [closure expression of optional type]1. Declare the closure expression as an optional type
//Declare a closure of an optional type
<!-- Wrong writing -- >
var clourse: (Int) -> Int?
clourse = nil

<!-- Correct writing -- >
var clourse: ((Int) -> Int)?
clourse = nil
  • [closure constant]2. PassletDeclare a closure as aconstant(i.eOnce assigned, it cannot be changed
//2. The closure is declared as a constant through let, that is, it cannot be changed once assigned
let clourse: (Int) -> Int
clourse = {(age: Int) in
    return age
}
//Error: immutable value 'clourse' may only be initialized once
clourse = {(age: Int) in
    return age
}
Swift advanced 08: closure & capture principle
  • Closure parameters3. Use closures as arguments to functions
//3. Use closures as arguments to functions
func test(param: () -> Int){
    print(param())
}
var age = 10
test { () -> Int in
    age += 1
    return age
}

Trailing closure

When the closure is the last parameter of the function, if the current closure expression is very long, we canTrailing closureThe way of writingImprove code readability

//The closure expression is used as the last parameter of the function
func test(_ a: Int, _ b: Int, _ c: Int, by: (_ item1: Int, _ item2: Int, _ item3: Int) -> Bool) -> Bool{
    return by(a, b, c)
    
}
//Conventional writing
test(10, 20, 30, by: {(_ item1: Int, _ item2: Int, _ item3: Int) -> Bool in
        return (item1 + item2 < item3)
})
//Trailing closure
test(10, 20, 30) { (item1, item2, item3) -> Bool in
    return (item1 + item2 < item3)
}
  • We usually usearray.sortedIn fact, it is a trailing closure, and this function has only one parameter, as shown below
//Array. Sorted is a trailing closure
var array = [1, 2, 3]
//1. Complete writing
array.sorted { (item1: Int, item2: Int) -> Bool in return item1 < item2}
//2. Omit parameter type: infer the type from the parameters in the array
array.sorted { (item1, item2) -> Bool in return item1 < item2}
//3. Omit parameter type + return value type: infer the return value type through return
array.sorted { (item1, item2) in return item1 < item2}
//4. Omit parameter type + return value type + return keyword: a single expression can implicitly return an expression, that is, omit the return keyword
array.sorted { (item1, item2) in item1 < item2}
//5. Parameter name abbreviation
array.sorted {return $0 < $1}
//6. Parameter name abbreviation + omit the return keyword
array.sorted {$0 < $1}
//7. Simplest: direct transmission of comparison symbols
array.sorted (by: <)

Capture principle

Capture a value

What is the print result of the following code?

func makeIncrementer() -> () -> Int {
    var runningTotal = 10
    //An embedded function is also a closure
    func incrementer() -> Int {
        runningTotal += 1
        return runningTotal
    }
    return incrementer
}
let makeInc = makeIncrementer()
print(makeInc())
print(makeInc())
print(makeInc())

<!-- Print results -- >
11
12
13

The print results are as follows. It can be seen from the results that each result is accumulated based on the last function execution, but as far as we knowrunningTotalIt is a temporary variable. It is supposed to be 10 every time it enters the function. Why is it accumulated every time?main cause: embedded function capturedrunningTotal, it is no longer a simple variable

  • What if it is called in the following way?
print(makeIncrementer()())
print(makeIncrementer()())
print(makeIncrementer()())

<!-- Print results -- >
11
11
11

Why does this print the same result every time?

1. SIL analysis

Analyze the above codes through SIL:

  • 1. Passalloc_boxA memory space is requested on the heap and variables are stored on the heap
  • 2. Passproject_boxTake the variable from the heap
  • 3. Give the extracted variable to the closure for calling

    Swift advanced 08: closure & capture principle

conclusionThe essence of capturing values is:Variables are stored on the heap

2. Breakpoint verification

  • It can also be verified through breakpointsmakeIncrementerMethod was called internallyswift_allocObjectmethod
Swift advanced 08: closure & capture principle

summary

  • A closure can capture defined constants and variables from the context. Even if the original scope of these defined constants and variables does not exist, the closure can still reference and modify these values in its function body
  • When every timeModify capture valueWhen, you modifyValue value in heap
  • When every timeRe execute the current functionWhen, willRecreate memory space

So in the above case, we know:

  • makeIncIs used for storagemakeIncrementerThe global variable of the function call, so you need to rely on the last result every time
  • When directly calling a function, it is equivalent to creating a new heap memory each time, so the results are irrelevant, that is, the results are consistent each time

Closures are reference types

Here’s another question,makeIncWhat exactly is stored? Personal guess stored isrunningTotalFor the heap address of, let’s verify it by analysis

But at this time, we find that there is no way to analyze anything through SIL, so we can reduce SIL by one levelIRCode to observe the composition of the data

Before analyzing, let’s first understand the basic syntax of IR

IR basic syntax

  • Convert the code to an IR file with the following command
Swift C - emit IR file name >. / main.ll & & code main.ll

For example:
-CD file path
- swiftc -emit-ir main.swift > ./main.ll && open main.ll
  • array
/*
-Elementnumber the number of data stored in the array
-The type of data stored in the ElementType array
*/
[<elementnumber> x <elementtype>]

<!-- Example -- >
/*
All 24 I8 are 0
-In: integer indicating the number of bits, that is, 8-bit integer - 1 byte
*/
alloca [24 x i8], align 8
  • structural morphology
/*
-T: structure name
-< type list >: list, that is, the member list of the structure
*/
//It is similar to the structure of C language
%T = type {<type list>}


<!-- Example -- >
/*
-Swift.refcounted: structure name
-% swift. Type *: swift.type pointer type
-I64: 64 bit integer - 8 bytes
*/
%swift.refcounted = type { %swift.type*, i64}
  • Pointer type
<type> *

<!-- Example -- >
//64 bit integer - 8 bytes
i64*
  • getelementptrinstructions

When getting members of arrays and structures in llvmgetelementptr, the syntax rules are as follows:

<result> = getelementptr <ty>, <ty>* <ptrval>{, [inrange] <ty> <id x>}*

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

<!-- Example -- >
struct munger_struct{
    int f1;
    int f2;
};
void munge(struct munger_struct *P){
    P[0].f1 = P[1].f1 + P[2].f2;
}

//Use
struct munger_struct* array[3];

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

Compile C / C + + into IR with the following command

Clang - S - emit llvm filename >. / main.ll & & code main.ll

<!-- Example -- >
clang -S -emit-llvm ${SRCROOT}/HTClourseTest/main.c > ./main.ll && "/Applications/Visual Studio Code.app/Contents/Resources/app/bin/code" main.ll
Swift advanced 08: closure & capture principle

Combined with figure to understand

int main(int argc, const char * argv[]) { 
    int array[4] = {1, 2, 3, 4}; 
    int a = array[0];
    return 0;
}
Where int a = array [0]; The llvm code corresponding to this sentence should be as follows:
/*
-[4 x I32] * array: the first address of the array
-The first 0: offset relative to the array itself, that is, offset 0 bytes 0 * 4 bytes
-The second 0: the offset relative to the array element, that is, the first member variable of the structure is 0 * 4 bytes
*/
a = getelementptr inbounds [4 x i32], [4 x i32]* array, i64 0, i64 0
  • You can see the first one0, using basic types[4 x i32]Therefore, the returned pointer advances0 * 16Byte, i.eFirst address of current array
  • the secondindex, using basic typesi32, the returned pointer advances 0 bytes, i.eThe first element of the current array, the returned pointer type isi32*
Swift advanced 08: closure & capture principle

summary

  • The first index does not change the type of pointer returned, that isptrvalWhat type does the preceding * correspond to and what type is returned
  • Of the first indexOffsetByValue of the first indexandThe first ty specifies the base typeJointly determined
  • The following indexes are indexed in an array or structure
  • Each additional index will make theIndex usageofRemove one layer from the basic type and the returned pointer type(for example, if [4 x I32] is removed, the next layer is I32)

IR analysis

Analyze IR code
  • seemakeIncrementermethod
    • 1. First passswift_allocObjectestablishswift.refcountedstructural morphology
    • 2. Thenswift.refcountedConvert to<{ %swift.refcounted, [8 x i8] }>*Structure (i.e. box)
    • 3. Take out the member variable with index equal to 1 in the structure and store it in the[8 x i8]*In contiguous memory space
    • 4. Store the address of the embedded function in I8 the void address
    • 5. Finally, a structure is returned
Swift advanced 08: closure & capture principle

Its structure is defined as follows

Swift advanced 08: closure & capture principle
Imitation writing

Through the above analysis, copy its internal structure, and then construct a function structuremakeIncThe address of is bound to the structure

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

//Function return value structure
//Boxtype is a generic type, which is ultimately determined by the incoming box
struct FunctionData<BoxType>{
    //Embedded function address
    var ptr: UnsafeRawPointer
    var captureValue: UnsafePointer<BoxType>
}

//Structure of capture value
struct Box<T> {
    var refCounted: HeapObject
    var value: T
}

//Encapsulates the structure of a closure so that the return value is not affected
struct VoidIntFun {
    var f: () ->Int
}

//What is the print result of the following code?
func makeIncrementer() -> () -> Int{
    var runningTotal = 10
    //An embedded function is also a closure
    func incrementer() -> Int{
        runningTotal += 1
        return runningTotal
    }
    return incrementer
}
let makeInc = VoidIntFun(f: makeIncrementer())

let ptr = UnsafeMutablePointer<VoidIntFun>.allocate(capacity: 1)
//Initialized memory space
ptr.initialize(to: makeInc)
//Rebind PTR to memory
let ctx = ptr.withMemoryRebound(to: FunctionData<Box<Int>>.self, capacity: 1) {
     $0.pointee
}
print(ctx.ptr)
print(ctx.captureValue.pointee)

<!-- Print results -- >
0x00000001000018f0
Box<Int>(refCounted: HTClourseTest.HeapObject(type: 0x0000000100004038, refCount1: 2, refCount2: 2), value: 10)
  • Terminal command lookup00000001000018f0(including:0x00000001000018f0yesAddress of the embedded function
nm -p HTClourseTest | grep 00000001000018f0

amongt _$s13HTClourseTest15makeIncrementerSiycyF11incrementerL_SiyFTAIs the symbol corresponding to the address of the embedded function

Swift advanced 08: closure & capture principle

conclusion: so when wevar makeInc2 = makeIncrementer()When used, it is equivalent to givingmakeInc2namelyFunctionDataStructure, which is associated with the address of the embedded function and the address of the captured variable, so it can be accumulated on the basis of the previous one

Capture two variables

In the above case, we analyzed the case where a closure captures a variable. If yes, we willCapture a variableChange toCapture two variablesAnd? Modify as followsmakeIncrementerfunction

func makeIncrementer(forIncrement amount: Int) -> () -> Int {
    var runningTotal = 0
    //An embedded function is also a closure
    func incrementer() -> Int {
        runningTotal += amount
        return runningTotal
    }
    return incrementer
}
  • View its IR code
Swift advanced 08: closure & capture principle
Internal structure imitation

According to the imitation of capturing one variable, continue to imitate the case of capturing two variables

//2. How closures capture multiple values
struct HeapObject {
    var type: UnsafeRawPointer
    var refCount1: UInt32
    var refCount2: UInt32
}

//Function return value structure
//Boxtype is a generic type, which is ultimately determined by the incoming box
struct FunctionData<BoxType>{
    Var PTR: unsaferawpointer // address of embedded function
    var captureValue: UnsafePointer<BoxType>
}

//Structure of capture value
struct Box<T> {
    var refCounted: HeapObject
    var value: T
}

//Encapsulates the structure of a closure so that the return value is not affected
struct VoidIntFun {
    var f: () ->Int
}

//What is the print result of the following code?
func makeIncrementer(forIncrement amount: Int) -> () -> Int{
    var runningTotal = 0
    //An embedded function is also a closure
    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)
//Initialized memory space
ptr.initialize(to: f)
//Rebind PTR to memory
let ctx = ptr.withMemoryRebound(to: FunctionData<Box<Int>>.self, capacity: 1) {
     $0.pointee
}
print(ctx.ptr)
print(ctx.captureValue)

<!-- Print results -- >
0x00000001000058c0
0x0000000100640310
  • Check whether the first address is the address of the embedded function through the terminal command
Swift advanced 08: closure & capture principle
  • adoptcatView the first address, i.eAddress of the embedded function
Swift advanced 08: closure & capture principle
  • x/8gSecond address
Swift advanced 08: closure & capture principle
  • Continue to view memory
Swift advanced 08: closure & capture principle

If willrunningTotalChange it to 12? To verify whether it is as we suspect. It turns out that it is storedrunningTotal

Swift advanced 08: closure & capture principle

Therefore, when the closure captures two variables, the box structure changes. The modified imitation code is as follows:

//2. How closures capture multiple values
struct HeapObject {
    var type: UnsafeRawPointer
    var refCount1: UInt32
    var refCount2: UInt32
}

//Function return value structure
//Boxtype is a generic type, which is ultimately determined by the incoming box
struct FunctionData<BoxType>{
    Var PTR: unsaferawpointer // address of embedded function
    var captureValue: UnsafePointer<BoxType>
}

//Structure of capture value
struct Box<T> {
    var refCounted: HeapObject
    //Valuebox is used to store the box type
    var valueBox: UnsafeRawPointer
    var value: T
}

//Encapsulates the structure of a closure so that the return value is not affected
struct VoidIntFun {
    var f: () ->Int
}

//What is the print result of the following code?
func makeIncrementer(forIncrement amount: Int) -> () -> Int{
    var runningTotal = 12
    //An embedded function is also a closure
    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)
//Initialized memory space
ptr.initialize(to: f)
//Rebind PTR to memory
let ctx = ptr.withMemoryRebound(to: FunctionData<Box<Int>>.self, capacity: 1) {
     $0.pointee
}
print(ctx.ptr)
print(ctx.captureValue.pointee)
print(ctx.captureValue.pointee.valueBox)

<!-- Print results -- >
0x0000000100005860
Box<Int>(refCounted: HTClourseTest.HeapObject(type: 0x0000000100008098, refCount1: 2, refCount2: 4), valueBox: 0x0000000100481330, value: 10)
0x0000000100481330
Question: what if you capture three variables?
  • As shown below, it is the memory condition that captures three values
Swift advanced 08: closure & capture principle
  • Found through IR file, fromBack extrapolation of return value
<!-- Return value -- >
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-->
//Compared with capturing two variables, the difference is that i64 32 becomes 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

thereforeBoxThe structure is changed to

//Structure of capture value
struct Box<T> {
    var refCounted: HeapObject
    //This is also a heapobject
    var valueBox: UnsafeRawPointer
    var value1: T
    var value2: T
}

The final complete imitation code is

//2. How closures capture multiple values
struct HeapObject {
    var type: UnsafeRawPointer
    var refCount1: UInt32
    var refCount2: UInt32
}

//Function return value structure
//Boxtype is a generic type, which is ultimately determined by the incoming box
struct FunctionData<BoxType>{
    Var PTR: unsaferawpointer // address of embedded function
    var captureValue: UnsafePointer<BoxType>
}

//Structure of capture value
struct Box<T> {
    var refCounted: HeapObject
    //Valuebox is used to store the box type
    var valueBox: UnsafeRawPointer
    var value: T
    var value2: T
}

//Encapsulates the structure of a closure so that the return value is not affected
struct VoidIntFun {
    var f: () ->Int
}

//What is the print result of the following code?
func makeIncrementer(forIncrement amount: Int, amount2: Int) -> () -> Int{
    var runningTotal = 12
    //An embedded function is also a closure
    func incrementer() -> Int{
        runningTotal += amount
        runningTotal += amount2
        return runningTotal
    }
    return incrementer
}
var makeInc = makeIncrementer(forIncrement: 10, amount2: 18)
var f = VoidIntFun(f: makeInc)

let ptr = UnsafeMutablePointer<VoidIntFun>.allocate(capacity: 1)
//Initialized memory space
ptr.initialize(to: f)
//Rebind PTR to memory
let ctx = ptr.withMemoryRebound(to: FunctionData<Box<Int>>.self, capacity: 1) {
     $0.pointee
}
print(ctx.ptr)
print(ctx.captureValue)
print(ctx.captureValue.pointee)

summary

  • 1. Capture value principle:Open up memory space on the heap and put the captured values in this memory space
  • 2. When modifying capture values: essentiallyModify the value of heap space
  • 3. Closure is areference type(the reference type is address passing), and the underlying structure of the closure is the structure body:Function address + address of capture variable = = closure
  • 4、functionAlso areference type(essentially a structure in which only the address of the function is saved), for example, ormakeIncrementerFunction as an example
func makeIncrementer(inc: Int) -> Int{
    var runningTotal = 1
    return runningTotal + inc
}

var makeInc = makeIncrementer

By analyzing its IR code, the function is passed in the transfer processAddress of the function

Swift advanced 08: closure & capture principle

Will be imitatedFunctionDataMake modifications

struct FunctionData{
    Var PTR: unsaferawpointer // address of embedded function
    var captureValue: UnsafePointer<BoxType>
}

Then the revised structure is imitated as follows

//Functions are also reference types
struct FunctionData{
    //Function address
    var ptr: UnsafeRawPointer
    var captureValue: UnsafeRawPointer?
}

//Encapsulates the structure of a closure so that the return value is not affected
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)
//Initialized memory space
ptr.initialize(to: f)
//Rebind PTR to memory
let ctx = ptr.withMemoryRebound(to: FunctionData.self, capacity: 1) {
     $0.pointee
}

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

<!-- Print results -- >
0x0000000100002140
nil

adoptcatCommand to view the address. The address ismakeIncrementerAddress of the function

Swift advanced 08: closure & capture principle

summary

  • A closure canCapture defined constants / variables from context, even if its scope does not exist, the closure still existsIt can be referenced and modified in its function body

    • 1. Every timeModify capture valueThe essence of the modification is:Value value in heap
    • 2. Every timeRe execute the current function, will restartCreate new memory space
  • Capture value principle: the essence isHeap area opens up memory space, andCapture value storeTo thisMemory space

  • Closure is areference type(the essence isFunction address passing), the underlying structure is:Closure = function address + capture variable address

  • So is the functionreference type(the essence isstructural morphology, where the address of the function is saved)