跳至內容

匿名函式

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

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

外部連結

[編輯]