Local functions in C#

Today we will talk about native functions in C#. Native functions were introduced in C# 7.0 and improved in C# 8.0 and C# 9.0.

Reasons for introducing local functions

Let's take a look at a passage from Mads Torgersen, the chief designer of Microsoft's C# language:

Mads Torgersen:
We think this scenario is useful-you need a helper function. You can only use it in a single function, and it may use variables and type parameters contained in the scope of the function. On the other hand, unlike lambda, you don't need to treat it as a first-class object, so you don't have to care about providing it with a delegate type and assigning an actual delegate object. In addition, you may want it to be recursive or generic, or implement it as an iterator. 1

It is this reason that Mads Torgersen said that allowed the C# language team to add support for native functions.
I have used local functions many times in recent projects and found that it is more convenient and clearer than using delegates and Lambda expressions.

What is a local function

In the simplest vernacular, the local function is the method in the method . Can you understand it all at once? However, this understanding of local functions is inevitably a bit one-sided and superficial.

Let's take a look at the official definition of local functions:

A local function is a private method nested in another member, and it can only be called from the member that contains it. 2

Three key points are pointed out in the definition:

  1. Local functions are private methods .
  2. Local functions are methods nested in another member .
  3. It can only be called from the member that defines the local function , not elsewhere.

Among them, the members that can declare and call local functions are as follows:

  • Methods, especially iterator methods and asynchronous methods
  • Constructor
  • Property accessor
  • Event accessor
  • Anonymous method
  • Lambda expression
  • Destructor
  • Other local functions

As a simple example, in the process of Mdefining a local function add:

public class C
{
    
    
    public void M()
    {
    
    
        int result = add(100, 200);
        // 本地函数 add
        int add(int a, int b) {
    
     return a + b; }
    }
}

Local private functions are currently available only modifiers async, , unsafe(staticstatic local function can not access the local variables and instance members) and externfour. All local variables and method parameters defined in the containing member can be accessed in non-static local functions. Local functions can be declared in any position which contains members, but the common practice is to include a statement in its final position members (ie end }before).

Comparison of local functions and lambda expressions

The local function is very similar to the well-known Lambda expression 3. For example, the local function in the above example, we can use the Lambda expression to achieve the following:

public void M()
{
    
    
    // Lambda 表达式
    Func<int, int, int> add = (int a, int b) => a + b;
    int result = add(100, 200);
}

In this way, it seems that the choice to use Lambda expressions or local functions is just a matter of coding style and personal preference. However, it should be noted that the timing and conditions for using them are actually quite different.

Let's take a look at an example of obtaining the nth term of the Fibonacci sequence. Its implementation includes recursive calls.

// 使用本地函数的版本
public static uint LocFunFibonacci(uint n)
{
    
    
    return Fibonacci(n);

    uint Fibonacci(uint num)
    {
    
    
        if (num == 0) return 0;
        if (num == 1) return 1;
        return checked(Fibonacci(num - 2) + Fibonacci(num - 1));
    }
}
// 使用 Lambda 表达式的版本
public static uint LambdaFibonacci(uint n)
{
    
    
    Func<uint, uint> Fibonacci = null; //这里必须明确赋值
    Fibonacci = num => {
    
    
        if (num == 0) return 0;
        if (num == 1) return 1;
        return checked(Fibonacci(num - 2) + Fibonacci(num - 1));
    };

    return Fibonacci(n);
}

name

The naming of local functions is similar to the methods in the class, and the process of declaring local functions is like writing ordinary methods. Lambda expressions is an anonymous method, you need to assign a delegate type of variable, usually Actionor Functypes of variables.

Parameters and return value types

Because the syntax of local functions is similar to that of ordinary methods, the parameter types and return value types are already part of the function declaration. Lambda expressions dependent on the assigned Actionor Functhe type of a variable to determine the type of the parameter and return value.

Definite assignment

Native functions are methods defined at compile time . Since local functions are not assigned to variables, they can be called from any code location of the member that contains it. In this example, we will local functions Fibonaccidefined in the method comprising LocFunFibonaccithe returnfollowing statement, the end of the method body }before, without any compilation errors.

