Dotnet的區域性函式和委託的對比

老王Plus發表於2021-02-03

上一篇說了一下委託,這篇來說說區域性函式和委託的對比。

把委託和區域性函式放成前後篇,是因為這兩個內容很像,用起來容易混。

需要了解委託相關內容,可以看這一篇 【傳送門

使用委託表示式(Lambda)

假設一個場景:我們有一個訂單列表,裡面有售價和採購價。我們需要計算所有物品的毛利率。

public class OrderDetails
{
    public int Id { get; set; }
    public string ItemName { get; set; }
    public double PurchasePrice { get; set; }
    public double SellingPrice { get; set; }
}

通過迭代,我們可以計算出每個專案的毛利率:

static void Main(string[] args)
{
    List<OrderDetails> lstOrderDetails = new List<OrderDetails>();

    lstOrderDetails.Add(new OrderDetails() { Id = 1, ItemName = "Item 1", PurchasePrice = 100, SellingPrice = 120 });
    lstOrderDetails.Add(new OrderDetails() { Id = 2, ItemName = "Item 2", PurchasePrice = 800, SellingPrice = 1200 });
    lstOrderDetails.Add(new OrderDetails() { Id = 3, ItemName = "Item 3", PurchasePrice = 150, SellingPrice = 150 });
    lstOrderDetails.Add(new OrderDetails() { Id = 4, ItemName = "Item 4", PurchasePrice = 155, SellingPrice = 310 });
    lstOrderDetails.Add(new OrderDetails() { Id = 5, ItemName = "Item 5", PurchasePrice = 500, SellingPrice = 550 });

    Func<double, double, double> GetPercentageProfit = (purchasePrice, sellPrice) => (((sellPrice - purchasePrice) / purchasePrice) * 100);

    foreach (var order in lstOrderDetails)
    {
        Console.WriteLine($"Item Name: {order.ItemName}, Profit(%) : {GetPercentageProfit(order.PurchasePrice, order.SellingPrice)} ");
    }
}

例子中,我們建立了一個有5個商品的列表。我們還建立了一個委託表示式,並在迴圈中呼叫。

    為了防止不提供原網址的轉載,特在這裡加上原文連結:https://www.cnblogs.com/tiger-wang/p/14361561.html

我們來看看這個委託表示式在IL中是什麼樣子:

圖上能很清楚看到,Lambda被轉換成了類。

等等,為什麼lambda表示式被轉成了類,而不是一個方法?

這裡需要劃重點。Lambda表示式,在IL中會被轉為委託。而委託是一個類。關於委託為什麼是一個類,可以去看上一篇。這兒知道結論就好。

所以,Lambda表示式會轉成一個類,應該通過一個例項來使用。而這個例項是new出來的,所以是分配在堆上的。

另外,通過IL程式碼我們也知道,IL是使用虛方法callvirt來呼叫的這個表示式。

現在,我們知道了一件事:Lambda會被轉成委託和類,由這個類的一個例項來使用。這個物件的生命週期必須由GC來處理。

使用區域性函式(Local Function)

上面的示例程式碼,我們換成區域性函式:

static void Main(string[] args)
{
    List<OrderDetails> lstOrderDetails = new List<OrderDetails>();

    lstOrderDetails.Add(new OrderDetails() { Id = 1, ItemName = "Item 1", PurchasePrice = 100, SellingPrice = 120 });
    lstOrderDetails.Add(new OrderDetails() { Id = 2, ItemName = "Item 2", PurchasePrice = 800, SellingPrice = 1200 });
    lstOrderDetails.Add(new OrderDetails() { Id = 3, ItemName = "Item 3", PurchasePrice = 150, SellingPrice = 150 });
    lstOrderDetails.Add(new OrderDetails() { Id = 4, ItemName = "Item 4", PurchasePrice = 155, SellingPrice = 310 });
    lstOrderDetails.Add(new OrderDetails() { Id = 5, ItemName = "Item 5", PurchasePrice = 500, SellingPrice = 550 });

    double GetPercentageProfit(double purchasePrice, double sellPrice)
    {
        return (((sellPrice - purchasePrice) / purchasePrice) * 100);
    }

    foreach (var order in lstOrderDetails)
    {
        Console.WriteLine($"Item Name: {order.ItemName}, Profit(%) : {GetPercentageProfit(order.PurchasePrice, order.SellingPrice)} ");
    }
}

現在,我們在Main方法中放入了區域性函式GetPercentageProfit

我們再檢查下IL裡的程式碼:

沒有新類,沒有新物件,只是一個簡單的函式呼叫。

此外,Lambda表示式和區域性函式的一個重要區別是IL中的呼叫方式。呼叫區域性函式用call,它比callvirt要快,因為它是儲存在堆疊上的,而不是堆上。

通常我們不需要關注IL如何運作,但好的開發人員真的需要了解一些框架的內部細節。

callcallvert的區別在於,call不檢查呼叫者例項是否存在,而且callvert總是在呼叫時檢查,所以callvert不能呼叫靜態類方法,只能呼叫例項方法。

還是上面的例子,這回我們用迭代器實現:

static void Main(string[] args)
{
    List<OrderDetails> lstOrderDetails = new List<OrderDetails>();

    lstOrderDetails.Add(new OrderDetails() { Id = 1, ItemName = "Item 1", PurchasePrice = 100, SellingPrice = 120 });
    lstOrderDetails.Add(new OrderDetails() { Id = 2, ItemName = "Item 2", PurchasePrice = 800, SellingPrice = 1200 });
    lstOrderDetails.Add(new OrderDetails() { Id = 3, ItemName = "Item 3", PurchasePrice = 150, SellingPrice = 150 });
    lstOrderDetails.Add(new OrderDetails() { Id = 4, ItemName = "Item 4", PurchasePrice = 155, SellingPrice = 310 });
    lstOrderDetails.Add(new OrderDetails() { Id = 5, ItemName = "Item 5", PurchasePrice = 500, SellingPrice = 550 });

    var result = GetItemSellingPice(lstOrderDetails);

    foreach (string s in result)
    {
        Console.WriteLine(s.ToString());
    }
}

private static IEnumerable<string> GetItemSellingPice(List<OrderDetails> lstOrderDetails)
{
    if (lstOrderDetails == null) throw new ArgumentNullException();

    foreach (var order in lstOrderDetails)
    {
        yield return ($"Item Name:{order.ItemName}, Selling Price:{order.SellingPrice}");
    }
}

我們將列表傳遞給GetItemSellingPice。我們在方法中檢查了列表不能為null,並在迴圈中使用yield return返回資料。

程式碼看起來沒問題,是吧?

那我們假設列表真的為空,會怎麼樣呢?應該會返回ArgumentNullException,預期是這樣。

執行一下看看,實際不是這樣。當我們使用迭代器時,方法並沒有立即執行並返回異常,而是在我們使用結果foreach (string s in result)時,才執行並返回異常。這種情況,會讓我們對於異常的判斷和處理出現錯誤。

這時候,區域性函式就是一個好的解決方式:

static void Main(string[] args)
{
    var result = GetItemSellingPice(null);

    foreach (string s in result)
    {
        Console.WriteLine(s.ToString());
    }
}

private static IEnumerable<string> GetItemSellingPice(List<OrderDetails> lstOrderDetails)
{
    if (lstOrderDetails == null) throw new ArgumentNullException();

    return GetItemPrice();

    IEnumerable<string> GetItemPrice()
    {
        foreach (var order in lstOrderDetails)
        {
            yield return ($"Item Name:{order.ItemName}, Selling Price:{order.SellingPrice}");
        }
    }
}

現在,我們正確地在第一時間得到異常。

總結

區域性函式是一個非常強大的存在。它與Lambda表示式類似,但有更優的效能。

又是一個好東西,是吧?

微信公眾號:老王Plus

掃描二維碼,關注個人公眾號,可以第一時間得到最新的個人文章和內容推送

本文版權歸作者所有,轉載請保留此宣告和原文連結

相關文章