preface
Dart was originally designed with a lot of JavaScript features in mind. It can be seen in both asynchronous processing and syntax. Those of you familiar with Dart should know that everything is an object in Dart. Not only are ints and bool objects created from classes provided by the Core Library, but functions are also considered objects. This article will take you through Dart’s functions & closures and how they can be used.
What is a Closure?
If you’ve never heard of closures, don’t worry, this section introduces the concept of closures from scratch. Before we get serious about closures, we need to take a look at Lexical scoping.
Lexical scope scoping
Maybe you are unfamiliar with this word, but it is the most familiar stranger. Let’s take a look at the following code.
void main() {
var a = 0;
var a = 1; // Error:The name 'a' is already defined
}
Copy the code
As you must have noticed, we made an obvious mistake in this code. The variable a is defined twice, and the compiler will tell us that the variable name a has already been defined.
This is because each of our variables has its lexical scope, and only one variable named A is allowed to exist in the same lexical scope, and syntactic errors can be reported at compile time.
This makes sense because if there are two a variables with the same name in a Lexical scoping, then it is syntactically impossible to tell which A you want to access.
In the code above, we define a twice in the lexical scope of main
It just needs to be modified
void main() {
var a = 1;
print(a); / / = > 1
}
var a = 0;
Copy the code
We can normally print out that a is 1. The simple explanation is var a = 0; Is a variable defined in the dart file’s semantiscoping, and var a = 1; Is a variable defined by semanticimetry in main, and the two are not in the same space, so they do not collide.
Function is Object
First, it’s easy to prove that a method (function) is an object.
print( (){} is Object ); // true
Copy the code
(){} is an anonymous function and we can see that the output is true.
Function is Object.
void main() {
var name = 'Vadaski';
var printName = (){
print(name);
};
}
Copy the code
As you can clearly see, we can define a new method inside main and assign the method to a variable printName.
But if you run this code, you won’t see any output, and that’s why.
In fact, once we’ve defined printName here, we’re not really going to execute it. We know that to execute a method, you need to use XXX() to actually execute it.
void main() {
var name = 'Vadaski';
var printName = (){
print(name);
};
printName(); // Vadaski
}
Copy the code
The example above is very common, where the externally defined variable name is accessed inside printName. That is, the internal variables of a Lexical scoping can be accessed by the external variables defined in the Lexical scoping.
Function + Lexical scoping
Internal access to externally defined variables is ok, and it’s easy to wonder if externally defined variables can be accessed.
For normal access, this would look like the following.
void main() {
var printName = (){
var name = 'Vadaski';
};
printName();
print(name); // Error: Undefined name' name'
}
Copy the code
The variable defined in printName is not visible to the variables in main. Dart, like JavaScript, has chained scope, meaning that a child scope can access variables in a parent (or even an ancestor) scope and not vice versa.
Access rules
As we can see from the above example, synecdosimetry actually exists as a chain. A new scope can be opened in one scope, and variables with same names can be allowed in different scopes. What rules do we use to access a variable in a scope?
void main() {
var a = 1;
firstScope(){
var a = 2;
print('$a in firstScope'); //2 in firstScope
}
print('$a in mainScope'); //1 in mainScope
firstScope();
}
Copy the code
In the example above, we can see that the variable A is defined in both main and firstScope. We print in firstScope, prints 2 in firstScope and print in main prints 1 in mainScope.
We can already summarize the rule: the nearest is first.
If you access a variable in a scope, it first looks to see if the variable is already defined in the current scope, and if so, it is used. If the current scope does not find the variable, it looks for it in the scope above it, and so on down to the original scope. If this variable does not exist on any scope chain, Error: Undefined name’ name’ is raised.
Dart scope variables are statically determined.
void main() { print(a); // Local variable 'a' can't be referenced before it is declared var a; } var a = 0; Copy the code
We can see that the variable A exists in main’s parent scope and has been assigned a value, but we also define a variable ain main’s scope. If a is not specified within the scope of print, the scope of a is not specified within the scope of print. If a is not specified within the scope of print, the scope of A is not specified within the scope of print. Local variable ‘a’ can’t be referenced before it is declared.
The definition of a Closure
With that in mind, we can now look at the definition of Closure.
A closure is a function object that has access to variables in its lexical scope, even when the function is used outside of its original scope.
A closure is a function object that can access variables in its lexical scope even if the function object is called outside its original scope.
You may still have a hard time grasping what this passage is really about. If I summarize a Closure briefly, it is really just a stateful function.
Function state
Stateless function
Usually when we execute a function, it’s stateless. You might say, what? State?? Let’s look at an example.
void main() {
printNumber(); / / 10
printNumber(); / / 10
}
void printNumber(){
int num = 0;
for(int i = 0; i < 10; i++){
num+ +; }print(num);
}
Copy the code
The code above is very predictable, it will print 10 twice, and when we call a function many times, it will still get the same output.
However, once we understand that Function is Object, how should we look at Function execution from an Object perspective?
Apparently printNumber (); We create a Function object, but we don’t assign it to any variable, next time a printNumber(); A new Function is actually created, and both objects execute the method body once, so you get the same output.
Stateful function
Stateless functions are easy to understand, so now we can look at stateful functions.
void main() {
var numberPrinter = (){
int num = 0;
return() {for(int i = 0; i < 10; i++){
num+ +; }print(num);
};
};
var printNumber = numberPrinter();
printNumber(); / / 10
printNumber(); / / 20
}
Copy the code
This code also executes printNumber() twice; Whereas we get a different output 10,20. It’s starting to smell like a state.
But it still seems a little hard to understand, so let’s look at it layer by layer.
var numberPrinter = (){
int num = 0;
/// execute function
};
Copy the code
First we define a Function object and hand it over to numberPrinter management. In the Function scoping that was created, a num variable was defined and assigned to 0.
Note: this method is not executed immediately, but only when numberPrinter() is called. So num doesn’t exist at this point.
return() {for(int i = 0; i < 10; i++){
num+ +; }print(num);
};
Copy the code
It then returns a Function. This Function takes num from its parent scope, increments it by 10, and prints the value of num.
var printNumber = numberPrinter();
Copy the code
We then create the Function object by calling numberPrinter(), which is a Closure! This object actually performs the numberPrinter we just defined, and defines an int num in its internal scope. And then it returns a method to printNumber.
In fact, the anonymous Function returned is another closure.
Then we do the first printNumber(), which will retrieve the num variable stored in the closure, and do the following.
// num: 0
for(int i = 0; i < 10; i++){
num+ +; }print(num);
Copy the code
At first, the scope of printNumber stored num as 0, so after 10 increments, num was 10. Finally, print printed 10.
For the second printNumber(), we used the same numberPrinter object. After the first printNumber(), num was already 10, so after the second printNumber(), num was 10, so the result of print was 20.
The printNumber serves as a closure throughout the call. It holds the state of the internal num, and as long as the printNumber is not reclaimed, no objects inside it will be removed by the GC.
So we also need to be aware that closures can cause memory leaks or create memory stress problems.
What exactly is a closure
Coming back, our definition of closures should make sense.
A closure is a function object that can access variables in its lexical scope even if the function object is called outside its original scope.
In the example above, our num is defined inside a numberPrinter, but we can access the variable externally by returning Function. Our printNumber keeps num all the time.
Look at closures in stages
When we use closures, I think of it as three phases.
Definition phase
At this stage, we defined Function as a closure, but we didn’t actually implement it.
void main() {
var numberPrinter = (){
int num = 0;
return() {print(num);
};
};
Copy the code
At this point, since we only defined the closure and did not execute it, the num object does not exist.
Create a stage
var printNumber = numberPrinter();
Copy the code
At this point, we actually execute the contents of the Nu mberPrinter closure and return the result, and num is created. In this case, num will remain as long as printNumber is not GC.
Access stage
printNumber();
printNumber();
Copy the code
We can then somehow access the contents of the numberPrinter closure. (In this case, num is accessed indirectly)
The above three stages are only easy to understand, not rigorous description.
The application of Closure
If we just understand the concept, we might forget it. How can Closure be used?
Execute the method at the location of the passed object
For example, we have some problems with the content of a Text Widget and show us an Error Widget. At this point, I want to print this out to see what’s going on, you can do that.
Text((){
print(data);
returndata; } ())Copy the code
Isn’t it amazing that there is such an operation?
Tip executes the closure contents immediately: here we execute the closure contents immediately using the closure syntax (){}() and return our data.
Even though Text only allows us to pass a String, I can still do print.
As another case, if we want to execute certain statements only in Debug mode, we can also use closure with assertions to do so, as described in this article.
Implementation policy Pattern
Closure makes it easy to implement the policy pattern.
void main(){
var res = exec(select('sum'),1 ,2);
print(res);
}
Function select(String opType){
if(opType == 'sum') return sum;
if(opType == 'sub') return sub;
return (a, b) => 0;
}
int exec(NumberOp op, int a, int b){
return op(a,b);
}
int sum(int a, int b) => a + b;
int sub(int a, int b) => a - b;
typedef NumberOp = Function (int a, int b);
Copy the code
With the SELECT method, we can dynamically select the specific method we want to execute. You can run this code here.
Implement Builder mode/lazy loading
If you have any experience with Flutter, you should have used ListView.builder. We pass only one method to the Builder property, from which the ListView can build each of its items. In fact, this is a closure.
ListView.builder({
/ /...
@required IndexedWidgetBuilder itemBuilder,
/ /...
})
typedef IndexedWidgetBuilder = Widget Function(BuildContext context, int index);
Copy the code
A Flutter defines a Function via a typedef that takes BuildContext and int as arguments and returns a Widget. For such a Function we define it as IndexedWidgetBuilder and return the widgets inside it. This enables external scopes to access widgets defined within the Scope of IndexedWidgetBuilder, thereby implementing the Builder pattern.
Lazy loading of listViews is also an important feature of closures
The wheel test
Now that you’ve learned about closure, let’s put it to the test to see if you really understand it
main(){
var counter = Counter(0);
fun1(){
var innerCounter = counter;
Counter incrementCounter(){
print(innerCounter.value);
innerCounter.increment();
return innerCounter;
}
return incrementCounter;
}
var myFun = fun1();
print(myFun() == counter);
print(myFun() == counter);
}
class Counter{
int value;
Counter(int value)
: this.value = value; increment(){ value++; }}Copy the code
What does this code output?
If you already have an answer, check it out! You are welcome to join us in the comments section below
Write in the last
Thanks to @Realank Liu for his Review and valuable suggestions
Closure plays an important role in implementing many of Flutter’s functions. It can be said that it has been embedded in your programming routine to help us better write Dart code. As a Dart developer who is constantly improving, It’s time to put it to use. In future articles, I’ll move on to Dart with more in-depth coverage. Stay tuned!
If you have any questions or suggestions about this article, please feel free to contact me in the comments section below or at [email protected], and I will reply as soon as possible!
My subsequent blog will be the first xinlei.dev, welcome to follow!