The Lambda expressions are declared and allocated at runtime object. When using Lambda expressions, it must first be clear assignment: to be assigned to its statement Actionor Funcvariable, and assign Lambda expressions before calling them later in your code. In this example, we first declare and initialize a delegate variable Fibonacci, and then assign the Lambda expression to the delegate variable.

These differences mean that it is easier to create recursive algorithms using local functions . Because when creating a recursive algorithm, using a local function is the same as using a normal method; while using a Lambda expression, you must declare and initialize a delegate variable before it can be reassigned to the subject that refers to the same Lambda expression.

Variable capture

When we use VS to write or compile code, the compiler can perform static analysis on the code and inform us of problems in the code in advance.

Consider the following example:

static int M1()
{
    
    
    int num; //这里不用赋值默认值
    LocalFunction();
    return num; //OK
    void LocalFunction() => num = 8; // 本地函数
}

static int M2()
{
    
    
    int num;    //这里必须赋值默认值(比如改为:int num = 0;),下面使用 num 的行才不会报错
    Action lambdaExp = () => num = 8; // Lambda 表达式
    lambdaExp();
    return num; //错误 CS0165 使用了未赋值的局部变量“num”
}

When using a local function, because local functions are defined at compile time , the compiler can determine at a local function call LocalFunctionexplicitly assigned time num. Because it is called before the return statement LocalFunction, it is explicitly allocated before the return statement num, so no compilation exception will be raised.
When using Lambda expressions, because Lambda expressions are declared and allocated at runtime , the compiler cannot determine whether they are allocated before the return statement num, so a compilation exception will be raised.

Memory allocation

In order to better understand the difference between local functions and Lambda expressions in allocation, let's take a look at the following two examples first and take a look at their compiled code.

Lambda expression:

public class C
{
    
    
    public void M()
    {
    
    
        int c = 300;
        int d = 400;
        int num = c + d;
        //Lambda 表达式
        Func<int, int, int> add = (int a, int b) => a + b + c + d;
        var num2 = add(100, 200);
    }
}

Using Lambda expressions, the compiled code is as follows :

public class C
{
    
    
    [CompilerGenerated]
    private sealed class <>c__DisplayClass0_0
    {
    
    
        public int c;

        public int d;

        internal int <M>b__0(int a, int b)
        {
    
    
            return a + b + c + d;
        }
    }

    public void M()
    {
    
    
        <>c__DisplayClass0_0 <>c__DisplayClass0_ = new <>c__DisplayClass0_0();
        <>c__DisplayClass0_.c = 300;
        <>c__DisplayClass0_.d = 400;
        int num = <>c__DisplayClass0_.c + <>c__DisplayClass0_.d;
        Func<int, int, int> func = new Func<int, int, int>(<>c__DisplayClass0_.<M>b__0);
        int num2 = func(100, 200);
    }
}

It can be seen that when using Lambda expressions, after compilation, a class containing implementation methods is actually generated, and then an object of this class is created and assigned to the delegate. Because objects of the class are to be created, additional heap allocation is required.

Let's take a look at the local function implementation with the same function:

public class C
{
    
    
    public void M()
    {
    
    
        int c = 300;
        int d = 400;
        int num = c + d;
        var num2 = add(100, 200);
        //本地函数
        int add(int a, int b) {
    
     return a + b + c + d; }
    }
}

Using native functions, the compiled code is as follows :

public class C
{
    
    
    [StructLayout(LayoutKind.Auto)]
    [CompilerGenerated]
    private struct <>c__DisplayClass0_0
    {
    
    
        public int c;

        public int d;
    }

    public void M()
    {
    
    
        <>c__DisplayClass0_0 <>c__DisplayClass0_ = default(<>c__DisplayClass0_0);
        <>c__DisplayClass0_.c = 300;
        <>c__DisplayClass0_.d = 400;
        int num = <>c__DisplayClass0_.c + <>c__DisplayClass0_.d;
        int num2 = <M>g__add|0_0(100, 200, ref <>c__DisplayClass0_);
    }

    [CompilerGenerated]
    private static int <M>g__add|0_0(int a, int b, ref <>c__DisplayClass0_0 P_2)
    {
    
    
        return a + b + P_2.c + P_2.d;
    }
}

