匿名函數

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

外部連結