Local function syntax
Local functions are defined as nested methods that contain members. Its definition has the following syntax:
<modifiers> <return-type> <method-name> <parameter-list>
You can use the following modifiers for local functions:
async
unsafe
static (in C# 8.0 and later). static local functions cannot capture local variables or instance states.
extern (in C# 9.0 and later). External local functions must be static.
All local variables defined in the containing member, including its method parameters, are accessible in non static local functions
Unlike method definitions, local function definitions cannot contain member access modifiers. Because all local functions are private, including access modifiers such as
Private keyword) will generate compiler error CS0106 "the modifier 'private' is invalid for this item".
The following example defines a local function named AppendPathSeparator, which is private to the method named GetText:
private static string GetText(string path, string filename)
{
var reader = File.OpenText($"{AppendPathSeparator(path)}{filename}");
var text = reader.ReadToEnd();
return text;
string AppendPathSeparator(string filepath)
{
return filepath.EndsWith(@"\") ? filepath : filepath + @"\";
}
}
Starting with C# 9.0, you can apply features to local functions, their parameters, and type parameters, as shown in the following example:
#nullable enable
private static void Process(string?[] lines, string mark)
{
foreach (var line in lines)
{
if (IsValid(line))
{
// Processing logic...
}
}
bool IsValid([NotNullWhen(true)] string? line)
{
return !string.IsNullOrEmpty(line) && line.Length >= mark.Length;
}
}
The previous example uses special properties to help the compiler perform static analysis in a nullable context.
Local functions and exceptions
Starting with C# 9.0, you can apply properties to local functions, their parameters, and type parameters, as shown in the following example:
The previous example uses special properties to help the compiler perform static analysis in a nullable context.
A practical feature of local functions is to allow exceptions to be displayed immediately. For method iterators, exceptions are displayed only when enumerating the returned sequences, not
When retrieving iterators. For asynchronous methods, any exceptions thrown in the asynchronous method are observed while waiting for the returned task.
The following example defines the OddSequence method to enumerate odd numbers in a specified range. Because it will pass a number greater than 100 to
OddSequence iterator method that will throw ArgumentOutOfRangeException. As the output in the example shows, only if the loop is accessed
The exception is displayed only when the number is, not when the iterator is retrieved.
using System;
using System.Collections.Generic;
public class IteratorWithoutLocalExample
{
public static void Main()
{
IEnumerable<int> xs = OddSequence(50, 110);
Console.WriteLine("Retrieved enumerator...");
foreach (var x in xs) // line 11
{
Console.Write($"{x} ");
}
}
public static IEnumerable<int> OddSequence(int start, int end)
{
if (start < 0 || start > 99)
throw new ArgumentOutOfRangeException(nameof(start), "start must be between 0 and 99.");
if (end > 100)
throw new ArgumentOutOfRangeException(nameof(end), "end must be less than or equal to 100.");
if (start >= end)
throw new ArgumentException("start must be less than end.");
for (int i = start; i <= end; i++)
{
if (i % 2 == 1)
yield return i;
}
}
}
// The example displays the output like this:
//
// Retrieved enumerator...
// Unhandled exception. System.ArgumentOutOfRangeException: end must be less than or equal to 100.
(Parameter 'end')
// at IteratorWithoutLocalExample.OddSequence(Int32 start, Int32 end)+MoveNext() in
IteratorWithoutLocal.cs:line 22
// at IteratorWithoutLocalExample.Main() in IteratorWithoutLocal.cs:line 11
If the iterator logic is put into a local function, a parameter validation exception will be thrown when retrieving the enumerator, as shown in the following example:
using System;
using System.Collections.Generic;
public class IteratorWithLocalExample
{
public static void Main()
{
IEnumerable<int> xs = OddSequence(50, 110); // line 8
Console.WriteLine("Retrieved enumerator...");
foreach (var x in xs)
{
Console.Write($"{x} ");
}
}
public static IEnumerable<int> OddSequence(int start, int end)
{
if (start < 0 || start > 99)
throw new ArgumentOutOfRangeException(nameof(start), "start must be between 0 and 99.");
if (end > 100)
throw new ArgumentOutOfRangeException(nameof(end), "end must be less than or equal to 100.");
if (start >= end)
throw new ArgumentException("start must be less than end.");
return GetOddSequenceEnumerator();
IEnumerable<int> GetOddSequenceEnumerator()
{
for (int i = start; i <= end; i++)
{
if (i % 2 == 1)
yield return i;
}
}
}
}
// The example displays the output like this:
//
// Unhandled exception. System.ArgumentOutOfRangeException: end must be less than or equal to 100.
(Parameter 'end')
// at IteratorWithLocalExample.OddSequence(Int32 start, Int32 end) in IteratorWithLocal.cs:line 22
// at IteratorWithLocalExample.Main() in IteratorWithLocal.cs:line 8
Local functions and Lambda expressions
public static int LocalFunctionFactorial(int n)
{
return nthFactorial(n);
int nthFactorial(int number) => number < 2
? 1
: number * nthFactorial(number - 1);
}
At first glance, native functions are very similar to lambda expressions. In many cases, choosing whether to use lambda expressions or native functions is the style
And personal preferences. However, it should be noted that there are differences in the timing and conditions for choosing one of the two.
Let's examine the difference between the local function implementation of factorial algorithm and the lambda expression implementation. The following is the version using local functions:
public static int LambdaFactorial(int n)
{
Func<int, int> nthFactorial = default(Func<int, int>);
nthFactorial = number => number < 2
? 1
: number * nthFactorial(number - 1);
return nthFactorial(n);
}
name
Local functions are named the same way. Lambda expression is an anonymous method that needs to be assigned to a variable of type delegate, usually
Action or Func type. When declaring a local function, this process is similar to writing a common method; Declare a return type and a function signature.
Function signature and Lambda expression type
Lambda expressions depend on the type of Action / Func variables assigned to them to determine the parameters and return types. In the local function, because of the syntax
It is very similar to writing a regular method, so the parameter type and return type are already part of the function declaration.
definitely assigned
Lambda expressions are objects declared and allocated at run time. To use a lambda expression, you need to assign an explicit value to it: you must declare that you want to
The Action / Func variable assigned to it and a Lambda expression assigned to it. Note that LambdaFactorial must be declared and initialized first
Lambda expression nthFactorial, and then define it. Otherwise, a compile time error will occur when referring to nthFactorial before allocation
Wrong.
Local functions are defined at compile time. Since they are not assigned to variables, they can be referenced from any code location within the scope; In the first example
In local function factorial, we can declare local functions above or below the return statement without triggering any compiler errors
Wrong.
These differences mean that it is easier to create recursive algorithms using local functions. You can declare and define a local function that calls itself. Must declare
Lambda expression, which is assigned to the default value before it can be reassigned to the body that references the same lambda expression.
Implement as delegate
Lambda expressions are converted to delegates when declared. Local functions are more flexible and can be written like traditional methods or as delegates. only
Local functions are converted to delegates only when they are used as delegates.
If a local function is declared, but it is only referenced by calling it like a method, it will not be converted into a delegate
Variable capture
Explicit allocation rules also affect any variables captured by local functions or Lambda expressions. The compiler can perform static analysis, so local functions
The number can explicitly allocate captured variables within a closed range. See the following example:
int M()
{
int y;
LocalFunction();
return y;
void LocalFunction() => y = 0;
}
The compiler can determine that the localfunction is explicitly assigned y when called. Because LocalFunction was called before the return statement, so
y is explicitly assigned in the return statement.
Note that when a local function captures a variable in an enclosing scope, the local function is implemented as a delegate type.
Heap allocation
Depending on their purpose, native functions can avoid the heap allocation that Lambda expressions always need. If the local function will never be converted to a delegate, and
And the variables captured by the local function will not be captured by other lambda or local functions converted to delegates, the compiler can avoid heap allocation.
Consider the following asynchronous example:
public async Task<string> PerformLongRunningWorkLambda(string address, int index, string name)
{
if (string.IsNullOrWhiteSpace(address))
throw new ArgumentException(message: "An address is required", paramName: nameof(address));
if (index < 0)
throw new ArgumentOutOfRangeException(paramName: nameof(index), message: "The index must be nonnegative");
if (string.IsNullOrWhiteSpace(name))
throw new ArgumentException(message: "You must supply a name", paramName: nameof(name));
Func<Task<string>> longRunningWorkImplementation = async () =>
{
var interimResult = await FirstWork(address);
var secondResult = await SecondStep(index, name);
return $"The results are {interimResult} and {secondResult}. Enjoy.";
};
return await longRunningWorkImplementation();
}
The closure of this lambda expression contains the address, index, and name variables. In the case of local functions, the object implementing the closure may be
struct type. The structure type will be passed to the local function by reference. This difference in the implementation will be saved on the allocation.
The instantiation required for Lambda expressions means additional memory allocation, which may be a performance factor in the time critical code path. Local function does not
This overhead will arise. In the above example, the native function version has 2 fewer allocations than the Lambda expression version.
If you know that local functions will not be converted to delegates, and the variables captured by local functions will not be converted to delegate lambda or local functions
Capture, you can ensure that local functions are not allocated on the heap by declaring them static local functions. Please note that this function is in C#
8.0 and later.
be careful:
Local functions equivalent to this method also use classes for closures. Implementation details include whether the closure of the local function is implemented as class or struct. Local letter
The number may use struct, while lambda will always use class.
public async Task<string> PerformLongRunningWork(string address, int index, string name)
{
if (string.IsNullOrWhiteSpace(address))
throw new ArgumentException(message: "An address is required", paramName: nameof(address));
if (index < 0)
throw new ArgumentOutOfRangeException(paramName: nameof(index), message: "The index must be nonnegative");
if (string.IsNullOrWhiteSpace(name))
throw new ArgumentException(message: "You must supply a name", paramName: nameof(name));
return await longRunningWorkImplementation();
async Task<string> longRunningWorkImplementation()
{
var interimResult = await FirstWork(address);
var secondResult = await SecondStep(index, name);
return $"The results are {interimResult} and {secondResult}. Enjoy.";
}
}
Usage of yield keyword
A final advantage that has not been demonstrated in this example is that local functions can be implemented as iterators, generating a series of values using the yield return syntax.
public IEnumerable<string> SequenceToLowercase(IEnumerable<string> input)
{
if (!input.Any())
{
throw new ArgumentException("There are no items to convert to lowercase.");
}
return LowercaseIterator();
IEnumerable<string> LowercaseIterator()
{
foreach (var output in input.Select(item => item.ToLower()))
{
yield return output;
}
}
}
yield return statements are not allowed in Lambda expressions. See compiler error CS1621.
Although local functions may be a little redundant to lambda expressions, in fact, their purpose and usage are different. If you want to write only from the context
Or the functions invoked in other methods, it is more efficient to use local functions.