在最初设计 Dart 的时候,参考了 JavaScript
许多特性。不管是在异步处理,仍是在语法上,都能看到它的影子。熟悉 Dart 的同窗应该明白,在 Dart 中一切皆为对象。不只 int
、bool
是经过 core library 提供的类建立出的对象,连函数也被看做是对象。(本文中可能会出现 函数 / 方法 两者仅叫法不一样)而本文将带你深刻理解 Dart 的函数 (Function)&闭包(Closure)以及它们的用法。编程
若是你从未据说过闭包,不要紧,本节将会从零开始引入闭包这个概念。在正式介绍闭包以前,咱们须要先来了解一下 Lexical scoping。markdown
也许你对这个词很陌生,可是它倒是最熟悉的陌生人。咱们先来看下面一段代码。闭包
void main() {
var a = 0;
var a = 1; // Error:The name 'a' is already defined
}
复制代码
你确定已经发现了,咱们在该段代码中犯了一个明显的错误。那就是定义了两次变量 a
,而编译器也会提示咱们,a 这个变量名已经被定义了。异步
这是因为,咱们的变量都有它的 词法做用域 ,在同一个词法做用域中仅容许存在一个名称为 a
的变量,且在编译期就可以提示语法错误。ide
这很好理解,若是一个 Lexical scoping 中存在两个同名变量 a
,那么咱们访问的时候从语法上就没法区分到底你是想要访问哪个 a
了。函数
上述代码中,咱们在
main
函数的词法做用域中定义了两次 a学习
仅需稍做修改ui
void main() {
var a = 1;
print(a); // => 1
}
var a = 0;
复制代码
咱们就可以正常打印出 a
的值为 1。 简单的解释,var a = 0;
是该 dart 文件的 Lexical scoping 中定义的变量,而 var a = 1;
是在 main 函数的 Lexical scoping 中定义的变量,两者不是一个空间,因此不会产生冲突。this
首先,要证实方法(函数)是一个对象这很简单。spa
print( (){} is Object ); // true
复制代码
(){}
为一个匿名函数,咱们能够看到输出为 true
。
知道了 Function is Object 还不够,咱们应该如何看待它呢。
void main() {
var name = 'Vadaski';
var printName = (){
print(name);
};
}
复制代码
能够很清楚的看到,咱们能够在 main
函数内定义了一个新的方法,并且还可以将这个方法赋值给一个变量 printName
。
可是若是你运行这段代码,你将看不到任何输出,这是为何呢。
实际上咱们在这里定义了 printName
以后,并无真正的去执行它。咱们知道,要执行一个方法,须要使用 XXX()
才能真正执行。
void main() {
var name = 'Vadaski';
var printName = (){
print(name);
};
printName(); // Vadaski
}
复制代码
上面这个例子很是常见,在 printName
内部访问到了外部定义的变量 name
。也就是说,一个 Lexical scoping 内部 是可以访问到 外部 Lexical scoping 中定义的变量的。
内部访问外部定义的变量是 ok 的,很容易就可以想到,外部是否能够访问内部定义的变量呢。
若是是正常访问的话,就像下面这样。
void main() {
var printName = (){
var name = 'Vadaski';
};
printName();
print(name); // Error:Undefined name 'name'
}
复制代码
这里出现了未定义该变量的错误警告,能够看出 printName
中定义的变量,对于 main
函数中的变量是不可见的。Dart 和 JavaScript 同样具备链式做用域,也就是说,子做用域能够访问父(甚至是祖先)做用域中的变量,而反过来不行。
从上面的例子咱们能够看出,Lexical scoping 其实是以链式存在的。一个 scope 中能够开一个新的 scope,而不一样 scope 中是能够容许重名变量的。那么咱们在某个 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();
}
复制代码
在上面这个例子中咱们能够看到,在 main 和 firstScope 中都定义了变量 a。咱们在 firstScope
中 print,输出了 2 in firstScope
而在 main 中 print 则会输出 1 in mainScope
。
咱们已经能够总结出规律了:近者优先。
若是你在某个 scope 中访问一个变量,它首先会看当前 scope 中是否已经定义该变量,若是已经定义,那么就使用该变量。若是当前 scope 没找到该变量,那么它就会在它的上一层 scope 中寻找,以此类推,直到最初的 scope。若是全部 scope 链上都不存在该变量,则会提示 Error:Undefined name 'name'
。
Tip: Dart scope 中的变量是静态肯定的,如何理解呢?
void main() { print(a); // Local variable 'a' can't be referenced before it is declared var a; } var a = 0; 复制代码
咱们能够看到,虽然在 main 的父 scope 中存在变量 a,且已经赋值,可是咱们在 main 的 scope 中也定义了变量 a。由于是静态肯定的,因此在 print 的时候会优先使用当前 scope 中定义的 a,而这时候 a 的定义在 print 以后,一样也会致使编译器错误:Local variable 'a' can't be referenced before it is declared。
有了上面这些知识,咱们如今能够来看看 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.
闭包 即一个函数对象,即便函数对象的调用在它原始做用域以外,依然可以访问在它词法做用域内的变量。
你可能对这段话仍是很难一下就理解到它到底在说什么。若是简要归纳 Closure 的话,它实际上就是有状态的函数。
一般咱们执行一个函数,它都是无状态的。你可能会产生疑问,啥?状态??咱们仍是看一个例子。
void main() {
printNumber(); // 10
printNumber(); // 10
}
void printNumber(){
int num = 0;
for(int i = 0; i < 10; i++){
num++;
}
print(num);
}
复制代码
上面的代码很好预测,它将会输出两次 10,咱们屡次调用一个函数的时候,它仍是会获得同样的输出。
可是,当咱们理解 Function is Object 以后,咱们应该如何从 Object 的角度来看待函数的执行呢。
显然 printNumber();
建立了一个 Function 对象,可是咱们没有将它赋值给任何变量,下次一个 printNumber();
实际上建立了一个新的 Function,两个对象都执行了一遍方法体,因此获得了相同的输出。
无状态函数很好理解,咱们如今能够来看看有状态的函数了。
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
}
复制代码
上面这段代码一样执行了两次 printNumber();
,然而咱们却获得了不一样的输出 10,20。好像有点 状态 的味道了呢。
但看上去彷佛仍是有些难以理解,让咱们一层一层来看。
var numberPrinter = (){
int num = 0;
/// execute function
};
复制代码
首先咱们定义了一个 Function 对象,而后把交给 numberPrinter
管理。在建立出来的这个 Function 的 Lexical scoping 中定义了一个 num 变量,并赋值为 0。
注意:这时候该方法并不会马上执行,而是等调用了
numberPrinter()
的时候才执行。因此这时候 num 是不存在的。
return (){
for(int i = 0; i < 10; i++){
num++;
}
print(num);
};
复制代码
而后返回了一个 Function。这个 Function 可以拿到其父级 scope 中的 num ,并让其增长 10,而后打印 num
的值。
var printNumber = numberPrinter();
复制代码
而后咱们经过调用 numberPrinter(),建立了该 Function 对象,这就是一个 Closure! 这个对象真正执行咱们刚才定义的 numberPrinter
,而且在它的内部的 scope 中就定义了一个 int 类型的 num
。而后返回了一个方法给 printNumber
。
实际上返回的 匿名 Function 又是另外一个闭包了。
而后咱们执行第一次 printNumber()
,这时候将会得到闭包储存的 num 变量,执行下面的内容。
// num: 0
for(int i = 0; i < 10; i++){
num++;
}
print(num);
复制代码
最开始 printNumber 的 scope 中储存的 num 为 0,因此通过 10 次自增,num 的值为 10,最后 print
打印了 10。
而第二次执行 printNumber()
咱们使用的仍是同一个 numberPrinter
对象,这个对象在第一次执行完毕后,其 num 已经为 10,因此第二次执行后,是从 10 开始自增,那么最后 print
的结果天然就是 20 了。
在整个调用过程当中,printNumber 做为一个 closure,它保存了内部 num 的状态,只要 printNumber 不被回收,那么其内部的全部对象都不会被 GC 掉。
因此咱们也须要注意到闭包可能会形成内存泄漏,或带来内存压力问题。
再回过头来理解一下,咱们对于闭包的定义就应该好理解了。
闭包 即一个函数对象,即便函数对象的调用在它原始做用域以外,依然可以访问在它词法做用域内的变量。
在刚才的例子中,咱们的 num 是在 numberPrinter
内部定义的,但是咱们能够经过返回的 Function 在外部访问到了这个变量。而咱们的 printNumber
则一直保存了 num
。
在咱们使用闭包的时候,我将它看为三个阶段。
这个阶段,咱们定义了 Function 做为闭包,可是却没有真正执行它。
void main() {
var numberPrinter = (){
int num = 0;
return (){
print(num);
};
};
复制代码
这时候,因为咱们只是定义了闭包,而没有执行,因此 num 对象是不存在的。
var printNumber = numberPrinter();
复制代码
这时候,咱们真正执行了 nu mberPrinter 闭包的内容,并返回执行结果,num 被建立出来。这时候,只要 printNumber 不被 GC,那么 num 也会一直存在。
printNumber();
printNumber();
复制代码
而后咱们能够经过某种方式访问 numberPrinter 闭包中的内容。(本例中间接访问了 num)
以上三个阶段仅方便理解,不是严谨描述。
若是仅是理解概念,那么咱们看了可能也就忘了。来点实在的,到底 Closure 能够怎么用?
好比说咱们有一个 Text Widget 的内容有些问题,直接给咱们 show 了一个 Error Widget。这时候,我想打印一下这个内容看看到底发生了啥,你能够这样作。
Text((){
print(data);
return data;
}())
复制代码
是否是很神奇,居然还有这种操做。
Tip 当即执行闭包内容:咱们这里经过闭包的语法
(){}()
马上执行闭包的内容,并把咱们的 data 返回。
虽然 Text 这里仅容许咱们传一个 String,可是我依然能够执行 print
方法。
另外一个 case 是,若是咱们想要仅在 debug 模式下执行某些语句,也能够经过 closure 配合断言来实现,具体能够看我这篇文章。
经过 closure 咱们能够很方便实现策略模式。
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);
复制代码
经过 select 方法,能够动态选择咱们要执行的具体方法。你能够在 这里 运行这段代码。
若是你有 Flutter 经验,那么你应该使用过 ListView.builder
,它很好用对不对。咱们只向 builder 属性传一个方法,ListView
就能够根据这个 builder
来构建它的每个 item。实际上,这也是 closure 的一种体现。
ListView.builder({
//...
@required IndexedWidgetBuilder itemBuilder,
//...
})
typedef IndexedWidgetBuilder = Widget Function(BuildContext context, int index);
复制代码
Flutter 经过 typedef 定义了一种 Function,它接收 BuildContext
和 int
做为参数,而后会返回一个 Widget。对这样的 Function 咱们将它定义为 IndexedWidgetBuilder
而后将它内部的 Widget 返回出来。这样外部的 scope 也可以访问 IndexedWidgetBuilder
的 scope 内部定义的 Widget,从而实现了 builder 模式。
一样,ListView 的懒加载(延迟执行)也是闭包很重要的一个特性哦~
在学习了 closure 之后,咱们来道题检验一下你是否真正理解了吧~
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++;
}
}
复制代码
上面这段代码会输出什么呢?
若是你已经想好了答案,就来看看是否正确吧!也欢迎你们在底下评论区一块儿讨论~
本文很是感谢彦博哥 @Realank Liu 的 Review 以及宝贵的建议~
时隔半年来迟迟的更新,不知道是否对你们有点帮助呢~ Closure 在实现 Flutter 的诸多功能上都发挥着重要的做用,能够说它已经深刻你编程的平常,默默帮助咱们更好地编写 Dart 代码,做为一名不断精进的 Dart 开发者,是时候用起来啦~以后的文章中,我会逐渐转向 Dart,给你们带来更深刻的内容,敬请期待!
若是您对本文还有任何疑问或者文章的建议,欢迎在下方评论区以及个人邮箱xinlei966@gmail.com 与我联系,我会及时回复!
后续个人博文将首发 xinlei.dev,欢迎关注!