It can be seen that when using a native function, only a private method is generated in the containing class after compilation, so there is no need to instantiate the object when calling, and no additional heap (heap) allocation is required.
When a variable in its containing member is used in a local function, the compiler generates a structure and passes an instance of this structure refto the local function by reference ( ), which also helps to save memory allocation.

In summary, using local functions can save time and space more than using Lambda expressions.

Paradigms and iterators

Local functions support paradigms, just like ordinary methods; Lambda expressions do not support paradigms, because they must be assigned to a delegate variable with a specific type (they can use external paradigm variables in the scope, but that is not One thing). 4

Local functions can be implemented as an iterative; and Lambda expressions can not be used yield returnand yield breakkeywords to achieve return IEnumerable<T>function.

Local functions and exceptions

Another useful feature of local functions is that exceptions can be displayed immediately in iterator methods and asynchronous methods.

We know that the main body of the iterator method is executed lazily, so the exception is only displayed when enumerating the returned sequence, not when the iterator method is called.
Let's look at an example of a classic iterator method:

static void Main(string[] args)
{
    
    
    int[] list = new[] {
    
     1, 2, 3, 4, 5, 6 };
    var result = Filter(list, null);

    Console.WriteLine(string.Join(',', result));
}

public static IEnumerable<T> Filter<T>(IEnumerable<T> source, Func<T, bool> predicate)
{
    
    
    if (source == null) throw new ArgumentNullException(nameof(source));
    if (predicate == null) throw new ArgumentNullException(nameof(predicate));

    foreach (var element in source)
        if (predicate(element))
            yield return element;
}

Running the above code, since the main body iterator method is to delay the execution, the exception thrown will occur in the position of string.Join(',', result)the row is located, is returned in the sequence enumeration result resultis displayed, as shown:

exception_1

If we take the above method iterator Filteriterator section into the local function:

static void Main(string[] args)
{
    
    
    int[] list = new[] {
    
     1, 2, 3, 4, 5, 6 };
    var result = Filter(list, null);

    Console.WriteLine(string.Join(',', result));
}

public static IEnumerable<T> Filter<T>(IEnumerable<T> source, Func<T, bool> predicate)
{
    
    
    if (source == null) throw new ArgumentNullException(nameof(source));
    if (predicate == null) throw new ArgumentNullException(nameof(predicate));
    //本地函数
    IEnumerable<T> Iterator()
    {
    
    
        foreach (var element in source)
            if (predicate(element))
                yield return element;
    }
    return Iterator();
}

So then thrown location will take place in Filter(list, null)line is located, it is calling Filterupon the method displayed, as shown:

exception_2

It can be seen that the use of local functions to wrap the iterator logic is equivalent to bringing the position of the display exception in advance, which helps us to observe the exception and deal with it more quickly.

Similarly, using the asyncasynchronous method, if the asynchronous execution portion into asynclocal function, also help to show abnormal immediately. Due to space issues, I will not give an example here, you can check the official document .

to sum up

In summary, a local function is a method in a method , but it is not only a method in a method , it can also appear in members such as constructors, property accessors, event accessors, etc.; local functions are similar in function It is similar to Lambda expressions, but it is more convenient and clearer than Lambda expressions, and has a slight advantage over Lambda expressions in terms of allocation and performance; local functions support paradigms and are implemented as iterators; local functions also help in iterators Exceptions are displayed immediately in methods and asynchronous methods.


Author: Technical Zemin
Publisher: Technical Translation Station

Public Number: Technical Translation Station


  1. https://github.com/dotnet/roslyn/issues/3911 C# Design Meeting Notes ↩︎

  2. https://docs.microsoft.com/zh-cn/dotnet/csharp/programming-guide/classes-and-structs/local-functions local function ↩︎

  3. https://docs.microsoft.com/zh-cn/dotnet/csharp/language-reference/operators/lambda-expressions the Lambda expressions ↩︎

  4. https://stackoverflow.com/questions/40943117/local-function-vs-lambda-c-sharp-7-0 Local function vs Lambda C# 7.0 ↩︎

Guess you like

Origin blog.csdn.net/weixin_47498376/article/details/110089347