篇首語
近來發現園子里有不少人在討論函數式相關的問題,從個人性格來講,我不愛看學術氣氛太強的東西,從責任上來講,我認為也有必要寫一篇“干貨”把函數式這個問題說得明白一些,也作為自己的一個知識沉淀,於是便有了此文。
個人認為,C#語言的某些設計並不非常適合函數式開發,比如它的類型推斷並不是很近乎人意,我們知道C#還是主打面向對象的,不過這並不妨礙我們用C#來討論函數式,至少可以借鑒函數式的一些思路來優化我們的代碼。
我希望通過這篇文章讓讀者通過簡單的例子,在短時間內掌握基本函數式編程方法,了解Action與Func類型的使用。同時我希望讀者對C#泛型集合、Linq、lambda表達式和yield關鍵字有所了解。
主要內容
Action與Func類型介紹,在函數內部定義函數與返回函數,閉包與函數柯里化,高階函數與Linq應用。
第一部分 Action與Func類型介紹
近來有一些人問我Action和Func類型是什么意思,為了整篇文章知識體系的完整性,先來給大家做一番介紹(如果你熟悉這兩個類型,請跳過這部分)。
首先來看這樣一個JavaScript函數:
function sum(n1, n2) { return n1 + n2; }
我們知道,在JavaScript當中,函數是可以賦值為一個變量的,即:
var sum = function(n1, n2) { return n1 + n2; }
定義這個“變量”之后,我們可以通過sum(1,2)的方式調用這個函數。那么,如果javaScript是一種強類型語言的話,這個var是什么類型呢?
來看一下這個函數的C#代碼:
static int Sum(int n1, int n2) { return n1 + n2; }
注意到這個函數接收了兩個int型參數,返回了一個int值。那么,它的類型就是Func<int,int,int>,即它的等效代碼為:
Func<int,int,int> Sum = (int n1, int n2) => { return n1 + n2; };
我們可以F12一下,看到Func類的定義如下:
public delegate TResult Func<in T1, in T2, out TResult>(T1 arg1, T2 arg2);
這個類型實質上是一個委托,返回值是個泛型的TResult,從定義的參數表可以看出,前兩個類型T1和T2是傳入參數的類型,第三個類型是返回值類型。
根據這個道理,假設有一個Func<int,string,bool>型的變量,它表示一個委托,這個委托內包含了這樣一個函數:該函數的兩個參數是int和string類型,返回值為bool。當Func<TResult>只有一個類型參數時,TResult表示返回值類型,即Func<bool>表示一個委托,它的參數表為空,返回值為bool類型。為了方便說明,下文將委托與函數兩個概念通通使用“函數”來表示。
猜猜看Func<object, Func<string,bool>>表示什么呢?它表示一個函數,接受一個object類型的參數,返回一個Func<string,bool>。這里可以看出,函數也是可以作為函數的返回值的。
接下來看Action,我們F12一下看Action<T>的定義:
public delegate void Action<in T>(T obj);
注意到委托的返回值為void,那么實際上Action就是一個沒有返回值,只有參數表的委托,即Action<T1,T2>等價於Func<T1,T2,void>。
最后來說一下Predicate<T>,當我們寫Linq方法的Where()時,可以看到它要求傳入了一個Predicate類型的參數,它實際上就是一個bool型委托,等價於Func<T,bool>。
這里只是對這幾個委托關鍵字做一個鋪墊性的介紹,大家可以去網上搜這兩個關鍵字的用法相關的帖子,如果沒搞懂請不要往下看。
第二部分 在函數內部定義函數與返回函數
那么有人該問了,好好的一個函數,干嘛非寫成Func這樣蹩腳的形式呢?下面來看一個例子:
static void DoSth() { //前置邏輯 if (Validate()) { //后續邏輯 } } static bool Validate() { //校驗邏輯 return true; }
也許這個例子不夠恰當,但是足以說明問題,我想略有經驗的程序員都明白將校驗方法(或者說,比較方法,嗯)重構到一個新函數里,這樣能讓程序脈絡清晰,《重構》當中也提到了這一手段,但是有沒有意識到這種做法有一個詬病:這兩個方法處於一個類環境當中,通常來說DoSth方法是publish的,那么為了重構,我們不得不在這個類環境當中搞出一個private方法來支撐這個public方法,顯然這個private方法沒有什么可復用性可言,而且它污染了整個類空間,再說從面向對象的角度來看,校驗成了我這個類要承擔的職責,這豈不是很詭異?
那么我要做的,就是在提取這個Validate方法的前提下,保證這個方法別污染類空間。那么一個切實可行的辦法,就是把這個校驗函數定義在DoSth的內部,代碼如下:
static void DoSth() { Func<bool> Validate = () => { //校驗邏輯 return true; }; //前置邏輯 if (Validate()) { //后續邏輯 } }
這段代碼把校驗函數定義為了DoSth內部的一個變量,它的生存期就在DoSth內部,這樣一來就絲毫不會影響類的結構了。這就是Func的應用之一——在函數內部定義局部函數。
但是這樣還是讓人覺得很啰嗦,這個Validate完全可以在其他地方定義,然后作為參數傳進來,比如這樣:
static void DoSth(Func<bool> Validate) { //前置邏輯 if (Validate()) { //后續邏輯 } }
如此一來,這個校驗方法就可以定義在其他地方了,這就給我們做一些面向對象方面的方便(比如通過依賴注入搞到這個函數),當然也可以在調用的時候直接在參數里寫lambda表達式:
static void Main(string[] args) { DoSth(() => { //校驗邏輯 return true; }); }
可能這個“校驗”的例子舉得不是很恰當,但是這已經足夠說明Func作為參數的用法。
如果你懷疑這種手段的實際價值,想想JavaScript里的SetTimeout的第二個參數吧!所謂的回調函數,就是一種由框架調用由客戶端實現的函數,用這種寫法可以大大增加客戶端代碼的直觀性與靈活性!
既然Func類型可以作為函數的參數,那么它可不可以作為函數返回值呢?答案必然是肯定的,我們還是來看一個加法例子:
static Func<int, Func<int, int>> Sum = n1 => { return n2 => n1 + n2; };
觀察返回值類型Func<int, Func<int,int>>,它表示這個函數接受一個int型參數,返回一個Func<int,int>,也就是返回一個接受int類型參數,返回int類型值的函數。即,Sum是一個返回函數的函數。
那么這個函數如何使用呢?觀察下列主函數:
static void Main(string[] args) { var Sum5 = Sum(5); int result = Sum5(10); Console.WriteLine(result); Console.ReadKey(); }
首先,我們通過Sum(5)的方式,返回了一個Sum5變量,這個變量的類型是Func<int,int>,也就是說,我們通過Sum函數返回了Sum5函數。接下來調用這個新函數Sum5(10),得到了答案15。當然,接下來我還可以調用Sum5(20)得到25。
自然地,這個調用可以寫成Sum(5)(10),與原本的Sum(5,10)相比,新的寫法將兩個參數拆解到了多個括號之中分部調用。聰明的你一定能發現這么做的好處,就是把這個參數解耦,讓各個算法(函數)之間有更高的靈活性和可復用性。但是要注意的是,要得到最終的結果,參數的數量依舊是一個都不能少的。
另外,你有沒有從這里嗅出一些“重載”的味道?
第三部分 閉包與函數柯里化
不要被這個標題嚇倒,嗯!我們來改寫一下剛才的代碼:
static void Main(string[] args) { var Sum5 = Sum(); int result = Sum5(10); Console.WriteLine(result); Console.ReadKey(); } static Func<Func<int, int>> Sum = () => { int n1 = 5; return n2 => n1 + n2; };
這次我們讓Sum不再接收第一個參數了,而把n1定義在Sum方法的內部,調用就變成了Sum()(10),大家可以試一下,結果依舊輸出15,一切看似很自然,不過請你反復讀一讀Sum的定義,是不敢覺得似乎少了點什么?希望你停下來多讀幾遍再往下看!
問題就出在n1的定義,請回答一個問題,變量n1的生存范圍是多大?Sum函數返回的時候,n1既然是Sum的內部的局部變量,應該就被釋放掉了,那么我調用Sum5(10)的時候,被釋放掉的5是從哪里來的呢?
在解釋這個問題之前,我想你應該可以理解“Func<Func<int,int>> Sum = xxx”這種寫法,等價於“Func<int,int> Sum() { xxx }”,如果不理解,請停下來,把上面的部分再看一遍。
我們打開反編譯器對這個Sum的定義,可以看到:
[CompilerGenerated] private static Func<int, int> <.cctor>b__0() { <>c__DisplayClass3 CS$<>8__locals4; return new Func<int, int>(CS$<>8__locals4, (IntPtr) this.<.cctor>b__1); }
奇怪的是,在這個函數的第一句話,定義了一個“<>c__DisplayClass3”匿名類的對象,也就是說,Sum5這個函數的內部攜帶着這個對象,想必5這個數字就保存在這個類里,來看這個類的定義:
[CompilerGenerated] private sealed class <>c__DisplayClass3 { public int n1; public int <.cctor>b__1(int n2) { return (this.n1 + n2); } }
看到這里我想我不用再解釋什么了吧。
觀察我們的函數n2=>n1+n2,它能夠拿到外部函數Sum中的n1,而Sum卻不能拿到它內部的n2,這一類的函數,起個名字——閉包。於是現在你稍微理解JavaScript中那個叫作用域鏈的東西了嗎?
嗯,這部分的標題上提到了函數的柯里化,那什么是柯里化呢?其實剛才已經看過了,把Sum(5,10,15,20)寫成Sum(5)(10)(15)(20)就叫柯里化,或者說把Func<int,int,int>搞成Func<int, Func<int,int>>就叫柯里化,也是起個名字唬人的,就像“面向切面編程”這個名字一樣!
第四部分 高階函數與Linq應用
現在進入理論篇的最后一部分,神馬叫高階函數?還就是起個名字而已,以其他函數做參數、或者返回一個函數的函數,就叫高階函數,剛才的Sum就是高階函數。至此大家已經了解了如何在函數中調用一個作為參數的函數,為了給后面的應用篇做鋪墊,這里介紹幾個經典的高階函數,希望大家都能理解。
(1)Map函數:接受一個轉換函數和一個集合,對這個集合中的每個元素,延遲返回它執行轉換函數后的值。
static IEnumerable<TR> Map<T, TR>(Converter<T, TR> select, IEnumerable<T> list) { foreach (T val in list) { yield return select(val); } }
其中Converter是一個委托,它接受一種類型的參數,返回另一種類型的參數,也就是說如果有一個Converter類型的函數,其作用就是將一種類型轉換為另一種類型,當然,在使用的時候,我們可以傳遞一個很復雜的類,返回其中的某個字段。
public delegate TOutput Converter<in TInput, out TOutput>(TInput input);
(2)Filter函數:接受一個布爾函數作為判斷條件,作用在一個集合上,延遲返回這個集合當中滿足條件的元素。
static IEnumerable<T> Filter<T>(Predicate<T> selector, IEnumerable<T> list) { foreach (T val in list) { if (selector(val)) { yield return val; } } }
(3)Fold函數:接受一個返回TR類型的算法函數,一個TR類型的起始值,及一個集合,對這個集合中的所有值應用這一算法,並”折疊“到返回值上返回。
static TR Fold<T, TR>(Func<TR, T, TR> accumulator, TR startVal, IEnumerable<T> list) { TR result = startVal; foreach (T val in list) { result = accumulator(result, val); } return result; }
大家有沒有看出這三個函數有什么貓膩?它們都有一個IEnumerable<T>的參數,那么下面我們就把他們改造為擴展方法,並且改個名:
public static partial class Enumerable { public static IEnumerable<TR> Select<T, TR>(this IEnumerable<T> list, Converter<T, TR> selectField) { foreach (T val in list) { yield return selectField(val); } } public static IEnumerable<T> Where<T>(this IEnumerable<T> list, Predicate<T> selector) { foreach (T val in list) { if (selector(val)) { yield return val; } } } public static TR Sum<T, TR>(this IEnumerable<T> list, Func<TR, T, TR> accumulator, TR startVal) { TR result = startVal; foreach (T val in list) { result = accumulator(result, val); } return result; } }
我們可以這樣使用:
static void Main(string[] args) { IEnumerable<int> list = new List<int>() { 1, 2, 3, 4, 5, 6, 7, 8 }; list.Where(num => num % 2 == 0) .Select(num => num) .ToList().ForEach(num => { //這里就直接調用Linq了 Console.WriteLine(num); }); int sum = list.Where(num => num % 2 == 0) .Sum((x, y) => x + y, 0); Console.WriteLine("sum=" + sum); Console.ReadKey(); }
這基本和Linq沒有什么差別了,嗯,其實Linq里就是這么搞的,只是它更加豐富和嚴謹,依舊不用多解釋了。
后記
相信大家讀完這篇文章之后已經對函數式編程有了一個初步的認識,函數式還有很多精彩的應用,請關注下回分解!