匿名函数

匿名函数(英语:Anonymous Function)在计算机编程中是指一类无需定义标识符(函数名)的函数子程序,普遍存在于多种编程语言中。

1958年LISP首先采用匿名函数,自此之后,越来越多编程语言陆续采用,主流的编程语言如PHP[1]C++[2]也陆续采用。

用途

排序

尝试将类按名称排序:

a = [10, '10', 10.0]
a.sort(lambda x,y: cmp(x.__class__.__name__, y.__class__.__name__))
print a
[10.0, 10, '10']

上述 10.0 的类名是“float”,10 的类名是“int”而 '10' 的类名是“str”,排列后的顺序为“float”“int”,接着是“str”。

该示例中的匿名函数就是lambda表达式:

lambda x,y: cmp(...)

该匿名函数接受两个变量 xy ,通过内部函数 cmp() 返回两者的比较值,下面的例子将按长度为字符串列表排序:

a = ['three', 'two', 'four']
a.sort(lambda x,y: cmp(len(x), len(y)))
print a
['two', 'four', 'three']

语言列表

语言 支持 备注
ActionScript  
ALGOL 68  
C   在有clang和llvm的compiler-rt程序库的环境下支持
C#   从C# 3.0 (.Net Farmework 3.5)开始支持
C++   C++11开始支持,标准中称之为lambda表达式(lambda expression)[3][2]
Clojure  
Curl  
D  
Delphi   从Delphi 2009开始支持
Dylan  
Erlang  
F#  
Frink  
Go  
Haskell  
Java   Java 8开始支持[4]
JavaScript  
Kotlin  
Lisp  
Logtalk  
Lua  
Mathematica  
Matlab  
ML语言
(Objective Caml, Standard ML, etc.)
 
Octave  
Object Pascal   原生支持匿名函数,其正式名称为“匿名方法”(anonymous method)。Oxygene Object Pascal也支持匿名函数。
Objective-C (Mac OS X 10.6+)   称作“块”(block)
Pascal  
Perl  
PHP   从PHP 5.3.0开始支持真匿名函数,之前则只支持部分匿名函数
Python   Python用lambda语法定义匿名函数,只需用表达式而无需声明
R  
Ruby   Ruby的匿名函数源自Smalltalk,也同样叫“块”(block)。
Rust   Rust的匿名函数可以使用“闭包”(Closures)来实现。
Scala  
Scheme  
Smalltalk   Smalltalk的匿名函数称为“块”(block)
Tcl  
Visual Basic .NET v9  
Visual Prolog v 7.2  
Vala  

示例

Python

Python用lambda语法定义匿名函数,只需用表达式而无需声明

# 以下两种相等同
# 1.不使用匿名函数
def f(x):
	return x * x
# 2.使用匿名函数
lambda x: x * x

JavaScript

JavaScript支持匿名函数。

alert((function(x){
	return x*x;
})(10)); // 提示100

小书签也经常使用这种结构,例如下面的一个小书签就将当前网页的标题显示为其URL:

javascript:document.title=location.href;

然而,由于该赋值语句返回了一个值(即URL本身),很多浏览器会同时创建一个新的页面显示这个值。

取而代之,下面的匿名函数就可以做到不返回任何值:

javascript:(function(){document.title=location.href;})();

第一对圆括号中的函数(“(function(){document.title=location.href;})”)用作声明一个匿名函数,而最后的一对圆括号则用来执行这个函数。同等用法有:

javascript:var f = function(){document.title=location.href;}; f();

PHP

PHP 4.0.1之前不支持匿名函数[5]

4.0.1 至 5.3

PHP 4.0.1新增加了create_function函数,这是匿名函数的雏形。该函数能创建一个随机命名的新函数并以字符串形式返回新函数的函数名。

$foo = create_function('$x', 'return $x*$x;');
$bar = create_function("\$x", "return \$x*\$x;");
echo $foo(10);

要注意的是,新函数本身及其变量都要放在单引号里面,如果要放在双引号之内,美元符号“$”则需要转码成为“\$”。

5.3

