Dotnet的局部函數和委托的對比
本文轉載自微信公眾號「老王Plus」,作者老王Plus的老王 。轉載本文請聯系老王Plus公眾號。
把委托和局部函數放成前后篇,是因為這兩個內容很像,用起來容易混。
使用委托表達式(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個商品的列表。我們還創建了一個委托表達式,并在循環中調用。
我們來看看這個委托表達式在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如何運作,但好的開發人員真的需要了解一些框架的內部細節。
call和callvert的區別在于,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表達式類似,但有更優的性能。
又是一個好東西,是吧?