.NET併發程式設計-函式閉包

那是山發表於2021-02-08

本系列學習在.NET中的併發並行程式設計模式,實戰技巧

內容目錄

函數語言程式設計閉包的應用記憶化函式快取

函數語言程式設計

一個函式輸出當做另一個函式輸入。有時候一個複雜問題,我們拆分成很多個步驟函式,這些函式組合起來呼叫解決一個複雜問題。

在C#中不支援函式組合,但可以直接像這樣呼叫B(A(n)),這也是函式組合,但這不利於閱讀,人們習慣從左往右閱讀,而不是相反的方向。通過建立擴充套件方法可以任何組合兩個函式,像下面這樣

Func<A,C> Compose<A,B,C>(this Func<A.B> f ,Func<B,C> g)=>(n)=>g(f(n))

上述程式碼為泛型委託Func<a,b style="font-size: inherit; color: inherit; line-height: inherit; margin: 0px; padding: 0px;">建立了一個擴充套件Compose的擴充套件方法,以泛型委託Func<b,c style="font-size: inherit; color: inherit; line-height: inherit; margin: 0px; padding: 0px;">為輸入引數,返回組合後的函式Func<a,c style="font-size: inherit; color: inherit; line-height: inherit; margin: 0px; padding: 0px;">。建立一個高階函式Compose把不利於閱讀的隱藏起來。

在F#中就非常方便的使用函式組合。舉個例子,將一個列表中數字增加4再乘以3,構建這兩個步驟的函式(當然利用C#linq或F#map可以直接(x+4)*3,這裡主要演示兩個功能函式如何組合起來)。

let add4 x=x+4
let mulitply3 x=x*3
let list=[0..10]
let newList=List.map(fun x->mulitply3(add4(x))) list
let newList2=list |>List.map(add4>>mulitply3

在F#中使用>>中綴運算子來使函式組合可以從左到右閱讀,更加精煉、簡潔。

閉包的應用

閉包可以讓函式訪問其所在的外部函式中的引數和變數,即使在其外部函式被返回之後。在js中經常會出現閉包的場景,在C#和F#中,編譯器使用閉包來增加和擴充套件變數的範圍。

C#在.NET2.0後引入閉包。在lambda和匿名方法中得到充分的使用。像下面的匿名函式引用變數a,訪問和管理變數a的狀態。如果不用閉包,就需要額外建立一個類函式來呼叫。

string s= "free variable";
Func<string,string> lambda = value=> a + " " + value;

以下載圖片更新窗體PictureBox控制元件為例:

void UpdateImage(string url)
{
    System.Windows.Forms.PictureBox picbox = this.pictureBox1;
    var client = new WebClient();
    client.DownloadDataCompleted += (o, e) =>
        {
            if (picbox != null)
            {
                using (var ms = new MemoryStream(e.Result))
                {
                    picbox.Image = Image.FromStream(ms);
                }
            }
        };
    client.DownloadDataAsync(new Uri(url));
    //picbox = null;
}

因為是非同步下載,UPdateImage方法返回後,圖片還未下載完成,但picbox變數仍然可以使用。這就是變數捕獲。lambda表示式捕獲了區域性變數image,因此它仍停留在作用域中。但捕獲的變數值是在執行時確定的,而不是在捕獲時,最後一句如果放開,將不能更新窗體。執行時picbox為null了,在F#中不存在null的概念,所以也不會出現此類錯誤。

.NET併發程式設計-函式閉包

多執行緒環境中的閉包使用。猜測下面的程式碼執行結果如何?

for (int i = 1; i < 10; i++)
{
    Task.Factory.StartNew(()=>Console.WriteLine("{0}-{1}",
        Thread.CurrentThread.ManagedThreadId,i));
}

不會按期望的那樣列印1-9,因為他們共享變數i,呼叫時i的值可能已經被迴圈修改了。印證上面說的捕獲的變數值是在執行時確定的。

這種情況就很難搞,給並行程式設計帶來了頭疼的問題,變數可變,這不廢話嗎,變數不會變就不叫變數了。在C#中解決此類問題的一個方法就是為每個任務建立建立和捕獲一個新的臨時變數,這樣它就能保留捕獲時的值。在F#中不存在這個問題,它的For迴圈每次建立一個新的不可變值。

記憶化函式快取

一些函式會頻繁的使用相同的引數去呼叫。我們可以將用相同的引數呼叫函式的結果儲存起來,以便下次呼叫直接返回結果。例如對圖片每個畫素做處理,一張圖片可能相同畫素的會有很多,通過快取可以直接返回上次計算結果。

//簡單的函式快取
public static Func<T, R> Memoize<T, R>(Func<T, R> func) where T : IComparable 
{
    Dictionary<T, R> cache = new Dictionary<T, R>();    
    return arg =>                                       
    {
        if (cache.ContainsKey(arg))                     
            return cache[arg];                          
        return (cache[arg] = func(arg));                
    };
}

// 執行緒安全的函式快取
public static Func<T, R> MemoizeThreadSafe<T, R>(Func<T, R> func) where T : IComparable
{
    ConcurrentDictionary<T, R> cache = new ConcurrentDictionary<T, R>();
    return arg => cache.GetOrAdd(arg, a => func(a));
}

// 利用延遲提高效能的函式快取
public static Func<T, R> MemoizeLazyThreadSafe<T, R>(Func<T, R> func) where T : IComparable
{
    ConcurrentDictionary<T, Lazy<R>> cache = new ConcurrentDictionary<T, Lazy<R>>();
    return arg => cache.GetOrAdd(arg, a => new Lazy<R>(() => func(a))).Value;
}

上述示例程式碼中有三個版本的函式記憶化。呼叫像下面這樣

public static string Greeting(string name)
{
    return $"Warm greetings {name}, the time is {DateTime.Now.ToString("hh:mm:ss")}";
}

public static void RunDemoMemoization()
{
    var greetingMemoize = Memoize<stringstring>(Greeting);
    Console.WriteLine(greetingMemoize("Richard"));
    Console.WriteLine(greetingMemoize("Paul"));
    Console.WriteLine(greetingMemoize("Richard"));
}

執行緒安全字典ConcurrentDictionary可以保證只向集合裡新增一個相同值,但函式求值可能會被執行多次,所以利用.NET4之後的延遲物件載入技術。在真正需要使用物件時候才去例項化(通過訪問延遲物件的Value屬性),而且是執行緒安全的。

補充一個,以前寫設計模式時,單例模式的建立方式,第四種,延遲+執行緒安全的單例模式傳送門,設計模式速查手冊

 public sealed class Singleton
 {
    private static readonly Lazy<Singleton> lazy =
        new Lazy<Singleton>(() => new Singleton(), true); //#A

    public static Singleton Instance => lazy.Value;

    private Singleton()
    
{ }
}

to be contiued!
下集:不可變性

寫給普通:


花有重開日,人無再少年

小時候快樂是本能,長大後快樂是本事

相關文章