天天看點

★Dart-4-函數與閉包(closure)

目錄

          • 1.Dart函數
          • 2.使用一級(first-class)函數
          • 3. 閉包(Closures)
          • 總結

Dart,一切都是一個對象,包括函數,這意味着您可以将函數存儲在變量中,并以與傳遞String、int或任何其他對象相同的方式在應用程式中傳遞函數。這被稱為具有一級函數(first-class functions),因為它們被視為等同于其他類型,而不是語言中的二級公民(second-class citizens)。

最後,我們将研究閉包(closure),閉包是在建立函數并使用存在于其自身範圍之外的變量時發生的。當您将該函數(存儲在變量中)傳遞給應用程式的另一部分時,它被稱為閉包。這可能是一個複雜的話題;它在JavaScript中廣泛用于模拟諸如getter和setter之類的特性,以及已經内置到Dart語言中的類隐私特性。

1.Dart函數

按照如下這個配方制作出優質的通用混凝土。每個功能都有輸入和輸出:

  1. Measure水泥量(水泥體積)。
  2. Measure水泥體積兩倍的砂量。
  3. Measure三倍于水泥體積的礫石數量。
  4. Mix水泥和沙子,形成砂漿混合物。
  5. Mix砂漿混合物與礫石,形成幹燥的混凝土混合物。
  6. Mix水和混凝土混合物攪拌,形成濕混凝土。
  7. Lay混凝土,凝固前鋪設混凝土。

在這些步驟中,measure()和mix()函數被重用,接受上一步的輸入以生成新的輸出。當我混合兩種配料時,比如水泥和沙子,這就給了我一種新的配料(砂漿),我可以在配方的其他地方使用。還有一個lay()函數,我隻使用了一次。水泥起始量的初始體積取決于作業;例如,我使用一個袋子作為近似的度量機關。

您可以使用下面清單中的代碼在Dart中表示這些函數。清單中省略了函數傳回的各種成分類,但對于本例,它們是不必要的(您可以在與本書相關的源代碼中找到它們)。main()函數遵循混凝土攪拌指令集,它是所有Dart應用程式中執行的第一個函數。

ingredient 成分;cement 水泥;proportion 比例;concrete 混凝土

sand 沙子;gravel 礫石;mortar 砂漿;lay鋪設

Ingredient mix(Ingredient item1, Ingredient item2) {
	return item1 + item2;
}
Ingredient measureQty(Ingredient ingredient, int numberOfCementBags, int proportion) {
	return ingredient * (numberOfCementBags * proportion);
}
void lay(ConcreteMix concreteMix) {
	// snip – implementation not required
}
main() {
	Ingredient cement = new Cement();
	cement.bags = 2;
	print(cement.bags);
	Ingredient sand = measureQty(new Sand(), cement.bags, 2);
	Ingredient gravel = measureQty(new Gravel(), cement.bags, 3);
	Ingredient mortar = mix(cement, sand);
	Ingredient dryConcrete = mix(mortar, gravel);
	ConcreteMix wetConcrete = new ConcreteMix(dryConcrete, new Water());
	lay(wetConcrete);
}
           

Dart函數在聲明上類似于Java和C#,因為它們具有傳回類型、名稱和參數清單。與JavaScript不同,它們不需要關鍵字函數來聲明它們是函數;與Java和C#不同,參數類型和傳回類型都是可選的,作為Dart可選類型系統的一部分。

現在已經定義了一些示例函數,讓我們看看在Dart中定義這些函數的其他一些方法,考慮到Dart的可選類型以及長、速記函數文法。第3章簡要介紹了Dart的長手和速記函數文法:速記文法允許您編寫自動傳回單行輸出的單行函數。Dart的可選類型還意味着傳回類型和參數類型都是可選的。

您隻能對單行函數使用Dart的速記函數文法,而對單行或多行函數可以使用長記函數文法。簡寫文法對于編寫簡潔、清晰的代碼非常有用。

函數傳回類型和參數類型:

Ingredient mix(item1, item2) { ...snip...; return ingredient;}
mix(item1, item2) { ...snip...; return ingredient;}
void go(...){...snip...;}
go(...){...snip...;}
           

第一行代碼明确表示傳回一個Ingredient對象。

第二行代碼與下面代碼等效(傳回類型為

dynamic

關鍵字):

第三行代碼明确表示沒有傳回值。

第四行代碼沒有return語句,它等效于return null; 即,有傳回值null。

dynamic、var、object 三種類型的差別:

①dynamic:所有Dart對象的基礎類型,在大多數情況下,不直接使用它。通過它定義的變量會關閉類型檢查,這意味着 dynamix x= ‘hal’; x.foo();這段代碼靜态類型檢查不會報錯,但是運作時會crash,因為 x 并沒有foo()方法,是以建議大家在程式設計時不要直接使用dynamic;

②var: 是一個關鍵字,意思是"我不關心這裡的類型是什麼",系統會自動判斷運作時類型(runtimeType);

③object: 是Dart對象的基類,當你定義:object o =xxx ;時這時系統會認為 o 是個對象,你可以調用o的toString()和hashCode()方法,因為Object 提供了這些方法,但是如果你嘗試調用o.foo()時,靜态類型檢查會運作報錯。

綜上不難看出dynamic與object的最大的差別是在靜态類型檢查上。

measureQty(Ingredient ingredient,
	int numberOfCementBags,
	int proportion) {
	// ...snip...
}
calculateQty(ingredient,
	numberOfCementBags,
	proportion) {
	// ...snip...
}
calculateQty(dynamic ingredient,
	dynamic numberOfCementBags,
	dynamic proportion) {
	// ...snip...
} 
           

第一個函數,參數清單有明确的類型聲明。

第二個函數,參數沒有類型聲明,它等效于第三個函數。

按引用傳遞參數:

void main(List<String> arguments) {
  /* 示例1:傳值 */
  String str = "abc";
  tryChangeOriginStr(str);
  print("result1:" + str);

  /* 示例2:傳引用 */
  var box = Box();
  tryChangeOriginClz(box);
  print("result2:${box.x}");

  /* 示例3:不可變的——immutable */
  var a = ['Apple', 'Orange'];
  var b = a; // a,b引用同一個記憶體對象,但a,b之間無任何聯系!
  // 丢失原始引用并建立新引用
  a = ['Banana']; // a進行指派操作,對b沒有影響,a現在引用一個新對象,b還是引用原記憶體對象
  print("result3:a=$a,b=$b");

  /* 示例4:可變的——mutable */
  var m = ['Apple', 'Orange'];
  var n = m;
  m.clear();// 清空m引用的list,而n一直引用這個記憶體對象,即n中内容被清空
  m.add('Banana');// 由于此時m、n都引用同一list,是以變化對m和n是同步的
  print("result4:m=$m,n=$n");
}

void tryChangeOriginStr(String str) {// 傳值(by value)
  str += "XYZ";
}
void tryChangeOriginClz(Box box) {// 傳引用(by reference)
  box.x *= 2;
}

class Box {
  num x = 8;
}
           
result1:abc
result2:16
result3:a=[Banana],b=[Apple, Orange]
result4:m=[Banana],n=[Banana]
           

var a = 10;

這種指派文法的含義是a是指向一個記憶體對象10的引用(ref).

var a = 10; var b=a;

則a,b都指向同一個記憶體對象,即引用同一個記憶體對象。但是a,b之間沒有任何聯系。

關于mutable(可變的)和immutable(不變的)類型

mutable類型會減少資料的拷貝次數,進而其效率要高于immutable,但内部資料的不可變導緻其更加安全,可以用作多線程的共享對象而不必考慮同步問題。但可變類型由于其内部資料可變,是以其風險更大,由于内部資料不可變,是以對其頻發修改會産生大量的臨時拷貝,浪費空間。

Dart中不是所有的對象都是mutable,其中Number,String,Bool等都是immutable,每次修改都是産生一個新的對象。而其他大部分對象都是mutable。

