本地函数 vs Lambda C# 7.0

232
我正在研究C# 7.0 的新实现,发现它们实现了局部函数。但我无法想象一个局部函数比Lambda表达式更好的场景,并且两者之间有什么区别。
我理解Lambda是匿名函数,而局部函数不是,但我无法想出一个现实世界的场景,局部函数优于Lambda表达式。
非常感谢您提供任何示例。谢谢。

12
泛型、输出参数、无需初始化lambda即可进行递归函数等。 - Kirk Woll
5
@KirkWoll - 你应该将这个发言作为答案发布。 - Enigmativity
5个回答

347

这在C#设计会议笔记中被Mads Torgersen解释过,该会议首次讨论了局部函数:

你需要一个辅助函数。你只从单个函数内部使用它,它可能使用在包含函数中范围内的变量和类型参数。另一方面,与Lambda不同,您不需要将其作为第一类对象,因此您不关心为其分配委托类型并分配实际委托对象。您还可以将其实现为递归或通用函数,或者将其实现为迭代器。

更详细地说,其优点如下:

  1. 性能。

    创建Lambda时必须创建委托,但在本例中,这是不必要的分配。局部函数只是函数,不需要委托。

    此外,局部函数更有效地捕获局部变量:Lambda通常将变量捕获到类中,而局部函数可以使用结构体(使用ref传递),这样可以避免分配。

    这也意味着调用局部函数更便宜,并且它们可以内联,进一步提高性能。

  2. 局部函数可以是递归的。

    Lambda也可以是递归的,但它需要笨拙的代码,其中您首先将null分配给委托变量,然后再分配Lambda。局部函数可以自然地递归(包括相互递归)。

  3. 局部函数可以是通用的。

    Lambda不能是通用的,因为它们必须分配给具有具体类型的变量(该类型可以使用来自外部作用域的通用变量,但这不是同一件事)。

  4. 局部函数可以实现为迭代器。

    Lambda不能使用yield return(和yield break)关键字来实现返回IEnumerable<T>的函数。局部函数可以。

  5. 局部函数看起来更好。

    这在上面的引用中没有提到,可能只是我的个人偏见,但我认为普通函数语法比将Lambda分配给委托变量要好看。局部函数也更简洁。

    比较:

    int add(int x, int y) => x + y;
    Func<int, int, int> add = (x, y) => x + y;
    

38
我想补充一点,局部函数在调用方有参数名称,而 Lambda 表达式没有。 - Lensflare
3
@Lensflare 是的,Lambda表达式的参数名称不会被保留,这是因为它们必须转换为委托类型,而委托类型有它们自己的名称。例如:Func<int, int, int> f = (x, y) => x + y; f(arg1:1, arg2:1); - svick
1
很棒的列表!然而,如果委托的使用遵循特定规则,我可以想像IL/JIT编译器如何执行1中提到的所有优化。 - Marcin Kaczmarek
1
@svick 很棒的回答!有一个问题:什么情况下会选择lambda而不是本地函数? - happybits
1
@Abdul 如果本地函数和方法相同,则它们之间不应该有性能差异。但是,如果本地函数捕获变量而不是将其作为参数接受,则可能会有微小的差异。 - svick
显示剩余6条评论

111

除了 svick很好的回答 之外,本地函数还有一个优点:
它们可以在函数中的任何位置定义,甚至在return语句之后。

public double DoMath(double a, double b)
{
    var resultA = f(a);
    var resultB = f(b);
    return resultA + resultB;

    double f(double x) => 5 * x + 3;
}

7
这真的很有用,因为我可以习惯于将所有辅助函数放在函数底部的 #region Helpers 中,以避免在该函数内产生混乱,尤其是避免在主类中出现混乱。 - AustinWBryan
1
我也很欣赏这一点。这使得你正在查看的主要函数更易于阅读,因为你不需要四处寻找它的起始位置。如果你想要查看实现细节,请继续查找到结尾之后。 - Remi Despres-Smyth
13
如果你的函数如此庞大,以至于它们需要使用区域变量,那么它们就太大了。 - ssmith
1
@ssmith 不一定。现代 C# 特性使您可以使用非面向对象的技术编写代码——即以函数式方式或甚至是老派的过程式风格,而实现它的一种方法是将整个应用程序的主体放入一个静态的 Main() 方法中。坦白地说,这就是 C# 9 中顶级语句在幕后执行的操作。 - Ruslan
@Ruslan 是的,我会坚持我的原话。是的,你可以使用顶级语句创建1000多行的main()方法。但这并不意味着你应该这样做。例如,这个程序只有“仅仅”540行,但对于我来说,它仍然是一团糟,很难找到其中的任何东西。https://github.com/DamianEdwards/MinimalApiPlayground/blob/main/src/MinimalApiPlayground/Program.cs - ssmith
虽然不是真的,但仍然很酷。它们仍然需要在使用它们的作用域块中(否则它们就不会被称为局部变量)。 - Paul Childs

14
如果您想知道如何测试本地函数,可以检查JustMock,它具有此功能。以下是将被测试的简单类示例:
public class Foo // the class under test
{ 
    public int GetResult() 
    { 
        return 100 + GetLocal(); 
        int GetLocal () 
        { 
            return 42; 
        } 
    } 
}

这是测试的样子:

[TestClass] 
public class MockLocalFunctions 
{ 
    [TestMethod] 
    public void BasicUsage() 
    { 
        //Arrange 
        var foo = Mock.Create<Foo>(Behavior.CallOriginal); 
        Mock.Local.Function.Arrange<int>(foo, "GetResult", "GetLocal").DoNothing(); 

        //Act 
        var result = foo. GetResult(); 

        //Assert 
        Assert.AreEqual(100, result); 
    } 
} 

这里是 JustMock 的文档链接

声明:我是 JustMock 的开发人员之一。


5

我很好奇本地函数比lambda表达式快多少,因此我写了一个小基准测试:

[Benchmark]
public void TestLambda()
{
    Func<int, int> _Square = (x) => x * x;
    _Square(123);
}

[Benchmark]
public void TestLocalFunc()
{
    int _Square(int x) => x * x;
    _Square(123);
}

结果让我惊呆了。

快了4个数量级!

|        Method |      Mean |     Error |    StdDev |    Median | Allocated |
|-------------- |----------:|----------:|----------:|----------:|----------:|
|    TestLambda | 1.4949 ns | 0.1997 ns | 0.0109 ns | 1.4898 ns |         - |
| TestLocalFunc | 0.0008 ns | 0.0237 ns | 0.0013 ns | 0.0000 ns |         - |

// * Warnings *
ZeroMeasurement
  BenchMark.TestLocalFunc: ShortRun -> The method duration is indistinguishable from the empty method duration

0
我使用内联函数来避免垃圾回收压力,特别是在处理长时间运行的方法时。比如说,如果想要获取给定股票代码的2年市场数据,也可以打包大量功能和业务逻辑。
所做的就是打开一个套接字连接到服务器,并循环绑定事件。可以将其视为类的设计方式,只是不会在各个地方编写仅适用于一个功能的帮助方法。下面是一些示例,注意我使用变量和“helper”方法在最后。在Finally中,我很好地删除了事件处理程序,如果我的Exchange类是外部/注入的,我将没有任何未决的事件处理程序注册。
void List<HistoricalData> RequestData(Ticker ticker, TimeSpan timeout)
{
    var socket= new Exchange(ticker);
    bool done=false;
    socket.OnData += _onData;
    socket.OnDone += _onDone;
    var request= NextRequestNr();
    var result = new List<HistoricalData>();
    var start= DateTime.Now;
    socket.RequestHistoricalData(requestId:request:days:1);
    try
    {
      while(!done)
      {   //stop when take to long….
        if((DateTime.Now-start)>timeout)
           break;
      }
      return result;

    }finally
    {
        socket.OnData-=_onData;
        socket.OnDone-= _onDone;
    }


   void _OnData(object sender, HistoricalData data)
   {
       _result.Add(data);
   }
   void _onDone(object sender, EndEventArgs args)
   {
      if(args.ReqId==request )
         done=true;
   } 
}

您可以看到以下提到的优点,这里有一个示例实现。希望这有助于解释其好处。


2
  1. 这是一个非常复杂的例子和解释,只是为了演示本地函数。
  2. 在这个例子中,与lambda相比,本地函数并没有避免任何分配,因为它们仍然需要转换为委托。所以我不明白它们如何避免GC。
- svick
2
不需要传递/复制变量,svick的答案已经很好地涵盖了其余部分。不需要重复他的答案。 - Walter Verhoeven

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接