This is the 17th day of my participation in the August Challenge

This article focuses on closures and how they capture variables

closure

A closure is a function that captures a constant or variable in the global context. In layman’s terms, a closure can be a constant or a function

  • Defines a global function, except that the current global function does not capture values
func test(){
    print("test")
}
Copy the code
  • Function closureThe following function is a closure, in the functionincrementerIs aInline functionfrommakeIncrementerCapture variables inrunningTotal
Func makeIncrementer() -> () -> Int{var runningTotal = 10 Func incrementer() -> Int{runningTotal += 1 return runningTotal} return incrementer}Copy the code
  • Closure expressions/anonymous functions: Here’s oneClosure expression, that is, aAnonymous functions, and it isCapture variables and constants from the context
{(param) -> ReturnType in // method body}Copy the code

Benefits of using closures

  • Use context to infer parameter and return value types

  • 2. A single expression can implicitly return, that is, omit the return keyword

  • 3. A short form of the parameter name, for example, $0 for the first parameter

  • Trailing closure expressions

Closure expression

OC and SWIFT comparison

  • In the OCBlockIt is an anonymous function that needs to have the following characteristics:
    • 1. Scope {}

    • 2. Parameters and return values

    • 3. Code after the function body (in)

  • Closures in SWIFT can be passed as variables or parameters
var clourse: (Int)->(Int) = { (age: Int) in
    return age
}
Copy the code

How closure expressions are used

  • 1. Declare the closure expression as an optional type
// Declare an optional closure <! --> course: (Int) --> course? clourse = nil <! --> course: ((Int) -> course)? clourse = nilCopy the code
  • ** * closure constants **2, passletDeclare the closure as oneconstant(i.e.Once assigned, it cannot be changed)
/ / 2, through the let declare the closure as a constant, namely once assignment cannot be changed after the 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}Copy the code

  • ** closure argument **3
Func test(param: () -> Int){print(param())} var age = 10 test {() -> Int in age += 1 return age}Copy the code

Following the closure

When a closure is the last argument to a function, if the current closure expression is long, we can improve the readability of the code by writing after the closure

Func test(_ a: Int, _ b: Int, _ c: Int, by: (_ item1: Int, _ item2: Int, _ item3: func (_ a: Int, _ b: Int, _ c: Int, by: (_ item1: Int, _ item2: Int, _ item3: Int) -> Bool) -> Bool{return by(a, b, c)} by: {(item1: Int, item2: Int, item3: Bool) -> Bool{return by(a, b, c)} Int) -> Bool in return (item1 + item2 < item3)}) item3) -> Bool in return (item1 + item2 < item3) }Copy the code
  • The ones we usually usearray.sortedIs essentially a trailing closure, and the function has only one argument, as shown below