Ingredient measureQty(ingredient, numberOfCementBags, proportion) {
	if (ingredient.bags == 0) {
		ingredient = new Ingredient();
		ingredient.bags = numberOfCementBags * proportion;
		return ingredient;
	}
}
main() {
	var emptyBagOfCement = new Cement();
	emptyBagOfCement.bags = 0;
	var cement = measureQty(emptyBagOfCement, 1, 1);// return new object'ref
	print(emptyBagOfCement.bags);// original object unmodified
}
           

你在傳回時丢失了對新類型的引用,因為一切都是對象;當你更改對傳入對象的引用時,你在函數中所做的一切就是丢失原始引用并建立新引用。函數外部的代碼仍然具有原始對象引用的句柄。

可選位置參數:

Dart函數可以擁有具有預設值的可選參數。建立函數時,可以指定調用代碼可以提供的參數;但如果調用代碼選擇不執行,則函數将使用預設值。

measureQty(ingredient, int numberOfCementBags, int proportion) {
	if (numberOfCementBags == null) numberOfCementBags = 1;
	if (proportion == null) proportion = 1;
	return ingredient * (numberOfCementBags * proportion);
}
main() {
	measureQty(new Sand(), null, null);
	measureQty(new Sand(), 1, null);
	measureQty(new Sand(), null, 1);
	measureQty(new Sand(), 1,1);
}
           

以上代碼,當numberOfCementBags為null時,指派1;當proportion為null也指派為1。但如果有預設值,可以簡化程式,也不用傳遞預設值參數。

如果調用代碼可以隻傳遞它需要傳遞的值,例如配料和比例,而不傳遞不需要的袋子數量,那就更好了。Dart允許我們通過可選參數實作這一點。定義所有位置參數後,可選參數必須同時出現在塊中。可選參數塊定義在一對方括号内,與位置參數一樣,是一個逗号分隔的清單。例如,您可以将示例函數更改為支援可選參數,如下所示:

measureQty(ingredient, [int numberOfCementBags, int proportion]) {
	// ... snip ...(null判斷)
}
// 可以像如下調用以上的函數
main() {
	measureQty(new Sand(), 2, 1);
	measureQty(new Sand(), 2);
	measureQty(new Sand());
}
           

當然,在這段代碼中,如果沒有提供參數值,參數值仍将初始化為

null

,這意味着measureQty()仍必須檢查null值并将其預設為1。幸運的是,您還可以在命名參數的函數聲明中提供預設值:

measureQty(ingredient, [int numberOfCementBags = 1, int proportion = 1]) {
	return ingredient * (numberOfCementBags * proportion);
}
           

可選命名參數:

可選位置參數的替代方法是使用可選的命名參數。這些允許調用代碼以任意順序指定将值傳遞到其中的參數。如前所述,強制參數排在第一位,但這次在大括号之間指定了可選參數,預設值如下表所示:

measureQty(ingredient, {int numberOfCementBags:1, int proportion:1}) {
	return ingredient * (numberOfCementBags * proportion);
}
// 可以像如下調用以上的函數
main() {
	measureQty(new Sand(), numberOfCementBags: 2, proportion: 1);
	measureQty(new Sand(), numberOfCementBags: 2);
	measureQty(new Sand(), proportion: 1);
	measureQty(new Sand());
	// 錯誤的調用如下:
	measureQty(new Sand(), 2, 1);// the optional values must be named
}
           

記住

■ 速記函數自動傳回由構成函數體的單行表達式建立的值。

■ 長柄函數應該使用return關鍵字傳回一個值;否則,将自動傳回null。

■ 您可以通過使用傳回類型void告訴類型檢查器您不打算傳回值。

■ 有關參數的類型資訊是可選的。

■ 在聲明強制參數後,可以将可選參數聲明為方括号内的逗号分隔清單。

■ 調用代碼可以使用name:value文法按名稱引用可選參數。

2.使用一級(first-class)函數

