Closure(閉包)是一種特殊的函數,可以讀取外面環境中的資料,而外部無法修改函數內部的資料。這樣的介紹有如天書,實際上Closure是一個C#工程師幾乎天天在寫的東西,再次把從資料庫取得會員資料的程式搬出來:
public Person GetPersonById(long id)
{
return _dbContext.Person.FirstOrDefault(x => x.Id == id);
}
仔細觀察FirstOrDefault裡面的lambda expression,把它擷取出來:
x => x.Id == id;
由於lambda其實就是一個方法,這個方法僅具有一個引數x
,但對它來說id
是一個變數,既然沒有將id傳入方法,那方法本身又是怎麼知道的呢?先回到GetPersonById
,我們知道id的作用範圍在這個方法裡面,那麼對於lambda來說,它就是直接取得了這個作用範圍內的環境變數,這個結構就是所謂的closure。
我們再來看昨天寫的有點微妙的委派,為了容易解釋closure把它用另一個方式寫出來
Func<long, Func<string, Func<DateTime, bool>>> currying1 =
id => name => date => ApplyInsurance(id,name,date);
// 可以寫成
Func<long, Func<string, Func<DateTime, bool>>> insurance2 =
(long id) =>
{
return (string name) =>
{
return (DateTime date) =>
{
return ApplyInsurance(id, name, date);
};
};
};
下面這個一層包一層的作法,對於最內層的lambda來說取得了外面的id與name變數,其實就是一個多層closure的形式。
除了lambda expression以外,C#中還有一個東西會用到閉包叫做區域函式,比起lambda來說大家可能比較不習慣使用,但是熟悉後會覺得非常好用,假設我今天有個方法希望呼叫某個外部api後把response整理並且存到database,並且回傳操作是否成功
public bool GetInfomationAndSave(Request entity)
{
var apiKey = GetApiKey();
return GetApiResult(entity)
.Bind(Save)
.Bind(IsSuccess);
Either<Fail,Response> GetApiResult(Request entity) =>
{
var client = new HttpClient();
client.SetKey(apiKey); // 取得環境變數
// ...剩下的實做
}
Either<Fail,int> Save(Response) => // ...
bool IsSuccess(Either<Fail,int> result) => // ...
}
這邊有稍微展示了函數式設計的撰寫風格,C#中可以將各個步驟拆成local function,再透過HOF把每個步驟串起來組成方法鏈,而apiKey可以透過Closure傳遞到區域函式中。Either是FP中對於例外處裡的抽象,預計之後再跟錯誤處裡一起介紹。
補充一下我很喜歡寫local function,在設計程式的時候為了關注點分離,我們會將某些步驟抽成private方法,但很多時候抽出來的方法其實沒有重用性,從這個角度來思考又會覺得抽private有點多餘,我覺得local function很適合這個用途。順帶一提.net6的Top-level statements,在寫Console專案的時候已經看不到main()了,在裡面定義的方法會是local function。
Closure是讓函數取得外部變數的技巧,在FP中我們會將函數作為物件傳遞,這是一個幫助建構函數內容的技巧,明天預計先找個實例來小試一下函數式的設計風格。