Closure這種寫法,在程式語言領域存在已經有一段很長的時間了,其原意很簡單,就程式語言角度來看,Closure意指將一個function(函式)放到一個變數中,
也就是C++常用的function pointer(函式指標)的意思
文/黃忠成
Closure or Lambda
Closure這種寫法,在程式語言領域存在已經有一段很長的時間了,其原意很簡單,就程式語言角度來看,Closure意指將一個function(函式)放到一個變數中,
也就是C++常用的function pointer(函式指標)的意思,這是Closure的第一段定義,形成Closure的是重要的第二段定義,該函式必須能夠存取此function pointer形成
所在地的變數(local or global),即使所在地已經消滅,仍不影響Closure存取該所在地變數的事實。
在我了解的語言中,JavaScript對於Closure的實作的概念與外觀算是比較直覺的,例如下面這種Closure寫法。
function test() {
var p = function(x, y) {
returnx + y;
};
alert(p(15, 20));
}
當然,你也可以寫成以下這種非Closure的樣子。
function test11() {
alert("Hello World");
}
function test6() {
var p = test11;
p();
}
只是這樣就無法符合Closure的第二段定義,因此,第一種寫法在本地端具現函式的型態是必要的,下面是完整的Closure寫法。
function test2() {
var base = 15;
var p = function(y) {
return base + y;
};
alert(p(20));
}
注意,function(y)可以存取具現所在的地的base變數,即使test2函式已經結束也不影響base變數的存取,這是Closure定義中的第二段,也是精隨部分。
所謂具現所在地,通常泛指三種型態的變數,其種兩種是local(區域)與global(全域),第三種則是由函式參數傳導過來而形成的local(區域)變數。
function test3(base) {
var p = function(y) {
returnbase + y;
};
alert(p(20));
}
Closure也可以當成參數傳入。
function test6(myFunc) {
alert(myFunc(15, 25));
}
.................
test6(function (x, y) {
return x + y;
});
在JQuery誕生之前,我們很少使用這種手法去撰寫程式,因為這種方法有個觀念上的缺失,那就是函式本身是為了切割程式複雜度及可重用而設計的,Closure破壞了這個觀念,
這是為何在JQuery誕生之前我們很少看到這種寫法的主要原因。
任何應用程式都有著可重用性的部分,但相對的也存在著一次性、可拋棄性的部分,舉例來說,當為button撰寫一個click事件處理函式時,可能會是這樣寫的。
IE Only
<button name="testMyButton" type="button">variable catch2</button>
<script language="javascript">
function myButtonClick() {
alert("Hello World");
}
testMyButton.onclick = myButtonClick;
</script>
只是,myButtonClick其實就只用在testMyButton,根本不具備可重用性的價值,透過Closure,我們可以少打些字,並讓程式看起來更平述些。
<button name="testMyButton" type="button">variable catch2</button>
<script language="javascript">
testMyButton.onclick = function () {
alert("Hello World");
}
</script>
在C# 2.0時,新增了Anonymous Method的寫法,也就是匿名函式,這是C#最早所支援的Closure寫法。
public delegate void SumPrint(int x, int y);
public delegate void SumPrint2(int y);
public static void Test()
{
SumPrint p = delegate(int x, int y)
{
Console.WriteLine(x + y);
};
p(15, 20);
}
Anonymous Method其實也具備了Closure第二段定義的能力。
public delegate void SumPrint(int x, int y);
public delegate void SumPrint2(int y);
public static void Test2()
{
int baseValue = 15;
SumPrint2 p = delegate(int y)
{
Console.WriteLine(baseValue + y);
};
p(15);
}
在C# 3.0加入了新名詞: Lambda Expression,這是一個原本來自數學運算式表示方法的詞,在C# 3.0中寫法是這樣的。
public static void Test5(Action printFunc)
{
printFunc("Hello World");
}
………
static void Main(string[] args)
{
Test5(s => Console.WriteLine(s));
Console.ReadLine();
}
原本的Anonymous Method寫法則與新的Lambda Expression寫法結合,形成另一種更容易使用的Closure寫法。
public static void Test6()
{
int baseValue = 15;
var p = new Action((y) =>
{
baseValue += 5;
Console.WriteLine(baseValue + y);
});
p(15);
}
也可以當成參數傳遞。
public static void Test5(Action printFunc)
{
printFunc("Hello World");
}
.......................
Test5(s => Console.WriteLine(s));
如需傳入有傳回值的參數則是使用Func<T...>。
Closure in Java
相對於JavaScript與C#,Java對Closure的支援就較為薄弱,比較不那麼簡單易懂,但也不能說沒有,其以Anonymous Class型態呈現。
public static void Test() {
Thread th = new Thread(new Runnable()
{
@Override
public void run() {
System.out.println( "test");
}
});
th.start();
}
當然,也可存取所在地變數。
public static void Test2() {
final String baseString = "Hello ";
Thread th = new Thread(new Runnable()
{
@Override
public void run() {
System.out.println( baseString + "test");
}
});
th.start();
}
特別注意一下這段程式,這裡要存取baseString必須將其設定為final,也就是不可再次變更她的值,如果想這樣做,必須將
baseString提出成為類別變數才行。
public class TestClosure {
private static String g_baseValue = "Hello! ";
……………
public static void Test4() {
Thread th = new Thread(new Runnable()
{
@Override
public void run() {
g_baseValue = g_baseValue +"test";
System.out.println( g_baseValue);
}
}
);
th.start();
}
這指出了實作Closure必須注意的另一個問題,那就是存取所在變數分成兩部分,一個是讀,這點在多數語言都可以辦到,但另一個寫的動作在不同語言中會有不同行為。
另外,補充一點,JDK 1.8會支援更簡潔、用途更廣的的Closure寫法。
Closure in C++
C++ 0x(我知道她正名為C++ 11了,但我喜歡C++ OX這個詞),正式支援Lambda寫法,也就是Closure,下列的寫法可以在支援C++ 11的編譯器上正常運作。
#include
#include
#include
using namespace std;
void test1()
{
auto func1 = [](int x, int y) { return x + y; };
cout << func1(15,20) << endl;
}
在存取所在地變數部分,C++ 11做出更明確的規則。
[] |
不捕捉任何所在地變數,也就是說function body中不能存取任何所在地變數,是完全封閉的函式,前面列出的寫法就是這種。 |
[=] |
以唯讀方式捕捉所有所在地變數,function body不允許更改變數的值。 |
[&] |
以傳址方式捕捉所有所在地變數,function body允許更改變數的值。 |
[<variable name>] |
明確指定捕捉哪一個所在地變數。 |
[this] | 捕捉this指標。 |
[]方式最簡單,就是都不捕捉就是了,第二種是唯讀方式捕捉,例如下面這樣。
void test7(int x,int y)
{
auto func1 = [=]()
{
return x + y;
};
cout << func1() << endl;
}
此時function body中不允許對x或y變數賦值動作發生,就如同Java一樣。
Error!
auto func1 = [=]()
{
x = x +7;
return x + y;
};
要做到賦值,得用[&]。
auto func1 = [&]()
{
x = x +7;
return x + y;
};
最後一種是直接指定變數來補捉。
void test2()
{
int x = 15;
auto func1 = [x](int y) { return x + y; };
cout << func1(20) << endl;
}
你也可以指定捕捉一個以上的變數,或是指定特定變數以傳址方式補捉。
void test4(int x,int y)
{
auto func1 = [&x,y]()
{
x++;
return x + y;
};
cout << func1() << endl;
}
當沒有加上&時,就是以=方式捕捉。
C++在宣告Lambda時,允許明確指定函式的回傳值,如果不指定,就是交由編譯器決定。
void test4(int x,int y)
{
auto func1 = [&x,y]() -> int
{
x++;
return x + y;
};
cout << func1() << endl;
}
當賦值的變數可能影響回傳值時,某些編譯器(例如C++/CX,Visual C++ 11)需要明確指定回傳值的型別,
也就是 -> <型別>。
當成參數傳遞的寫法如下。
void test6(function<int (int,int)> func)
cout << func(15,20) << endl; test6([](int x,int y) { return x + y;}); |
Closure in Objective-C
Objective-C 2.0開始引入Blocks概念,對應了Closure寫法。
void test1()
{
int (^Sum)(int,int) = ^(int x,int y) {
return x + y;
};
NSLog(@"%d", Sum(15,20));
}
也可存取所在地變數。
void test2()
{
int baseValue = 20;
int (^Sum)(int) = ^(int y) {
return baseValue + y;
};
NSLog(@"%d", Sum(20));
}
變數捕捉部分預設為傳值,可使用__block宣告式改變為傳址。
void test3()
{
__block int baseValue = 15;
int (^Sum)(int) = ^(int y) {
baseValue += 5;
return baseValue + y;
};
NSLog(@"%d", Sum(20));
}
當成參數的寫法如下。
typedef int (^SumFunc)(int,int);
void test4(SumFunc func)
{
NSLog(@"%d", func(20,50));
}
...............
test4( ^(int x,int y) {
return x + y;
});
Closure or not?
當一種手法在誕生時不獲得重視時,必然有其背景因素存在,當年AJAX不被重視是因為網路速度及背景技術沒辦法支持這種寫法,Closure不被重視則是因為
函式本身就是用來切割複雜度及提高可用性的。
另外,Closure通常會帶出值捕捉的議題,假設以下這種C++寫法,你能猜出結果如何嗎?
void test7(int x,int y)
{
auto func1 = [&]()
{
x = x +7;
return x + y;
};
auto func2 = [&]()
{
return x + y;
};
cout << func1() << endl;
cout << func2() << endl;
}
….
test7(15,20);
答案是兩個都是42。
換一種寫法。
void test7(int x,int y)
{
auto func1 = [&]()
{
x = x +7;
return x + y;
};
auto func2 = [&]()
{
return x + y;
};
cout << func2() << endl;
cout << func1() << endl;
}
答案是35及42,這就是重點,因為傳址式捕捉行為,會讓程式變得難以預測,所以非到必要時不要使用傳址式補捉。
雖然C# 沒有明確指定值捕捉行為的語法,但有個隱規則存在,對於區域變數(值(Value)型態變數)預設為傳值式捕捉,類別變數與靜態變數則是傳址方式捕捉。
public static void Test7(int x,int y)
{
var p = new Func((int x1,int y1) =>
{
x1 += 7;
return x1 + y1;
});
var p1 = new Func((int x1, int y1) =>
{
return x1 + y1;
});
Console.WriteLine(p(x,y).ToString());
Console.WriteLine(p1(x, y).ToString());
}
private static int g_x = 15;
public static void Test8(int y)
{
var p = new Func(( int y1) =>
{
g_x += 7;
return g_x + y1;
});
var p1 = new Func((int y1) =>
{
return g_x + y1;
});
Console.WriteLine(p(y).ToString());
Console.WriteLine(p1(y).ToString());
}
對於參考型態變數則一律為傳址式捕捉。
public static void Test9(MyValue x)
{
var p = new Func((MyValue x1) =>
{
x1.x += 7;
return x1.x + x1.y;
});
var p1= new Func((MyValue x1) =>
{
return x1.x + x1.y;
});
Console.WriteLine(p(x).ToString());
Console.WriteLine(p1(x).ToString());
}
…………………
public class MyValue
{
public int x;
public int y;
}
JavaScript中沒有傳值捕捉的概念,一律為傳址。
var g_base = 15;
function test4() {
var p = function (y) {
g_base += 7;
return g_base + y;
};
var p1 = function (y) {
return g_base + y;
};
alert(p(20));
alert(p1(20));
}
後記
Closure是把雙面刃,一般來說其實不太適合程式初學者使用,只是因為時代的變遷而浮上檯面,在使用前,請記得注意值捕捉及重用性的課題,
本文所列出的也非所有情況,畢竟值捕捉搭上物件參考或是指標時,變化性非常多樣 。
PS: 本文中,我儘量解釋5種語言的傳址與傳值捕捉行為,若亂中有錯,也請指出,謝謝。