術語一級函數意味着您可以将函數存儲在變量中并在應用程式中傳遞。一級函數沒有特殊文法,Dart中的所有函數都是一級函數。要通路函數對象(而不是調用函數),請引用不帶括号的函數名,通常用于向函數提供參數。執行此操作時,您可以通路函數對象。

考慮本章前面Ingredient mix(item1, item2)函數,您可以在函數名後面加括号并傳遞函數參數的值來調用它,例如mix(sand, cement);。您也可以僅按名稱引用它,而不使用括号和參數;通過這種方式,您可以獲得對函數對象的引用,該引用可以像使用任何其他值一樣使用,例如String或int。

将函數對象存儲在變量中後,可以使用該新引用再次調用函數,如以下代碼段所示:

var mortar = mix(sand, cement);
var mixFunction = mix;
var dryConcrete = mixFunction(mortar, gravel);
print(mix is Object);// true,函數是個Object對象
print(mix is Function);// true,函數是Function類型
print(dryConcrete is Object);// true,一切皆對象
print(dryConcrete is Function);// false,它隻是函數對象的引用,而并非是函數類型
           

這個概念提出了一個有趣的可能性。如果可以将函數存儲在變量中,是否需要首先在頂級作用域中聲明函數?不,您可以内聯聲明函數(在另一個函數體中)并将其存儲在變量中,而不是在頂級庫作用域中聲明函數。事實上,有三種方法可以内聯聲明函數,一種方法是在頂級庫範圍内聲明函數,如下圖所示。

★Dart-4-函數與閉包(closure)

您已經使用了頂級庫作用域來聲明函數,例如mix1(),它被稱為庫函數(library function)。其他三個函數聲明都在另一個方法的主體内,稱為局部函數(local function),對它需要有更多的解釋。它們是Dart的一部分,看似簡單,但與閉包一樣,可能很複雜。

局部函數聲明:

Ingredient combineIngredients(mixFunc, item1, item2) {// 第一個參數是個函數名(即函數對象的引用)
	return mixFunc(item1, item2);// 通過函數名調用函數
}
           

既然您已經使用了存儲在變量中的函數對象,那麼讓我們看看聲明局部函數的三種方法,從最基本的方法開始:簡單的局部函數聲明。

➊簡單的局部函數聲明

mix2(item1, item2) {
	return item1 + item2;
}
           

當您在另一個函數的作用域中聲明一個簡單的局部函數時,不需要提供終止分号,因為右大括号提供了終止符,這與在頂級作用域中聲明一個函數是一樣的。這一點很重要,因為另外兩種将函數顯式配置設定給變量的聲明方法确實需要在右大括号後面加一個分号。

一個遞歸示例:

stir(ingredient, stirCount) {
	print("Stirring $ingredient")
	if (stirCount < 10) {// 如果stirCount小于10
		stirCount ++;// stirCount自增
		stir(ingredient, stirCount);// 再次調用stir()(遞歸)
	}
}
           

➋匿名函數聲明

var mix3 = (item1, item2) {
	return item1 + item2;
};// 就像聲明一個變量一樣,最後的分号一定要加上
           

聲明匿名函數時沒有函數名。與任何其他函數聲明一樣,您可以将其配置設定給函數對象變量,将其直接傳遞給另一個函數,或将其用作聲明函數的傳回類型。但是您不能遞歸地使用它,因為函數在自己的作用域中沒有自己的名稱。

() => null;

這是一個有效的匿名函數,不接受任何參數并傳回空值。

示例:将匿名函數存入一個list中

main() {
	List taskList = new List();
	taskList.add( (item) => item.pour() );
	taskList.add( (item) {
		item.level();
		item.store();
	} );
	var aggregate = new Aggregate();
	foreach(task in taskList) {
		task(aggregate);
	}
}
           

可以直接将一個完整的匿名函數作為調用函數的參數:

combineIngredients( (item1, item2) => item1 + item2, sand, gravel);

下面的代碼中函數名叫Ingredient,而不是一個匿名函數:

Ingredient(item) => item.openBag();

不提供傳回類型,因為它認為函數名為Ingredient。

