您的位置:首页 > 其它

闭包,lambda表达式之学习笔记

2012-08-16 11:35 381 查看
什么是闭包?
闭包并不是什么新奇的概念,它早在高级语言开始发展的年代就产生了。闭包(Closure)是词法闭包(Lexical Closure)的简称。对闭包的具体定义有很多种说法,这些说法大体可以分为两类:

一种说法认为闭包是符合一定条件的函数,闭包是在其词法上下文中引用了自由变量(自由变量是指除局部变量以外的变量)的函数。

另一种说法认为闭包是由函数和与其相关的引用环境组合而成的实体。在实现深约束(英文原词是 binding,也有人把它翻译为绑定)时,需要创建一个能显式表示引用环境的东西,并将它与相关的子程序捆绑在一起,这样捆绑起来的整体被称为闭包。

这两种定义在某种意义上是对立的,一个认为闭包是函数,另一个认为闭包是函数和引用环境组成的整体。虽然有些咬文嚼字,但可以肯定第二种说法更确切。闭包只是在形式和表现上像函数,但实际上不是函数。函数是一些可执行的代码,这些代码在函数被定义后就确定了,不会在执行时发生变化,所以一个函数只有一个实例。闭包在运行时可以有多个实例,不同的引用环境和相同的函数组合可以产生不同的实例。所谓引用环境是指在程序执行中的某个点所有处于活跃状态的约束所组成的集合。其中的约束是指一个变量的名字和其所代表的对象之间的联系。那么为什么要把引用环境与函数组合起来呢?这主要是因为在支持嵌套作用域的语言中,有时不能简单直接地确定函数的引用环境。这样的语言一般具有这样的特性:

函数是一阶值(First-class value),即函数可以作为另一个函数的返回值或参数,还可以作为一个变量的值。

函数可以嵌套定义,即在一个函数内部可以定义另一个函数。

这些概念上的解释很难理解,显然一个实际的例子更能说明问题。Lua 语言的语法比较接近伪代码,我们来看一段 Lua 的代码:

清单 1. 闭包示例1

function make_counter()
local count = 0

function inc_count()
count = count + 1
return count
end

return inc_count
end

c1 = make_counter()
c2 = make_counter()

print(c1())
print(c2())


在这段程序中,函数 inc_count 定义在函数 make_counter 内部,并作为 make_counter 的返回值。变量 count 不是 inc_count 内的局部变量,按照最内嵌套作用域的规则,inc_count 中的 count 引用的是外层函数中的局部变量 count。接下来的代码中两次调用 make_counter() ,并把返回值分别赋值给 c1 和 c2 ,然后又依次打印调用 c1 和 c2 所得到的返回值。

这里存在一个问题,当调用 make_counter 时,在其执行上下文中生成了局部变量 count 的实例,所以函数 inc_count 中的 count 引用的就是这个实例。但是 inc_count 并没有在此时被执行,而是作为返回值返回。当 make_counter 返回后,其执行上下文将失效,count 实例的生命周期也就结束了,在后面对 c1 和 c2 调用实际是对 inc_count 的调用,而此处并不在 count 的作用域中,这看起来是无法正确执行的。

上面的例子说明了把函数作为返回值时需要面对的问题。当把函数作为参数时,也存在相似的问题。下面的例子演示了把函数作为参数的情况。

清单 2. 闭包示例2

function do10times(fn)
for i = 0,9 do
fn(i)
end
end

sum = 0
function addsum(i)
sum = sum + i
end

do10times(addsum)
print(sum)


这里我们看到,函数 addsum 被传递给函数 do10times,被并在 do10times 中被调用10次。不难看出 addsum 实际的执行点在 do10times 内部,它要访问非局部变量 sum,而 do10times 并不在 sum 的作用域内。这看起来也是无法正常执行的。

这两种情况所面临的问题实质是相同的。在这样的语言中,如果按照作用域规则在执行时确定一个函数的引用环境,那么这个引用环境可能和函数定义时不同。要想使这两段程序正常执行,一个简单的办法是在函数定义时捕获当时的引用环境,并与函数代码组合成一个整体。当把这个整体当作函数调用时,先把其中的引用环境覆盖到当前的引用环境上,然后执行具体代码,并在调用结束后恢复原来的引用环境。这样就保证了函数定义和执行时的引用环境是相同的。这种由引用环境与函数代码组成的实体就是闭包。当然如果编译器或解释器能够确定一个函数在定义和运行时的引用环境是相同的(一个函数中没有自由变量时,引用环境不会发生变化),那就没有必要把引用环境和代码组合起来了,这时只需要传递普通的函数就可以了。现在可以得出这样的结论:闭包不是函数,只是行为和函数相似,不是所有被传递的函数都需要转化为闭包,只有引用环境可能发生变化的函数才需要这样做。

