跳至內容

匿名函數

本頁使用了標題或全文手工轉換
維基百科,自由的百科全書

匿名函數(英語: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.

外部連結