這個問題可以通過聲明本地函數的第三種也是最後一種方法來解決:函數指派(function assignment)。

➌命名函數指派聲明

第三種聲明函數的方法是前兩個版本的混合,即聲明命名函數并立即将該函數配置設定給變量。如下圖所示:

★Dart-4-函數與閉包(closure)

這種方法的優點是,您可以聲明傳回類型,并且您有一個位于函數範圍内的函數名,如果需要,允許遞歸。在本例中,函數作用域中的函數名為mixer(),此名稱僅在函數作用域中可用。要在别處引用該函數,必須使用名稱mix4。

如果将mix4()函數作為參數傳遞給combineIncrements()函數,則可以重寫該函數以使用遞歸并提供類型資訊,如下所示。

main() {
	var mix4 = Ingredient mixer(Ingredient item1, Ingredient item2) {
		if (item1 is Sand) {
			return mixer(item2, item1);
		} else (
			return item1 + item2;
		)
	}
	var sand = new Sand();
	var gravel = new Gravel();
	combineIngredients(mix4, sand, gravel);
}
           

名稱mixer()本質上是一次性的。它僅在函數範圍内可用,在其他地方無效。當您将mixer()函數作為參數直接聲明到另一個函數中時,除了它本身之外,不能在任何地方引用mixer()。

該示例看起來與我們首先看到的簡單局部函數聲明幾乎相同,但由于函數被隐式配置設定給CombineIngCredits()函數的參數,是以略有不同,如下圖所示。

★Dart-4-函數與閉包(closure)

我們已經研究了聲明函數并将它們配置設定給變量和函數參數,但是Dart的類型系統呢?您如何知道combineIncrements()函數将另一個函數作為其第一個參數?幸運的是,Dart允許強大的函數類型,并提供了一個新的關鍵字

typedef

定義強函數類型:

您已經看到一個函數 “is-an”

Object

,一個函數 “is-a”

Function

,是以你可以使用這些類型,如下面的清單所示。

// mixFunc參數是一個Function的強類型
Ingredient combineIngredients(Function mixFunc, item1, item2) {
	return mixFunc(item1, item2);
}
main() {
	// 将函數存儲于一個叫mix的Function強類型中
	Function mix = (item1, item2) {
		return item1 + item2;
	}
	var sand = new Sand();
	var gravel = new Gravel();
	// 将傳遞mix到調用函數中時,類型檢查器會驗證你提供的第一個參數是否是個函數
	combineIngredients(mix, sand, gravel);
}
           

當您使用存儲在變量中的函數對象時,您使用的是函數類的執行個體。然而,并非所有函數執行個體都是相同的。mix()函數不同于measureQty()函數,後者不同于lay()函數。

您需要一種在combineIngredients()上強鍵入mix()函數參數的方法,以指定它需要一個mix()函數,而不是其他函數。

Dart提供了兩種方法來實作這一點。第一個更輕,但更詳細:提供函數簽名作為函數參數定義,如下圖所示。

★Dart-4-函數與閉包(closure)

這種方法是一種冗長的方式,用于聲明函數參數必須具有特定的簽名。想象一下,如果有10個函數都接受一個mix()函數;

你需要寫10次函數。幸運的是,Dart允許您使用

typedef

關鍵字聲明函數簽名,進而建立自定義函數類型。typedef聲明您正在定義函數簽名,而不是函數或函數對象。隻能在庫的頂級作用域中使用typedef,而不能在其他函數中使用。下表顯示了如何使用typedef定義函數簽名,該簽名可用于替換combineIngIngredients()參數清單上的mixFunc參數聲明。

typedef Ingredient MixFunc(Ingredient, Ingredient);
Ingredient combineIngredients(MixFunc mixFunc, item1, item2) {
	return mixFunc(item1, item2);
}
           

記住

■ 按名稱使用函數時,如果沒有參數括号,則會得到對其函數對象的引用。

■ 以與頂級庫作用域函數類似的方式聲明的簡單本地函數能夠按名稱引用自己,并且可以充分利用參數和傳回類型資訊向工具提供類型資訊。