再次观察上面两个例子会发现,代码中并没有通过名字来调用函数 inc_count 和 addsum,所以他们根本不需要名字。以第一段代码为例,它可以重写成下面这样:

清单 3. 闭包示例3

function make_counter()
local count = 0
return function()
count = count + 1
return count
end
end

c1 = make_counter()
c2 = make_counter()

print(c1())
print(c2())


这里使用了匿名函数。使用匿名函数能使代码得到简化,同时我们也不必挖空心思地去给一个不需要名字的函数取名字了。

上面简单地介绍了闭包的原理,更多的闭包相关的概念和理论请参考参考资源中的"名字,作用域和约束"一章。

一个编程语言需要哪些特性来支持闭包呢,下面列出一些比较重要的条件:

函数是一阶值;
函数可以嵌套定义;
可以捕获引用环境,并
把引用环境和函数代码组成一个可调用的实体;
允许定义匿名函数;

这些条件并不是必要的,但具备这些条件能说明一个编程语言对闭包的支持较为完善。另外需要注意,有些语言使用与函数定义不同的语法来定义这种能被传递的"函数",如 Ruby 中的 Block。这实际上是语法糖,只是为了更容易定义匿名函数而已,本质上没有区别。

借用一个非常好的说法来做个总结(出自 Python 社区):对象是附有行为的数据,而闭包是附有数据的行为。

闭包的应用

闭包可以用优雅的方式来处理一些棘手的问题,有些程序员声称没有闭包简直就活不下去了。这虽然有些夸张,却从侧面说明闭包有着强大的功能。下面列举了一些闭包应用。

加强模块化

闭包有益于模块化编程,它能以简单的方式开发较小的模块,从而提高开发速度和程序的可复用性。和没有使用闭包的程序相比,使用闭包可将模块划分得更小。比如我们要计算一个数组中所有数字的和,这只需要循环遍历数组,把遍历到的数字加起来就行了。如果现在要计算所有元素的积呢?要打印所有的元素呢?解决这些问题都要对数组进行遍历,如果是在不支持闭包的语言中,我们不得不一次又一次重复地写循环语句。而这在支持闭包的语言中是不必要的,比如对数组求和的操作在 Ruby 中可以这样做:

清单 4. 加强模块化

nums = [10,3,22,34,17]
sum = 0
nums.each{|n| sum += n}
print sum


这种处理方法多少有点像我们熟悉的回调函数,不过要比回调函数写法更简单,功能更强大。因为在闭包里引用环境是函数定义时的环境,所以在闭包里改变引用环境中变量的值,直接就可以反映到它定义时的上下文中,这是通常的回调函数所不能做到的。这个例子说明闭包可以使我们把模块划分得更小。

抽象

闭包是数据和行为的组合,这使得闭包具有较好抽象能力,下面的代码通过闭包来模拟面向对象编程。函数 make_stack 用来生成 stack 对象,它的返回值是一个闭包,这个闭包作为一个 Dispatcher,当以 “push” 或 “pop” 为参数调用时,返回一个与函数 push 或 pop 相关联的闭包,进而可以操作 data 中的数据。

清单 5. 抽象

function make_stack()

local data = {};
local last = -1;

local function push(e)
last = last + 1;
data[last] = e;
end

local function pop()
if last == -1 then
return nil
end
last = last - 1
return data[last+1]
end

return function (index)
local tb = {push=push, pop=pop}
return tb[index]
end
end

s = make_stack()

s("push")("test0")
s("push")("test1")
s("push")("test2")
s("push")("test3")

print(s("pop")())
print(s("pop")())
print(s("pop")())


如果加入一些方便调用“对象方法”的语法糖,这看起来很像是面向对象的语法。当然 Lua 中有自己的面向对象语法和机制,所以几乎看不到有人写这样的 Lua 代码,但是对于 Scheme 等没有内建面向对象支持也没有内建复杂数据抽象机制的语言,使用闭包来进行抽象是非常重要的手段。

简化代码

我们来考虑一个常见的问题。在一个窗口上有一个按钮控件,当点击按钮时会产生事件,如果我们选择在按钮中处理这个事件,那就必须在按钮控件中保存处理这个事件时需要的各个对象的引用。另一种选择是把这个事件转发给父窗口,由父窗口来处理这个事件,或是使用监听者模式。无论哪种方式,编写代码都不太方便,甚至要借助一些工具来帮助生成事件处理的代码框架。用闭包来处理这个问题则比较方便,可以在生成按钮控件的同时就写下事件处理代码。比如在 Ruby 中可以这样写:

清单 6. 简化代码