PHP 5.3新增加了Closure类,以及能使类的实例可被调用的“魔术方法”__invoke()[6]。Lambda函数都是编译器的一种“花招”[7],它能产生新的能被调用的Closure实例,就像函数能被调用一样。

$x = 3;
$func = function($z) { return $z *= 2; };
echo $func($x); // 输出结果为6

上述例子中的$funcClosure类的一个实例,而echo $func()则相当于是$func->__invoke($z)。PHP 5.3模仿使用匿名函数,但并非支持真匿名函数,因为PHP的函数仍非第一类函数。

虽然PHP 5.3支持闭包,但还需要像这样明确标识其变量:

$x = 3;
$func = function() use(&$x) { $x *= 2; };
$func();
echo $x; // 输出结果为6

$func引用了变量$x(&$x),在调用的时候就会修改原来的$x,其结果在函数以外的地方也是可见的。

C++

C++ 98/03

C++ 98/03标准并不原生支持匿名函数。不过可以利用Boost库的Boost.Lambda来实现一个匿名函数[8]

C++ 11

C++11标准提供了匿名函数的支持,在《ISO/IEC 14882:2011》(C++11标准文档)中叫做lambda表达式[9]。一个lambda表达式有如下的形式:

[capture] (parameters) mutable exception attribute -> return_type { body }

必须用方括号括起来的capture列表来开始一个lambda表达式的定义。

lambda函数的形参表比普通函数的形参表多了3条限制:

  1. 参数不能有缺省值
  2. 不能有可变长参数列表
  3. 不能有无名参数

如果lambda函数没有形参且没有mutable、exception或attribute声明,那么参数的空圆括号可以省略。但如果需要给出mutable、exception或attribute声明,那么参数即使为空,圆括号也不能省略。

如果函数体只有一个return语句,或者返回值类型为void,那么返回值类型声明可以被省略:

[capture](parameters){body}

一个lambda函数的例子如下:

[](int x, int y) { return x + y; } // 從return語句中隱式獲得的返回值類型
[](int& x) { ++x; }   // 沒有return語句 -> lambda函數的返回值為void
[]() { ++global_x; }  // 沒有參數,僅僅是訪問一個全局變量
[]{ ++global_x; }     // 與前者相同,()可以被省略

在上面的第一个例子中这个无名函数的返回值是decltype(x+y)。如果lambda函数体的形式是return expression,或者什么也没返回,或者所有返回语句用decltype都能检测到同一类型,那么返回值类型可以被省略。

返回值类型可以显式指定,如下所示:

[](int x, int y) -> int { int z = x + y; return z; }

在这个例子中,一个临时变量,z,被创建来存储中间过程。与一般的函数一样,中间值在调用的前后并不存在。什么也没有返回的lambda表达式无需显式指定返回值,没有必要写-> void代码。

lambda函数可以捕获lambda函数外的具有automatic storage duration的变量,即函数的局部变量与函数形参变量。因而,具有static storage duration的变量不能被lambda捕获,只能直接使用,这包括静态局部变量。函数体与这些变量的集合合起来称做闭包。这些外部变量在声明lambda表达式时列在在方括号[]中。空的方括号表示没有外界变量被capture或者按照默认方式捕获外界变量。这些变量被传值捕获或者引用捕获。对于传值捕获的变量,默认为只读(这是由于lambda表达式生成的为一个函数对象,它的operator()成员缺省有const属性)。修改这些传值捕获变量将导致编译报错。但在lambda表达式的参数表的圆括号后面使用mutable关键字,就允许lambda函数体内的语句修改传值捕获变量,这些修改与lambda表达式(实际上是用函数对象实现)有相同的生命期,但不影响被传值捕获的外部变量的值。lambda函数可以直接使用具有static存储期的变量。如果在lambda函数的捕获列表中给出了static存储期的变量,编译时会给出警告,仍然按照lambda函数直接使用这些外部变量来处理。因此具有static存储期的变量即使被声明为传值捕获,修改该变量实际上直接修改了这些外部变量。编译器生成lambda函数对应的函数对象时,不会用函数对象的数据成员来保持被“捕获”的static存储期的变量。示例:

[]        // 沒有定義任何變量,但必须列出空的方括号。在Lambda表達式中嘗試使用任何外部變量都會導致編譯錯誤。
[x, &y]   // x是按值傳遞,y是按引用傳遞
[&]       // 任何被使用到的外部變量都按引用傳入。
[=]       // 任何被使用到的外部變量都按值傳入。
[&, x]    // x按值傳入。其它變量按引用傳入。
[=, &z]   // z按引用傳入。其它變量按值傳入。

下面这个例子展示了lambda表达式的使用:

std::vector<int> some_list{ 1, 2, 3, 4, 5 };
int total = 0;
std::for_each(begin(some_list), end(some_list), 
                 [&total](int x) {  total += x; }
              );

在类的非静态成员函数中定义的lambda表达式可以显式或隐式捕捉this指针,从而可以引用所在类对象的数据成员与函数成员。对象的this指针必需显式捕获声明。因此,被捕获的类的数据成员总是用this指针来访问,如果this所指的对象不存在,则this是空悬指针。解决办法是在定义lambda之前用局部变量复制一份数据成员的值,然后捕获这个局部变量的值。

lambda函数的函数体中,可以访问下述变量:

  • 函数参数
  • 局部声明的变量
  • 类数据成员:要求lambda表达式声明在类成员函数中,对象的this指针必需显式捕获声明。
  • 具有静态存储期的变量(如全局变量)。一般情况下,lambda是用来捕获局部变量的,如果用其来捕获全局变量或者静态变量,那么编译器会报warning
  • 被捕获的外部变量
    • 显式捕获的变量
    • 隐式捕获的变量,使用默认捕获模式(传值或引用)来访问。

lambda函数的数据类型是函数对象,保存时必须用std::function模板类型或auto关键字。 例如:

#include <functional>
#include <vector>
#include <iostream>

double eval(std::function <double(double)> f, double x = 2.0)
{
	return f(x);
}

int main()
{
	std::function<double(double)> f0    = [](double x){return 1;};
	auto                          f1    = [](double x){return x;};
	decltype(f0)                  fa[3] = {f0,f1,[](double x){return x*x;}};
	std::vector<decltype(f0)>     fv    = {f0,f1};
	fv.push_back                  ([](double x){return x*x;});
	for(int i=0;i<fv.size();i++)
		std::cout << fv[i](2.0) << std::endl;
	for(int i=0;i<3;i++)
		std::cout << fa[i](2.0) << std::endl;
	for(auto &f : fv)
		std::cout << f(2.0) << std::endl;
	for(auto &f : fa)
		std::cout << f(2.0) << std::endl;
	std::cout << eval(f0) << std::endl;
	std::cout << eval(f1) << std::endl;
	std::cout << eval([](double x){return x*x;}) << std::endl;
	return 0;
}

一个lambda函数的捕捉表达式为空,则可以用普通函数指针存储或调用。例如:

auto a_lambda_func = [](int x) { /*...*/ };
void (* func_ptr)(int) = a_lambda_func;
func_ptr(4); //calls the lambda.

C++14

C++11中捕获机制的局限

  1. lambda捕获的是局部变量或形参,不管是按值还是按引用捕获,这些都是具名的左值对象。而右值对象是匿名对象,无法被捕获。
  2. 按值捕获时,左值是被复制到闭包中的。如果被捕获的对象是个只移动类型的对象时,因其无法被复制,就会出错。
  3. 如果被捕获的对象如果是一个占用内存较大的对象时,按值捕获显然效率很低。

C++14增加了广义捕获(Generalized capture),或称“初始化捕获”。[10]即在捕获子句(capture clause)中增加并初始化新的变量,该变量不需要在lambda表达式所处的闭包域(enclosing scope)中存在;即使在闭包域中存在也会被新变量覆盖(override)。新变量类型由它的初始化表达式推导。也就是说可以创建新的变量并在捕获子句中对其进行初始化。这种方式称之为带有初始化程序的捕获或者广义lambda捕获。一个用途是可以从闭包域中捕获只供移动的变量并使用它。形如[mVar1 = localVar1, mVar2 = std::move(localVar2)](){};

