Dart 進階 | 深刻理解 Function & Closure

前言

在最初設計 Dart 的時候,參考了 JavaScript 許多特性。不管是在異步處理,仍是在語法上,都能看到它的影子。熟悉 Dart 的同窗應該明白,在 Dart 中一切皆爲對象。不只 intbool 是經過 core library 提供的類建立出的對象,連函數也被看做是對象。(本文中可能會出現 函數 / 方法 兩者僅叫法不一樣)而本文將帶你深刻理解 Dart 的函數 (Function)&閉包(Closure)以及它們的用法。編程

什麼是 Closure(閉包)

若是你從未據說過閉包,不要緊,本節將會從零開始引入閉包這個概念。在正式介紹閉包以前,咱們須要先來了解一下 Lexical scopingmarkdown

詞法做用域 Lexical scoping

也許你對這個詞很陌生,可是它倒是最熟悉的陌生人。咱們先來看下面一段代碼。閉包

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

Function is Object

首先,要證實方法(函數)是一個對象這很簡單。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 中定義的變量的。

Function + 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 的定義

有了上面這些知識,咱們如今能夠來看看 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 的應用

若是僅是理解概念,那麼咱們看了可能也就忘了。來點實在的,到底 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 方法,能夠動態選擇咱們要執行的具體方法。你能夠在 這裏 運行這段代碼。

實現 Builder 模式 / 懶加載

若是你有 Flutter 經驗,那麼你應該使用過 ListView.builder,它很好用對不對。咱們只向 builder 屬性傳一個方法,ListView 就能夠根據這個 builder 來構建它的每個 item。實際上,這也是 closure 的一種體現。

ListView.builder({
//...
    @required IndexedWidgetBuilder itemBuilder,
//...
  })
  
typedef IndexedWidgetBuilder = Widget Function(BuildContext context, int index);
複製代碼

Flutter 經過 typedef 定義了一種 Function,它接收 BuildContextint 做爲參數,而後會返回一個 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,歡迎關注!

相關文章
相關標籤/搜索