song = Song.new
start_button = MyButton.new("Start") { song.play }
stop_button = MyButton.new("Stop") { song.stop }


更多

闭包的应用远不止这些,这里列举的只能算是冰山一角而已,并且更多的用法还不断发现中。要想了解更多的用法,多看一些代码应该是个不错的选择。

总结

闭包能优雅地解决很多问题,很多主流语言也顺应潮流,已经或将要引入闭包支持。相信闭包会成为更多人爱不释手的工具。闭包起源于函数语言,也许掌握一门函数语言是理解闭包的最佳途径,而且通过学习函数语言可以了解不同的编程思想,有益于写出更好的程序。

摘自:http://www.ibm.com/developerworks/cn/linux/l-cn-closure/

------------------------------------------------------

lambda表达式:(c++)

lambda 函数

如果要给 C++0x 的新特性评奖的话,得头奖的应该是 lambda 函数。lambda 函数 是匿名的函数,这意味着不必定义典型的 C/C++ 函数也能够完成工作。lambda 函数最常用的地方可能是 STL sort。到目前为止,要想使用定制的比较函数,标准的做法是定义自己的函数对象,然后适当地定义操作符 ( )。请考虑一个包含 5 个字符串的向量;希望按字符串长度的升序排序。清单 1 给出过去的做法。


清单 1 过去的排序方法

#include <iostream>
#include <string>
#include <vector>
#include <algorithm>
using namespace std;

struct compare {
bool operator()(const string& s1, const string& s2) {
return s1.size() < s2.size();
}
};

int main()
{
vector<string> vs = {"This", "is", "a", "C++0x", "exercise"};
std::sort(vs.begin(), vs.end(), compare());

for (auto ivs = vs.begin(); ivs != vs.end(); ++ivs)
cout << *ivs << endl;
return 0;
}


清单 2 给出使用 lambda 函数的新方法。

#include <iostream>
#include <string>
#include <vector>
#include <algorithm>
using namespace std;

int main()
{
vector<string> vs = {"This", "is", "a", "C++0x", "exercise"};
std::sort(vs.begin(), vs.end(),
[ ](const string& s1, const string& s2) {
return s1.size() < s2.size();
}
)

for (auto ivs = vs.begin(); ivs != vs.end(); ++ivs)
cout << *ivs << endl;
return 0;
}


清单 2. 定义用于定制排序的 lambda 函数

这两个程序(清单 1 和 清单 2)的输出都是 a is This C++0x exercise。清单 3 说明如何定义 lambda 函数。它看起来与定义常规函数的代码很相似,除了 [ ] 部分,对吗?对也不对。在深入讨论之前您必须问几个问题:

lambda 函数的返回类型怎么处理?

这些函数可以访问在它的范围之外定义的变量吗?例如,清单 2 中的 lambda 函数是否可以访问 vs?

不需要为 lambda 函数提供返回类型;根据返回语句推导出返回类型。在 清单 2 中,lambda 函数的返回类型是 Boolean。对于第二个问题,可以通过 清单 3 中的代码来解答。

清单 3. 在 lambda 函数内访问范围外的变量

int main()
{
vector<string> vs = {"This", "is", "a", "C++0x", "exercise"};
std::sort(vs.begin(), vs.end(),
[ ](const string& s1, const string& s2) {
cout << vs.size( ) << endl;
return s1.size() < s2.size();
}
)
…
}


每当 sort 调用这个 lambda 函数时,试图输出向量的大小。下面是编译时 g++ 报告的消息:

1.cpp: In lambda function:

1.cpp:12:9: error: 'vs' is not captured

“纠正” 这个问题有两种方法 — 实际上是四种方法。可以使用前面的方括号 ([]) 把参数传递给 lambda 函数。如果选择通过引用传递变量,就在它前面加上 & 并把它放在 [ ] 内;对于通过值传递的变量,在它前面加上等号 (=)。清单 4 通过引用传递 vs。

清单 4. 通过引用把变量传递给 lambda 函数

int main()
{
vector<string> vs = {"This", "is", "a", "C++0x", "exercise"};
std::sort(vs.begin(), vs.end(),
[ &vs ](const string& s1, const string& s2) {
cout << vs.size( ) << endl;
return s1.size() < s2.size();
}
)
…
}


现在,编译器会顺利地完成编译。(请用 [ =vs ] 试试。)那么,把向量传递给 lambda 函数的第三种方法是什么?使用 [ & ]。这种语法表示把所有局部变量通过引用传递给 lambda 函数。从技术上说,还可以使用 [ = ] 把所有局部变量通过值传递给函数,这是第四种方法。但是,由于性能的原因,不推荐这种方法。

摘自:http://www.ibm.com/developerworks/cn/aix/library/au-gcc/index.html

