C#函數式編程

提起函數式編程,你們必定想到的是語法高度靈活和動態的LISP,Haskell這樣古老的函數式語言,往近了說ruby,javascript,F#也是函數式編程的流行語言。然而自從.net支持了lambda表達式,C#雖然做爲一種指令式程序設計語言,在函數式編程方面也絕不遜色。咱們在使用c#編寫代碼的過程當中,有意無心的都會使用高階函數,組合函數,純函數緩存等思想,連表達式樹這樣的idea也來自函數式編程思想。因此接下來咱們把經常使用的函數式編程場景作個總結,有利於咱們在程序設計過程當中靈活應用這些技術,拓展咱們的設計思路和提升代碼質量。javascript

  1、高階函數

  高階函數通俗的來說:某個函數中使用了函數做爲參數,這樣的函數就稱爲高階函數。根據這樣的定義,.net中大量使用的LINQ表達式,Where,Select,SelectMany,First等方法都屬於高階函數,那麼咱們在本身寫代碼的時候何時會用到這種設計?java

  舉例:設計一個計算物業費的函數,var fee=square*price, 而面積(square)根據物業性質的不一樣,計算方式也不一樣。民用住宅,商業住宅等須要乘以不一樣的係數,根據這樣的需求咱們試着設計下面的函數:express

  民用住宅面積:編程

1
2
3
4
public Func< int , int , decimal > SquareForCivil()
{
     return (width,hight)=>width*hight;
}

  商業住宅面積:c#

1
2
3
4
public Func< int , int , decimal > SquareForBusiness()
{
     return (width, hight) => width * hight*1.2m;
}

  這些函數都有共同的簽名:Func<int,int,decimal>,因此咱們能夠利用這個函數簽名設計出計算物業費的函數:api

1
2
3
4
public decimal PropertyFee( decimal price, int width, int hight, Func< int , int , decimal > square)
{
     return price*square(width, hight);
}

  是否是很easy,寫個測試看看緩存

1
2
3
4
5
6
7
8
9
10
11
12
[Test]
public void Should_calculate_propertyFee_for_two_area()
{
     //Arrange
     var calculator = new PropertyFeeCalculator();
     //Act
     var feeForBusiness= calculator.PropertyFee(2m,2, 2, calculator.SquareForBusiness());
     var feeForCivil = calculator.PropertyFee(1m, 2, 2, calculator.SquareForCivil());
     //Assert
     feeForBusiness.Should().Be(9.6m);
     feeForCivil.Should().Be(4m);
}

  2、惰性求值

  C#在執行過程使用嚴格求值策略,所謂嚴格求值是指參數在傳遞給函數以前求值。這個解釋是否是仍是有點不夠清楚?咱們看個場景:有一個任務須要執行,要求當前內存使用率小於80%,而且上一步計算的結果<100,知足這個條件才能執行該任務。ruby

  咱們能夠很快寫出符合這個要求的C#代碼:框架

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public double MemoryUtilization()
  {
      //計算目前內存使用率
      var pcInfo = new ComputerInfo();
      var usedMem = pcInfo.TotalPhysicalMemory - pcInfo.AvailablePhysicalMemory;
      return ( double )(usedMem / Convert.ToDecimal(pcInfo.TotalPhysicalMemory));
  }
  
  public int BigCalculatationForFirstStep()
  {
      //第一步運算
      System.Threading.Thread.Sleep(TimeSpan.FromSeconds(2));
      Console.WriteLine( "big calulation" );
      FirstStepExecuted = true ;
      return 10;
  }
  
  public void NextStep( double memoryUtilization, int firstStepDistance)
  {
//下一步運算
      if (memoryUtilization<0.8&&firstStepDistance<100)
      {
          Console.WriteLine( "Next step" );
      }
  }

  在執行NextStep的時候須要傳入內存使用率和第一步(函數BigCalculatationForFirstStep)的計算結果,如代碼所示,第一步操做是一個很費時的運算,可是因爲C#的嚴格求值策略,對於語句if(memoryUtilization<0.8&&firstStepDistance<100)來說,即便內存使用率已經大於80%了,第一步操做還得執行,很顯然,若是內存使用率大於80%,值firstStepDistance已經不重要了,徹底能夠不用計算。ide

  因此惰性求值是指:表達式或者表達式的一部分只有當真正須要它們的結果時纔會對它們進行求值。咱們嘗試用高階函數來重寫這個需求:

1
2
3
4
5
6
7
public void NextStepWithOrderFunction(Func< double > memoryUtilization,Func< int > firstStep)
{
     if (memoryUtilization() < 0.8 && firstStep() < 100)
     {
         Console.WriteLine( "Next step" );
     }
}

  代碼很簡單,就是用一個函數表達式來代替函數值,若是if (memoryUtilization() < 0.8..這句不知足,後面的函數也不會執行。微軟在.net4.0版本中加入了Lazy<T>類,你們能夠在有這種需求的場景下使用這個機制。

  3、函數柯里化(Curry)

  柯里化也稱做局部套用。定義:是把接受多個參數的函數變換成接受一個單一參數(最初函數的第一個參數)的函數,而且返回接受餘下的參數且返回結果的新函數的技術,ps:爲何官方解釋這麼繞口?

  看到這樣的定義估計你們也很難明白這是這麼一回事,因此咱們從curry的原理講起:

  寫一個兩個數相加的函數:

1
2
3
4
public Func< int , int , int > AddTwoNumber()
{
     return (x, y) => x + y;
}

  ok, 如何使用這個函數?

1
var result= _curringReasoning.AddTwoNumber()(1,2);

  1+2=3,調用很簡單。需求升級,咱們須要一個函數,這個函數要求輸入一個參數(number),算出10+輸入的參數(number)的結果。估計有人要說了,這需求上面的代碼徹底能夠實現啊,第一個參數你傳入10不就完了麼,ok,若是你是這樣想的,我也是迫不得已。還有人可能說了,再寫一個重載,只要一個參數便可,實際狀況是不允許,咱們在調用別人提供的api,沒法添加劇載。能夠看到局部套用的使用場景不是一種很廣泛的場景,因此在合適的場景配合合適的技術纔是最好的設計,咱們來看局部套用的實現:

1
2
3
4
5
public Func< int , Func< int , int >> AddTwoNumberCurrying()
{
     Func< int , Func< int , int >> addCurrying = x => y => x + y;
     return addCurrying;
}

  表達式x => y => x + y獲得的函數簽名爲Func<int, Func<int, int>>,這個函數簽名很是清楚,接收一個int類型的參數,獲得一個Func<int,int>類型的函數。此時若是咱們再調用:

1
2
3
4
5
6
//Act
var curringResult = curringReasoning.AddTwoNumberCurrying()(10);
var result = curringResult(2);
  
//Assert
result.Should().Be(12);

  這句話:var curringResult = curringReasoning.AddTwoNumberCurrying()(10); 生成的函數就是隻接收一個參數(number),且能夠計算出10+number的函數。

  一樣的道理,三個數相加的函數:

1
2
3
4
public Func< int , int , int , int > AddThreeNumber()
{
     return (x, y, z) => x + y + z;
}

  局部套用版本:

1
2
3
4
5
public Func< int ,Func< int ,Func< int , int >>> AddThreeNumberCurrying()
{
     Func< int , Func< int , Func< int , int >>> addCurring = x => y => z => x + y + z;
     return addCurring;
}

  調用過程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
[Test]
public void Three_number_add_test()
{
     //Arrange
     var curringReasoning = new CurryingReasoning();
  
     //Act
     var result1 = curringReasoning.AddThreeNumber()(1, 2, 3);
     var curringResult = curringReasoning.AddThreeNumberCurrying()(1);
     var curringResult2 = curringResult(2);
     var result2 = curringResult2(3);
     
     //Assert
     result1.Should().Be(6);
     result2.Should().Be(6);
}

  當函數參數多了以後,手動局部套用愈來愈不容易寫,咱們能夠利用擴展方法自動局部套用:

1
2
3
4
5
6
7
8
9
public static Func<T1, Func<T2, TResult>> Curry<T1, T2, TResult>( this Func<T1, T2, TResult> func)
{
     return x => y => func(x, y);
}
  
public static Func<T1, Func<T2, Func<T3, TResult>>> Curry<T1, T2, T3, TResult>( this Func<T1, T2, T3,TResult> func)
{
     return x => y => z=>func(x, y,z);
}

  一樣的道理,Action<>簽名的函數也能夠自動套用

  有了這些擴展方法,使用局部套用的時候就更加easy了

1
2
3
4
5
6
7
8
9
10
11
12
13
[Test]
public void Should_auto_curry_two_number_add_function()
{
     //Arrange
     var add = _curringReasoning.AddTwoNumber();
     var addCurrying = add.Curry();
  
     //Act
     var result = addCurrying(1)(2);
  
     //Assert
     result.Should().Be(3);
}

  好了,局部套用就說到這裏,stackoverflow有幾篇關於currying使用的場景和定義的文章,你們能夠繼續瞭解。

  函數式編程還有一些重要的思想,例如:純函數的緩存,所爲純函數是指函數的調用不受外界的影響,相同的參數調用獲得的值始終是相同的。尾遞歸,單子,代碼即數據(.net中的表達式樹),部分應用,組合函數,這些思想有的我也仍然在學習中,有的還在思考其最佳使用場景,因此再也不總結,若是哪天領會了其思想會補充。

  4、設計案例

  最後我仍是想設計一個場景,把高階函數,lambda表達式,泛型方法結合在一塊兒,我之因此設計這樣的例子是由於如今不少的框架,開源的項目都有相似的寫法,也正是由於各類技術和思想結合在一塊兒,纔有了極富有表達力而且很是優雅的代碼。

  需求:設計一個單詞查找器,該查找器能夠查找某個傳入的model的某些字段是否包含某個單詞,因爲不一樣的model具備不一樣的字段,因此該查找須要配置,而且能夠充分利用vs的智能提示。

  這個功能其實就兩個方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
private readonly List<Func< string , bool >> _conditions;
  
public WordFinder<TModel> Find<TProperty>(Func<TModel,TProperty> expression)
{
     Func< string , bool > searchCondition = word => expression(_model).ToString().Split( ' ' ).Contains(word);
     _conditions.Add(searchCondition);
     return this ;
}
  
public bool Execute( string wordList)
{
     return _conditions.Any(x=>x(wordList));
}

  使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
[Test]
public void Should_find_a_word()
{
     //Arrange
     var article = new Article()
     {
         Title = "this is a title" ,
         Content = "this is content" ,
         Comment = "this is comment" ,
         Author = "this is author"
     };
  
     //Act
     var result = Finder.For(article)
         .Find(x => x.Title)
         .Find(x => x.Content)
         .Find(x => x.Comment)
         .Find(x => x.Author)
         .Execute( "content" );
  
     //Assert
     result.Should().Be( true );
}

  該案例自己不具備實用性,可是你們能夠看到,正是各類技術的綜合應用才設計出極具語義的api, 若是函數參數改成Expression<Func<TModel,TProperty>> 類型,咱們還能夠讀取到具體的屬性名稱等信息。

相關文章
相關標籤/搜索