Dotnet的局部函数和委托的对比

开发 前端
把委托和局部函数放成前后篇,是因为这两个内容很像,用起来容易混。

 [[380147]]

本文转载自微信公众号「老王Plus」,作者老王Plus的老王 。转载本文请联系老王Plus公众号。

把委托和局部函数放成前后篇,是因为这两个内容很像,用起来容易混。

使用委托表达式(Lambda)

假设一个场景:我们有一个订单列表,里面有售价和采购价。我们需要计算所有物品的毛利率。

  1. public class OrderDetails 
  2.     public int Id { get; set; } 
  3.     public string ItemName { get; set; } 
  4.     public double PurchasePrice { get; set; } 
  5.     public double SellingPrice { get; set; } 

通过迭代,我们可以计算出每个项目的毛利率:

  1. static void Main(string[] args) 
  2.     List<OrderDetails> lstOrderDetails = new List<OrderDetails>(); 
  3.  
  4.     lstOrderDetails.Add(new OrderDetails() { Id = 1, ItemName = "Item 1", PurchasePrice = 100, SellingPrice = 120 }); 
  5.     lstOrderDetails.Add(new OrderDetails() { Id = 2, ItemName = "Item 2", PurchasePrice = 800, SellingPrice = 1200 }); 
  6.     lstOrderDetails.Add(new OrderDetails() { Id = 3, ItemName = "Item 3", PurchasePrice = 150, SellingPrice = 150 }); 
  7.     lstOrderDetails.Add(new OrderDetails() { Id = 4, ItemName = "Item 4", PurchasePrice = 155, SellingPrice = 310 }); 
  8.     lstOrderDetails.Add(new OrderDetails() { Id = 5, ItemName = "Item 5", PurchasePrice = 500, SellingPrice = 550 }); 
  9.  
  10.     Func<doubledoubledouble> GetPercentageProfit = (purchasePrice, sellPrice) => (((sellPrice - purchasePrice) / purchasePrice) * 100); 
  11.  
  12.     foreach (var order in lstOrderDetails) 
  13.     { 
  14.         Console.WriteLine($"Item Name: {order.ItemName}, Profit(%) : {GetPercentageProfit(order.PurchasePrice, order.SellingPrice)} "); 
  15.     } 

例子中,我们创建了一个有5个商品的列表。我们还创建了一个委托表达式,并在循环中调用。

我们来看看这个委托表达式在IL中是什么样子:

图上能很清楚看到,Lambda被转换成了类。

等等,为什么lambda表达式被转成了类,而不是一个方法?

这里需要划重点。Lambda表达式,在IL中会被转为委托。而委托是一个类。关于委托为什么是一个类,可以去看上一篇。这儿知道结论就好。

所以,Lambda表达式会转成一个类,应该通过一个实例来使用。而这个实例是new出来的,所以是分配在堆上的。

另外,通过IL代码我们也知道,IL是使用虚方法callvirt来调用的这个表达式。

现在,我们知道了一件事:Lambda会被转成委托和类,由这个类的一个实例来使用。这个对象的生命周期必须由GC来处理。

使用局部函数(Local Function)

上面的示例代码,我们换成局部函数:

  1. static void Main(string[] args) 
  2.     List<OrderDetails> lstOrderDetails = new List<OrderDetails>(); 
  3.  
  4.     lstOrderDetails.Add(new OrderDetails() { Id = 1, ItemName = "Item 1", PurchasePrice = 100, SellingPrice = 120 }); 
  5.     lstOrderDetails.Add(new OrderDetails() { Id = 2, ItemName = "Item 2", PurchasePrice = 800, SellingPrice = 1200 }); 
  6.     lstOrderDetails.Add(new OrderDetails() { Id = 3, ItemName = "Item 3", PurchasePrice = 150, SellingPrice = 150 }); 
  7.     lstOrderDetails.Add(new OrderDetails() { Id = 4, ItemName = "Item 4", PurchasePrice = 155, SellingPrice = 310 }); 
  8.     lstOrderDetails.Add(new OrderDetails() { Id = 5, ItemName = "Item 5", PurchasePrice = 500, SellingPrice = 550 }); 
  9.  
  10.     double GetPercentageProfit(double purchasePrice, double sellPrice) 
  11.     { 
  12.         return (((sellPrice - purchasePrice) / purchasePrice) * 100); 
  13.     } 
  14.  
  15.     foreach (var order in lstOrderDetails) 
  16.     { 
  17.         Console.WriteLine($"Item Name: {order.ItemName}, Profit(%) : {GetPercentageProfit(order.PurchasePrice, order.SellingPrice)} "); 
  18.     } 

现在,我们在Main方法中放入了局部函数GetPercentageProfit。

我们再检查下IL里的代码:

没有新类,没有新对象,只是一个简单的函数调用。

此外,Lambda表达式和局部函数的一个重要区别是IL中的调用方式。调用局部函数用call,它比callvirt要快,因为它是存储在堆栈上的,而不是堆上。

通常我们不需要关注IL如何运作,但好的开发人员真的需要了解一些框架的内部细节。

call和callvert的区别在于,call不检查调用者实例是否存在,而且callvert总是在调用时检查,所以callvert不能调用静态类方法,只能调用实例方法。

还是上面的例子,这回我们用迭代器实现:

  1. static void Main(string[] args) 
  2.     List<OrderDetails> lstOrderDetails = new List<OrderDetails>(); 
  3.  
  4.     lstOrderDetails.Add(new OrderDetails() { Id = 1, ItemName = "Item 1", PurchasePrice = 100, SellingPrice = 120 }); 
  5.     lstOrderDetails.Add(new OrderDetails() { Id = 2, ItemName = "Item 2", PurchasePrice = 800, SellingPrice = 1200 }); 
  6.     lstOrderDetails.Add(new OrderDetails() { Id = 3, ItemName = "Item 3", PurchasePrice = 150, SellingPrice = 150 }); 
  7.     lstOrderDetails.Add(new OrderDetails() { Id = 4, ItemName = "Item 4", PurchasePrice = 155, SellingPrice = 310 }); 
  8.     lstOrderDetails.Add(new OrderDetails() { Id = 5, ItemName = "Item 5", PurchasePrice = 500, SellingPrice = 550 }); 
  9.  
  10.     var result = GetItemSellingPice(lstOrderDetails); 
  11.  
  12.     foreach (string s in result) 
  13.     { 
  14.         Console.WriteLine(s.ToString()); 
  15.     } 
  16.  
  17. private static IEnumerable<string> GetItemSellingPice(List<OrderDetails> lstOrderDetails) 
  18.     if (lstOrderDetails == null) throw new ArgumentNullException(); 
  19.  
  20.     foreach (var order in lstOrderDetails) 
  21.     { 
  22.         yield return ($"Item Name:{order.ItemName}, Selling Price:{order.SellingPrice}"); 
  23.     } 

我们将列表传递给GetItemSellingPice。我们在方法中检查了列表不能为null,并在循环中使用yield return返回数据。

代码看起来没问题,是吧?

那我们假设列表真的为空,会怎么样呢?应该会返回ArgumentNullException,预期是这样。

执行一下看看,实际不是这样。当我们使用迭代器时,方法并没有立即执行并返回异常,而是在我们使用结果foreach (string s in result)时,才执行并返回异常。这种情况,会让我们对于异常的判断和处理出现错误。

这时候,局部函数就是一个好的解决方式:

  1. static void Main(string[] args) 
  2.     var result = GetItemSellingPice(null); 
  3.  
  4.     foreach (string s in result) 
  5.     { 
  6.         Console.WriteLine(s.ToString()); 
  7.     } 
  8.  
  9. private static IEnumerable<string> GetItemSellingPice(List<OrderDetails> lstOrderDetails) 
  10.     if (lstOrderDetails == null) throw new ArgumentNullException(); 
  11.  
  12.     return GetItemPrice(); 
  13.  
  14.     IEnumerable<string> GetItemPrice() 
  15.     { 
  16.         foreach (var order in lstOrderDetails) 
  17.         { 
  18.             yield return ($"Item Name:{order.ItemName}, Selling Price:{order.SellingPrice}"); 
  19.         } 
  20.     } 

现在,我们正确地在第一时间得到异常。

总结

局部函数是一个非常强大的存在。它与Lambda表达式类似,但有更优的性能。

又是一个好东西,是吧?

 

责任编辑:武晓燕 来源: 老王Plus
相关推荐

2021-01-27 08:12:04

Dotnet函数数据

2021-03-10 07:20:44

数据定位匹配

2010-03-15 09:32:56

Python函数

2020-11-11 21:26:48

函数变量

2016-09-14 21:28:25

JavaScript事件代理委托

2009-08-20 18:11:08

C#异步委托

2009-09-08 15:28:24

C#委托

2011-06-16 15:14:17

VB.NET事件委托

2009-08-18 10:54:17

C#事件和委托

2011-03-23 17:11:21

Lampwamp

2021-03-03 08:13:57

模式垃圾回收

2009-08-20 18:37:52

委托C#异步委托

2010-09-25 13:20:22

DHCP BOOTP

2009-08-26 17:05:55

.NET委托

2009-10-20 16:48:30

C#委托

2022-03-11 09:22:55

令牌Dotnet线程

2009-06-11 11:07:25

Java局部内部类Final类型

2013-03-19 09:48:38

C#

2016-09-08 14:50:59

AndroidiPhoneiOS

2010-09-01 16:36:20

DHCPNAT
点赞
收藏

51CTO技术栈公众号