Array. sorted {(item1: Int, item2: 1, 2, 3) Int) -> Bool in return item1 < item2} Array. sorted {(item1, item2) -> Bool in return item1 < item2} Array. sorted {(item1, item2) in return item1 < item2} Single expressions can be hermits, Sorted {(item1, Array. sorted {$0 < $1} array.sorted {$0 < $1} array.sorted {$0 < $1} 3, sorted (by: <)Copy the code

Capture a variable

What is the printed result of the following code?

Func makeIncrementer() -> () -> Int{var runningTotal = 10 Func incrementer() -> Int{runningTotal += 1 return runningTotal} return incrementer} let makeInc = makeIncrementer() print(makeInc()) print(makeInc()) print(makeInc()) <! -- Print the result --> 11 12 13Copy the code

RunningTotal is a temporary variable, which should be 10 each time it enters the function. RunningTotal is a temporary variable, which should be 10 each time it enters the function. Main reason: Embedded functions capture runningTotal and are no longer just a variable

  • What if it is called this way?
print(makeIncrementer()()) print(makeIncrementer()()) print(makeIncrementer()()) <! -- Print result --> 11 11 11Copy the code

Why does this print the same result every time?

1. SIL analysis

Run the above code through SIL analysis:

  • 1, through thealloc_boxA reference count on the heap is requested and the reference count address is given to RunningTotal, storing the variable on the heap
  • 2, through theproject_boxFetch variables from the heap
  • 3. Pass the extracted variable to the closure for invocation

conclusionSo, the essence of the capture value isStore variables on the heap

2. Breakpoint verification

  • It can also be verified by a breakpoint atmakeIncrementerMethod called internallyswift_allocObjectmethods

conclusion

  • A closure can capture defined constants and variables from the context, even if the original scope of those defined constants and variables does not exist, and the closure can still reference and modify those values in its function body

  • Each time you change the capture value, you change the value in the heap

  • Memory space is recreated each time the current function is re-executed

So in the case above we know:

  • MakeInc is the global variable used to store the makeIncrementer function call, so you need to rely on the last result each time

  • When you call a function directly, you create a new heap each time, so the result is uncorrelated, that is, the result is the same every time

Closures are reference types

One more question here, what exactly does makeInc store? I guess the heap address of runningTotal is stored. Let’s verify this by analyzing it

But at this point, we found that there was no way to analyze the SIL, so can we take the SIL down one level and look at the composition of the data through IR code

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

IR Basic Syntax

  • Convert the code to an IR file with the following command
Swiftc-emit -ir File name >./main.ll && code main.ll For example, -cd path where the file is stored -swiftc-emit -ir main.swift >./main.ll && open main.llCopy the code
  • An array of
[< elementNumber > x < elementType >] <! */ alloca [24 x i8], align 8Copy the code
  • The structure of the body
%T = type {<type list>} <! -- Example --> /* -swift. Refcounted: structure name - %swift.type* : swift. Type pointer type - i64: counted */ %swift. Refcounted = type {%swift. Type *, i64} Counted 64 bytes */ %swift.Copy the code
  • Pointer types
<type> * <! -- Example --> // 64-bit integer type - 8 bytes i64*Copy the code
  • getelementptrinstruction

Getelementptr is used to obtain members of arrays and structures in LLVM. The syntax is as follows:

<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; }Copy the code

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

Clang-s-emit - LLVM file name >./main.ll && code main.ll <! Clang-s -emit- LLVM ${SRCROOT}/06-EnumTestC/main.c >./main.ll && code main.llCopy the code

  • First index:%struct.munger_struct* %13, i32 0Is equivalent toFirst index type + first index value== “jointly decidedThe offset of the first index
  • Second index:i32 0

Let’s think about it in terms of the picture

int main(int argc, const char * argv[]) { int array[4] = {1, 2, 3, 4}; int a = array[0]; return 0; } int a = array[0]; /* - [4 x i32]* array: the first 0: the offset from the array itself, i.e., 0 bytes 0 * 4 bytes - the second 0: The offset to an array element, that is, the first member variable of the structure 0 * 4 bytes */ a = Getelementptr inbounds [4 x i32], [4 x i32]* array, i64 0, i64 0Copy the code
  • You can see the first of these0, using base types[4 x i32], so the returned pointer moves forward 0 * 16Byte, i.e.,First address of the current array
  • The secondindex, using base typesi32, returns a pointer that advances 0 bytes, i.eThe first element of the current array, returns a pointer of typei32*

conclusion

  • The first index does not change the type of the pointer returned. What type does the * before ptrval correspond to

  • The offset of the first index is determined by the value of the first index and the base type specified by the first TY

  • The latter index is indexed within an array or structure body

  • Each additional index removes a layer of the base type used by the index and the pointer type returned (e.g. [4 x i32] removes a layer of i32).

The IR analysis

Analyzing IR code

  • To viewmakeIncrementermethods
    • 1. First passswift_allocObjectcreateswift.refcountedThe structure of the body
    • 2, then willswift.refcountedconvert<{ %swift.refcounted, [8 x i8] }>*Structure (Box)
    • 3, select a member variable whose index is 1 from the structure and store it in[8 x i8]*Contiguous memory space
    • 4. Store the address of the embedded function in i8The voidIn the address
    • 5. Finally, return a structure

Its structure is defined as follows

Copy write

With the above analysis, we mock its internal structure, and then construct a function structure to which we bind the address of makeInc

struct HeapObject { var type: UnsafeRawPointer var refCount1: UInt32 var refCount2: Struct BoxType <BoxType>{var PTR: struct BoxType <BoxType>{var PTR: struct BoxType <BoxType> UnsafeRawPointer var captureValue: UnsafePointer<BoxType>} struct Box<T> {var refCounted: counted Struct VoidIntFun {var f: () ->Int} 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: Ptr. initialize(to: makeInc) let CTX = ptr.withMemoryRebound(to: FunctionData<Box<Int>>.self, capacity: 1) { $0.pointee } print(ctx.ptr) print(ctx.captureValue.pointee) <! --> 0x0000000100002bc0 Box<Int>(ref0000000100004038, refCount1: counted) * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *. 3, refCount2: 2), value: 10)Copy the code
  • Terminal command lookup0000000100002bc0(including0x0000000100002bc0The address of the embedded function)
nm -p / Users/chenjialin/Library/Developer/Xcode/DerivedData / 07, Clourse bsccpnlhsrkbzkdglsojfgisewnx/Build/Products/Debug / 07, Cl ourse | grep 0000000100002bc0Copy the code

Among thems10_7_Clourse15makeIncrementerSiycyF11incrementerL_SiyFTAIs the symbol corresponding to the address of the embedded function

Var makeInc2 = makeIncrementer(); makeInc2 = makeIncrementer(); makeInc2 = makeIncrementer()

Capture the case of two variables

In the example above, we examined the case of the closure capturing a variable. What if we were to capture two variables instead of one? Modify the makeIncrementer function as shown below

func makeIncrementer(forIncrement amount: Int) -> () -> Int{var runningTotal = 0 Func incrementer() -> Int{runningTotal += amount return runningTotal} return incrementer}Copy the code
  • Check its IR code

Internal structure imitation

Continue the case where the impersonation captures two variables based on the impersonation capture of one variable

Struct HeapObject {var type: UnsafeRawPointer var refCount1: UInt32 var refCount2: Struct BoxType <BoxType>{var PTR: struct BoxType <BoxType>{var PTR: struct BoxType <BoxType> UnsafeRawPointer {var captureValue: UnsafePointer<BoxType>} struct Box<T> {var refCounted: counted Struct VoidIntFun {var f: () ->Int} 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) Let CTX = ptr.withMemoryRebound(to: FunctionData<Box<Int>>. Self, capacity: 1) { $0.pointee } print(ctx.ptr) print(ctx.captureValue) <! Print result 0x0000000100002910 0x00000001040098E0Copy the code
  • Use the terminal command to check whether the first address is the address of the embedded function

Note: functions must be usedVoidIntFunWrap, otherwise the converted address is not the address of the embedded function), as shown below

  • throughcatLook at the first address, i.eThe address of the embedded function

-x /8g Indicates the second address– Continue to look at memory

If you haverunningTotalHow about 12? To see if it works as we suspect. As it turns out, they dorunningTotal So, when a closure captures two variables,BoxThe internal structure has changed, and the modified imitation code is as follows:

Struct HeapObject {var type: UnsafeRawPointer var refCount1: UInt32 var refCount2: Struct BoxType <BoxType>{var PTR: struct BoxType <BoxType>{var PTR: struct BoxType <BoxType> UnsafeRawPointer {var captureValue: UnsafePointer<BoxType>} struct Box<T> {var refCounted: counted HeapObject //valueBox Used to store Box types var valueBox: UnsafeRawPointer var value: Struct VoidIntFun {var f: () ->Int} 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) 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: counted) 3, refCount2: 4), valueBox: 0x00000001006094a0, value: 10) 0x00000001006094a0Copy the code

Question: What if you capture 3 variables?

  • The following is a memory case that captures three values

  • Found through IR files fromThe return value is pushed backwards
<! --> ret {i8*, % swif.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 <! // Instead of capturing two variables, The difference is that i64 32 becomes i64 40 %10 = Call noalias % swif.refcounted * @swift_allocobject (% swif.type * getelementptr inbounds) (%swift.full_boxmetadata, %swift.full_boxmetadata* @metadata.3, i32 0, i32 2), i64 40, i64 7) #1Copy the code

So the Box structure is changed to

Struct Box<T> {var refCounted: HeapObject counted var valueBox: UnsafeRawPointer var value1: undefined number of {var refCounted: HeapObject} T var value2: T }Copy the code

The final complete imitation code is

Struct HeapObject {var type: UnsafeRawPointer var refCount1: UInt32 var refCount2: UnsafeRawPointer Struct BoxType <BoxType>{var PTR: struct BoxType <BoxType>{var PTR: struct BoxType <BoxType> UnsafeRawPointer {var captureValue: UnsafePointer<BoxType>} struct Box<T> {var refCounted: counted HeapObject //valueBox Used to store Box types var valueBox: UnsafeRawPointer var value1: T var value2: Struct VoidIntFun {var f: () ->Int} 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 func incrementer() -> Int{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) 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) <! -- Print result --> 10 2Copy the code

As you can see from the print, it is exactly the two parameter values passed in

conclusion

  • 1. Capture value principle: Create a memory space on the heap and put the captured value into this memory space

  • 2. When modifying the captured value: in essence, the value of the heap space is changed

  • 3, closure is a reference type (reference type is address passing), the underlying structure of the closure (is structure: function address + capture variable address == closure)

  • 4. A function is also a reference type (essentially a structure that holds only the address of the function). Again, take the makeIncrementer function as an example

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

var makeInc = makeIncrementer
Copy the code

And if you look at the IR code, the function, as it’s passing, is passingAddress of function Write imitationFunctionDatamodified

Struct FunctionData{var PTR: UnsafeRawPointer var captureValue: UnsafePointer<BoxType>}Copy the code

Then the revised structure is written as follows

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) Let CTX = ptr.withMemoryRebound(to: functiondata. self, capacity: 1) { $0.pointee } print(ctx.ptr) print(ctx.captureValue) <! -- Print result --> 0x0000000100003370 nilCopy the code

throughcatCommand to view the address, the address ismakeIncrementerAddress of function

conclusion

  • A closure can capture a defined constant/variable from the context, even if its scope no longer exists, and the closure can still be referenced and modified within its function body

    • 1. Each time you modify the captured value: Essentially, you modify the value in the heap

    • 2. Each time the current function is re-executed, a new memory space is created

  • Capture value principle: The essence is to open up memory space in the heap and store the captured value in this memory space

  • A closure is a reference type (essentially function address passing) with the underlying structure: closure = function address + capture variable address

  • Functions are also reference types (essentially structures that hold the address of the function)