★Dart-4-函数与闭包(closure)

Posted itzyjr

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了★Dart-4-函数与闭包(closure)相关的知识,希望对你有一定的参考价值。


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关键字):

dynamic mix(item1, item2) { ...snip...; return ingredient;}

第三行代码明确表示没有返回值。
第四行代码没有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,它只是函数对象的引用,而并非是函数类型

这个概念提出了一个有趣的可能性。如果可以将函数存储在变量中,是否需要首先在顶级作用域中声明函数?不,您可以内联声明函数(在另一个函数体中)并将其存储在变量中,而不是在顶级库作用域中声明函数。事实上,有三种方法可以内联声明函数,一种方法是在顶级库范围内声明函数,如下图所示。

您已经使用了顶级库作用域来声明函数,例如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)。

➌命名函数赋值声明
第三种声明函数的方法是前两个版本的混合,即声明命名函数并立即将该函数分配给变量。如下图所示:

这种方法的优点是,您可以声明返回类型,并且您有一个位于函数范围内的函数名,如果需要,允许递归。在本例中,函数作用域中的函数名为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的类型系统呢?您如何知道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提供了两种方法来实现这一点。第一个更轻,但更详细:提供函数签名作为函数参数定义,如下图所示。

这种方法是一种冗长的方式,用于声明函数参数必须具有特定的签名。想象一下,如果有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;
	}
}


函数来混合配料,但如下面清单所示,铲子(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-4-函数与闭包(closure)的主要内容,如果未能解决你的问题,请参考以下文章

Groovy闭包 Closure ( 闭包作为函数参数 | 代码示例 )

Groovy闭包 Closure ( 闭包作为函数参数 | 代码示例 )

Swift之深入解析闭包Closures的使用和捕获变量的原理

Swift:闭包(Closures)

Groovy闭包 Closure ( 闭包参数绑定 | curry 函数 | rcurry 函数 | ncurry 函数 | 代码示例 )

PHP Closure(闭包)类详解