C++14还允许lambda函数的形参使用auto关键字作为其类型,这实质上是函数对象的operator()成员作为模板函数;并且允许可变参数模板

auto a_lambda_func = [data1=101](int x) { /*...*/ }; //广义捕获,实质上是在函数对象中增加了数据成员data1并初始化

auto ptr = std::make_unique<int>(10); //See below for std::make_unique
auto lambda1 = [ptr = std::move(ptr)] {return *ptr;}
          //大致等效于:   
auto lambda2 = [ptr = std::make_unique<int>(10)] {return *ptr;}

auto lambda3 = [](auto x, auto y) {return x + y;} //lambda函数的形参类型为auto
struct unnamed_lambda                            //这相当于函数对象:
{
  template<typename T, typename U>
    auto operator()(T x, U y) const {return x + y;}
};

auto lambda4 = [](auto&&... params)             //可变参数的函数模板
  {
     return (foo(std::forward<decltype(params)>(params)...));
  }

Visual Basic.NET

匿名函数或lambda表达式即无名的函数或过程,作为表达式的值。可以写为一行或多行。例如:

 Dim func1=Function(i As integer) i+10
 Dim action = sub()
   End Sub
 Dim func2 = Function()
   End Function

可以在声明匿名函数的同时调用它。单行的lambda表达式不能使用Return关键字,其返回类型是自动推导得出;其参数要么都是用As关键字指明类型,要么全部是自动推导出类型。

lambda表达式在定义时可以使用所在上下文(context,即C++语言的闭包closure)的局部变量、参数、属性、Me等等的值,即使lambda表达式离开了定义时所在的context,这些被使用的局部变量等的值仍然有效。这是因为lambda表达式在定义时把所用到的context的值保存到它自己的定义类中。lambda表达式可以嵌套定义。

参考资料

  1. ^ Anonymous functions. [2014-04-27]. (原始内容存档于2014-03-27) (英语). Anonymous functions, also known as closures, allow the creation of functions which have no specified name. They are most useful as the value of callback parameters, but they have many other uses. 
  2. ^ 2.0 2.1 5.1.2 Lambda expressions. ISO/IEC 14882:2011. [2014-04-27]. (原始内容存档于2014-04-27) (英语). Lambda expressions provide a concise way to create simple function objects. 
  3. ^ Lambda expressions and closures for C++ (PDF). V Samko; J Willcock, J Järvi, D Gregor, A Lumsdaine. 2006-02-26 [2010-06-01]. (原始内容存档 (PDF)于2011-07-28). 
  4. ^ Java 7 Features. Sun Microsystems. 2010-02-09 [2010-11-21]. (原始内容存档于2012-02-07). 
  5. ^ http://php.net/create_function页面存档备份,存于互联网档案馆) the top of the page indicates this with "(PHP 4 >= 4.0.1, PHP 5)"
  6. ^ 存档副本. [2011-08-13]. (原始内容存档于2011-07-24). 
  7. ^ 存档副本. [2011-08-13]. (原始内容存档于2011-07-24). 
  8. ^ Chapter 16. Boost.Lambda - 1.55.0. [2014-04-27]. (原始内容存档于2014-04-27) (英语). 
  9. ^ 该标准文档可以在ISO/IEC 14882:2011 - Information technology -- Programming languages -- C++页面存档备份,存于互联网档案馆)购买,或者可参看2012年一月份的标准草案文件N3337页面存档备份,存于互联网档案馆),第5.1.2节的Lambda expressions
  10. ^ MSDN: In C++14, you can introduce and initialize new variables in the capture clause, without the need to have those variables exist in the lambda function’s enclosing scope. The initialization can be expressed as any arbitrary expression; the type of the new variable is deduced from the type produced by the expression. One benefit of this feature is that in C++14 you can capture move-only variables (such as std::unique_ptr) from the surrounding scope and use them in a lambda.

外部链接