■ 匿名函數沒有名稱,不能使用遞歸或指定強傳回類型資訊,但它們确實為将函數添加到清單中或作為參數傳遞給其他函數提供了有用的速記。

■ 您可以使用命名函數代替匿名函數來允許遞歸和強傳回類型資訊,但其名稱僅在其自身範圍内可用。

■ 您可以使用typedef關鍵字聲明特定的函數簽名,以便類型檢查器可以驗證函數對象。

3. 閉包(Closures)

當函數對象引用在其自身直接作用域之外聲明的另一個變量時。閉包是一個強大的函數式程式設計概念。

閉包是使用函數的一種特殊方式。當在應用程式周圍傳遞函數對象時,開發人員通常在沒有意識到的情況下建立它們。閉包在JavaScript中被廣泛用于模拟基于類的語言中的各種構造,例如getter、setter和private屬性,方法是建立函數,其唯一目的是傳回另一個函數。但Dart在本地支援這些構造;是以,在編寫新代碼時,不太可能需要閉包。不過,大量代碼可能會從JavaScript移植到Dart,Dart的閉包支援類似于JavaScript,這将有助于這項工作。

當您聲明一個函數時,它不會立即執行;它作為函數對象存儲在變量中,與在變量中存儲字元串或int的方式相同。同樣,當您聲明函數時,還可以使用之前聲明的其他變量,如以下代碼段所示:

main() {
	var cement = new Cement();
	mix(item1, item2) {
		return cement + item1 + item2;
	}
}
           
★Dart-4-函數與閉包(closure)

函數來混合配料,但如下面清單所示,鏟子(shovel)上也有一些粘泥(sticky mud)。當getShovel()函數傳回時,mix()函數保留對stickyMud的引用,即使getShovel()函數已退出,stickyMud也會與配料混合。

getShovel() {
	var stickyMud = new Mud();
	var mix = (item1, item2) {
		return stickyMud + item1 + item2;
	}
	return mix;
}
main() {
	// 調用getShovel(),它傳回mix(),仍然包含對stickyMud的引用
	var mixFunc = getShovel();
	var sand = new Sand();
	var cement = new Cement();
	var muddyMortar = mixFunc(sand, cement);
}
           

記住

■ 使用未在其自身範圍内聲明的變量的函數有可能成為閉包。

■ 當一個函數通過傳遞到另一個函數或從聲明它的函數傳回而被傳遞到聲明它的作用域之外時,該函數就成為閉包。

總結

本章向您展示了如何使用速記文法和長寫文法聲明函數。使用速記文法時,它還隐式傳回構成速記函數體的單行表達式的值。但在使用長柄文法時,必須顯式使用return關鍵字傳回表達式的值。

如果未指定其他值,則所有函數都傳回null值,但您可以通過使用void傳回類型告訴Dart工具您不希望指定傳回值。

函數可以存儲在變量中,也可以通過不帶括号的名稱通路函數來引用函數。這種方法為您提供了一個包含函數對象的變量,您可以像其他變量一樣在應用程式中傳遞該變量。您可以傳回存儲在變量中的函數對象,也可以将其傳遞到另一個函數中,在該函數中可以像調用任何其他已聲明函數一樣調用它。函數對象與Function類共享“is-an”關系。

要強類型函數對象變量或參數,以便類型檢查器可以驗證代碼,請使用庫頂級作用域中的關鍵字typedef定義命名函數簽名。然後,您可以像使用任何其他類型一樣使用函數簽名的名稱。

我們還研究了閉包,閉包是在函數使用未在該函數中聲明的變量時建立的,并且該函數被傳遞到代碼的另一部分。您可以使用閉包來使用接收函數不應該或不可能知道的實作細節。

現在您已經了解了所有函數,在下一章中,我們将介紹Dart的庫和隐私機制。這些資訊很重要,因為在庫中使用的函數和類的名稱對隐私有很大影響。這兩個概念緊密相連,在開始研究Dart的類和接口之前,您需要了解這一主題。