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!