------------------------------------------------------

lambda表达式:(Java)

功能接口

只包含一个方法的接口被称为功能接口,Lambda 表达式用用于任何功能接口适用的地方。

java.awt.event.ActionListener 就是一个功能接口,因为它只有一个方法:void actionPerformed(ActionEvent). 在 Java 7 中我们会编写如下代码:

button.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
ui.dazzle(e.getModifiers());
}
});


而 Java 8 中可以简化为:

button.addActionListener(e -> { ui.dazzle(e.getModifiers()); });


编译器知道lambda 表达式必须符合 void actionPerformed(ActionEvent) 方法的定义。看起来 lambda 实体返回 void,实际上它可以推断出参数 e 的类型是 java.awt.event.ActionEvent.

函数集合

Java 8 的类库包含一个新的包 java.util.functions ,这个包中有很多新的功能接口,这些接口可与集合 API 一起使用。

java.util.functions.Predicate

使用谓词 (Predicate) 来筛选集合:

List<String> names = Arrays.asList("Alice", "Bob", "Charlie", "Dave");
List<String> filteredNames = names
.filter(e -> e.length() >= 4)
.into(new ArrayList<String>());
for (String name : filteredNames) {
System.out.println(name);
}


这里我们有两个新方法:

Iterable<T> filter(Predicate<? super T>) 用于获取元素满足某个谓词返回 true 的结果
<A extends Fillable<? super T>> A into(A) 将用返回的结果填充 ArrayList

java.util.functions.Block

我们可使用一个新的迭代器方法来替换 for 循环 void forEach(Block<? super T>):

List<String> names = Arrays.asList("Alice", "Bob", "Charlie", "Dave");
names
.filter(e -> e.length() >= 4)
.forEach(e -> { System.out.println(e); });


forEach() 方法是 internal iteration 的一个实例:迭代过程在 Iterable 和 Block 内部进行,每次可访问一个元素。

最后的结果就是用更少的代码来处理集合:

List<String> names = Arrays.asList("Alice", "Bob", "Charlie", "Dave");
names
.mapped(e -> { return e.length(); })
.asIterable() // returns an Iterable of BiValue elements
// an element's key is the person's name, its value is the string length
.filter(e -> e.getValue() >= 4)
.sorted((a, b) -> a.getValue() - b.getValue())
.forEach(e -> { System.out.println(e.getKey() + '\t' + e.getValue()); });


这样做的优点是:

元素在需要的时候才进行计算

如果我们取一个上千个元素的集合的前三条时,其他元素就不会被映射

鼓励使用方法链

我们无需才存储中间结果来构建新的集合

内部迭代过程因此大多数细节

例如,我们可以通过下面代码来并行 map() 操作

writing myCollection.parallel().map(e ‑> e.length()).

方法引用

我们可通过 :: 语法来引用某个方法。方法引用被认为是跟 lambda 表达式一样的,可用于功能接口所适用的地方。

我们可以引用一个静态方法:

executorService.submit(MethodReference::sayHello);

private static void sayHello() {
System.out.println("hello");
}


或者是一个实例的方法:

Arrays.asList("Alice", "Bob", "Charlie", "Dave").forEach(System.out::println);


我们也可以创建工程方法并将构造器引用赋值给 java.util.functions.Factory:

Factory<Biscuit> biscuitFactory = Biscuit::new;
Biscuit biscuit = biscuitFactory.make();


最后,我们创建一个引用到随意实例的例子:

interface Accessor<BEAN, PROPERTY> {
PROPERTY access(BEAN bean);
}

public static void main(String[] args) {
Address address = new Address("29 Acacia Road", "Tunbridge Wells");
Accessor<Address, String> accessor = Address::getCity;
System.out.println(accessor.access(address));
}


这里我们无需绑定方法引用到某个实例,我们直接将实例做为功能接口的参数进行传递。

默认方法

直到今天的 Java ,都不可能为一个接口添加方法而不会影响到已有的实现类。而 Java 8 允许你为接口自身指定一个默认的实现:

interface Queue {
Message read();
void delete(Message message);
void deleteAll() default {
Message message;
while ((message = read()) != null) {
delete(message);
}
}
}


子接口可以覆盖默认的方法:

interface BatchQueue extends Queue {
void setBatchSize(int batchSize);
void deleteAll() default {
setBatchSize(100);
Queue.super.deleteAll();
}
}


或者子接口也可以通过重新声明一个没有方法体的方法来删除默认的方法:

interface FastQueue extends Queue {

void deleteAll();
}


这个将强制所有实现了 FastQueue 的类必须实现 deleteAll() 方法。

摘自:http://www.oschina.net/question/12_59047
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: