首頁 >後端開發 >C#.Net教程 >詳細介紹C#函數式程式設計的範例程式碼

詳細介紹C#函數式程式設計的範例程式碼

黄舟
黄舟原創
2017-03-09 15:17:391842瀏覽

  提起函數式編程,大家一定想到的是語法高度靈活且動態的LISP,Haskell這樣古老的函數式語言,往近了說ruby,javascript,F#也是函數式程式設計的流行語言。然而自從.net支援了lambda表達式,C#雖然作為一種指令式程式設計語言,在函數式程式設計方面也毫不遜色。我們在使用c#編寫程式碼的過程中,有意無意的都會使用高階函數,組合函數,純函數快取等思想,連表達式樹這樣的idea也來自函數式程式設計思想。所以接下來我們把常用的函數式程式設計場景做個總結,有利於我們在程式設計過程中靈活應用這些技術,拓展我們的設計思路和提升程式碼品質。

#   一、高階函數

  高階函數通俗的來講:在某個函數中使用了函數作為參數,這樣的函數就稱為高階函數。根據這樣的定義,.net中大量使用的LINQ表達式,Where,Select,SelectMany,First等方法都屬於高階函數,那麼我們在自己寫程式碼的時候什麼時候會用到這種設計?

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

  民用住宅面積:

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

  商業住宅面積:

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

  這些函數都有共同的簽章:Func31803993a3bae4f7d54758d225c3e107,所以我們可以利用這個函式簽章設計出計算物件費的函式:

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

  是不是很easy,寫個測驗看看

[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);
}

  二、惰性求值

  C#在執行過程使用嚴格求值策略,所謂嚴格求值是指參數在傳遞給函數之前求值。這個解釋是不是還是有點不夠清楚?我們看個場景:有一個任務要執行,要求目前記憶體使用率小於80%,而上一個步驟計算的結果7ede4c5b8195d19f0bf8ba195e74ae2a類,大家可以在有這種需求的場景下使用這個機制。   三、函數柯里化(Curry)

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

  看到這樣的定義估計大家也很難明白這是這麼一回事,所以我們從curry的原理講起:

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

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

  ok, 如何使用這個函數?

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

  1+2=3,呼叫很簡單。需求升級,我們需要一個函數,這個函數要求輸入一個參數(number),算出10+輸入的參數(number)的結果。估計有人要說了,這需求上面的程式碼完全可以實現啊,第一個參數你傳入10不就完了麼,ok,如果你是這樣想的,我也是無可奈何。還有人可能說了,再寫一個重載,只要一個參數即可,實際情況是不容許,我們在調用別人提供的api,無法添加重載。可以看到局部套用的使用場景不是很普遍的場景,所以在合適的場景配合合適的技術才是最好的設計,我們來看局部套用的實作:

public Func<int, Func<int, int>> AddTwoNumberCurrying()
{
    Func<int, Func<int, int>> addCurrying = x => y => x + y;
    return addCurrying;
}

表達式x => y => x + y得到的函數簽名為Func9d41917661ce24c26c6c064bdf051b81>,這個函數簽名非常清楚,接收一個int類型的參數,得到一個Func4101e4c0559e7df80e541d0df514fd80類型的函數。此時如果我們再來呼叫:

//Act
var curringResult = curringReasoning.AddTwoNumberCurrying()(10);
var result = curringResult(2);
 
//Assert
result.Should().Be(12);

  这句话:var curringResult = curringReasoning.AddTwoNumberCurrying()(10); 生成的函数就是只接收一个参数(number),且可以计算出10+number的函数。

  同样的道理,三个数相加的函数:

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

  局部套用版本:

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;
}

  调用过程:

[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);
}

  当函数参数多了之后,手动局部套用越来越不容易写,我们可以利用扩展方法自动局部套用:

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);
}

  同样的道理,Actiona8093152e673feb7aba1828c43532094签名的函数也可以自动套用

  有了这些扩展方法,使用局部套用的时候就更加easy了

[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中的表达式树),部分应用,组合函数,这些思想有的我也仍然在学习中,有的还在思考其最佳使用场景,所以不再总结,如果哪天领会了其思想会补充。

  四、设计案例

  最后我还是想设计一个场景,把高阶函数,lambda表达式,泛型方法结合在一起,我之所以设计这样的例子是因为现在很多的框架,开源的项目都有类似的写法,也正是因为各种技术和思想结合在一起,才有了极富有表达力并且非常优雅的代码。

  需求:设计一个单词查找器,该查找器可以查找某个传入的model的某些字段是否包含某个单词,由于不同的model具有不同的字段,所以该查找需要配置,并且可以充分利用vs的智能提示。

  这个功能其实就两个方法:

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(&#39; &#39;).Contains(word);
    _conditions.Add(searchCondition);
    return this;
}
 
public bool Execute(string wordList)
{
    return _conditions.Any(x=>x(wordList));
}

  使用:

[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, 如果函数参数改为Expression7e21a607b5f8b4a44e84d6cad53bd045> 类型,我们还可以读取到具体的属性名称等信息。

以上是詳細介紹C#函數式程式設計的範例程式碼的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述:
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn