11

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

 3 years ago
source link: https://mp.weixin.qq.com/s?__biz=MjM5MjQwMDUzMw%3D%3D&%3Bmid=2247484237&%3Bidx=1&%3Bsn=1ed8bfa6ae6a6eda8782305d228d1af7
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

上一篇说了一下委托,这篇来说说局部函数和委托的对比。

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

需要了解委托相关内容,可以看这一篇 【传送门】

使用委托表达式(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中是什么样子:

vYfi6zQ.png!mobile

图上能很清楚看到,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里的代码:

NbInqiY.png!mobile

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

此外,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表达式类似,但有更优的性能。

又是一个好东西,是吧?


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK