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 function
incrementer
Is aInline function
frommakeIncrementer
Capture 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 one
Closure 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 OC
Block
It 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, pass
let
Declare 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 use
array.sorted
Is 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 the
alloc_box
A reference count on the heap is requested and the reference count address is given to RunningTotal, storing the variable on the heap - 2, through the
project_box
Fetch 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 at
makeIncrementer
Method called internallyswift_allocObject
methods
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
getelementptr
instruction
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 0
Is 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 these
0
, using base types[4 x i32]
, so the returned pointer moves forward0 * 16
Byte, i.e.,First address of the current array
- The second
index
, 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 view
makeIncrementer
methods- 1. First pass
swift_allocObject
createswift.refcounted
The structure of the body - 2, then will
swift.refcounted
convert<{ %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
- 1. First pass
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 lookup
0000000100002bc0
(including0x0000000100002bc0
是The 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_SiyFTA
Is 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 usedVoidIntFun
Wrap, otherwise the converted address is not the address of the embedded function), as shown below
- through
cat
Look at the first address, i.eThe address of the embedded function
-x /8g Indicates the second address– Continue to look at memory
If you haverunningTotal
How about 12? To see if it works as we suspect. As it turns out, they dorunningTotal
So, when a closure captures two variables,Box
The 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 from
The 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 imitationFunctionData
modified
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
throughcat
Command to view the address, the address ismakeIncrementer
Address 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)