Локальные функции (руководство по программированию на C#)
Локальные функции — это методы типа, вложенного в другой член. Они могут вызываться только из того элемента, в который вложены. Ниже перечислены элементы, в которых можно объявлять и из которых можно вызывать локальные функции:
- Методы, в частности методы итератора и асинхронные методы
- Конструкторы
- Методы доступа к свойствам
- Методы доступа событий
- Анонимные методы
- Лямбда-выражения
- Методы завершения
- Другие локальные функции
Тем не менее локальные функции нельзя объявлять внутри элемента, воплощающего выражение.
Примечание.
В некоторых случаях для реализации возможностей, поддерживаемых локальными функциями, также можно использовать лямбда-выражения. Дополнительные сведения см. в разделе Локальные функции или лямбда-выражения.
Применение локальных функций позволяет сделать предназначение кода более понятным. Другие пользователи, читающие ваш код, смогут видеть, что соответствующий метод вызывается только внутри того метода, в который он вложен. В случае с командными проектами это также гарантирует, что другой разработчик не сможет ошибочно вызвать метод напрямую из любого другого места в классе или структуре.
Синтаксис локальной функции
Локальная функция определяется как вложенный метод внутри содержащего ее элемента. Ниже приведен синтаксис определения локальной функции:
<modifiers> <return-type> <method-name> <parameter-list>
Примечание.
Не <parameter-list>
должен содержать параметры с именем контекстного ключевого словаvalue
.
Компилятор создает временную переменную "value", которая содержит ссылки на внешние переменные, которые позже вызывают неоднозначность, а также может привести к неожиданному поведению.
С локальной функцией можно использовать следующие модификаторы:
async
unsafe
static
Статическую локальную функцию нельзя записывать локальные переменные или состояние экземпляра.extern
Должна бытьstatic
внешняя локальная функция.
Все локальные переменные, определенные в содержащем функцию элементе (включая параметры метода), доступны в нестатической локальной функции.
В отличие от определения метода, определение локальной функции не может содержать модификатор доступа к элементу. Поскольку все локальные функции являются частными, при использовании модификатора доступа (например, ключевого слова private
) возникает ошибка компилятора CS0106, "Модификатор "private" недопустим для этого элемента".
В следующем примере определяется локальная функция AppendPathSeparator
, которая является частной для метода 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 + @"\";
}
}
Атрибуты можно применить к локальной функции, его параметрам и параметрам типа, как показано в следующем примере:
#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;
}
}
В предыдущем примере используется специальный атрибут для помощи компилятору в статическом анализе в контексте, допускающем значение NULL.
Локальные функции и исключения
Полезной особенностью локальных функций является то, что они допускают немедленную обработку исключений. Для методов итератора исключения отображаются только при перечислении возвращаемой последовательности, а не при извлечении итератора. В случае с асинхронными методами любые исключения, возникшие в таком методе, наблюдаются в тот момент, когда возвращаемая задача находится в состоянии ожидания.
В следующем примере определяется метод OddSequence
, который перечисляет нечетные числа в заданном диапазоне. Поскольку он передает в метод перечислителя OddSequence
число больше 100, этот метод вызывает исключение ArgumentOutOfRangeException. Как видно из выходных данных этого примера, исключение обрабатывается только в момент перебора чисел, а не при извлечении перечислителя.
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
Если поместить логику итератора в локальную функцию, при получении перечислителя вызываются исключения проверки аргументов, как показано в следующем примере:
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
Локальные функции или лямбда-выражения
На первый взгляд, локальные функции и лямбда-выражения во многом похожи. Во многих случаях выбор между лямбда-выражениями и локальными функциями является вопросом стиля и личного предпочтения. Однако существуют реальные различия в использовании этих сущностей, о которых нужно знать.
Рассмотрим различия в реализации алгоритма вычисления факториала с использованием локальной функции и лямбда-выражения. В этой версии используется локальная функция:
public static int LocalFunctionFactorial(int n)
{
return nthFactorial(n);
int nthFactorial(int number) => number < 2
? 1
: number * nthFactorial(number - 1);
}
В этой версии используются лямбда-выражения:
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);
}
Именование
Локальные функции явно именуются как методы. Лямбда-выражения представляют собой анонимные методы и должны назначаться переменным типа delegate
, как правило, типа Action
или Func
. Процесс объявления локальной функции аналогичен написанию обычного метода: вы объявляете тип возвращаемого значения и сигнатуру функции.
Сигнатуры функций и типы лямбда-выражений
Лямбда-выражения используют тип переменной Action
/Func
, которой они назначаются, для определения типов аргументов и возвращаемых значений. Поскольку синтаксис локальных функций во многом аналогичен обычному методу, типы аргументов и возвращаемых значений уже входят в объявление функции.
Начиная с C# 10, некоторые лямбда-выражения имеют естественный тип, который позволяет компилятору определить тип возвращаемого значения и типы параметров лямбда-выражения.
Определенное назначение
Лямбда-выражения — это объекты, которые объявляются и назначаются во время выполнения. Чтобы использовать лямбда-выражение, его необходимо определенно назначить: для этого необходимо объявить переменную Action
/Func
, которой оно будет назначено, и назначить ей лямбда-выражение. Обратите внимание на то, что LambdaFactorial
должно объявить и инициализировать лямбда-выражение nthFactorial
, прежде чем его определить. В противном случае возникает ошибка компилятора, связанная со ссылкой на объект nthFactorial
, который еще не был назначен.
Локальные функции определяются во время компиляции. Они не назначаются переменным, в связи с чем на них можно ссылаться из любого расположения кода, в области действия которого они находятся. В первом примере LocalFunctionFactorial
мы можем объявить локальную функцию выше или ниже инструкции return
, и это не приведет к ошибкам компилятора.
Эти различия означают, что рекурсивные алгоритмы легче создавать, используя локальные функции. Можно объявить и определить локальную функцию, которая вызывает саму себя. Необходимо объявить лямбда-выражения и назначить им значение по умолчанию, прежде чем их можно будет переназначить телу, которое ссылается на то же лямбда-выражение.
Реализация в виде делегата
Лямбда-выражения преобразуются в делегаты при объявлении. Локальные функции являются более гибкими и могут определяться в виде традиционного метода или делегата. Локальные функции преобразуются в делегаты только при использовании в качестве делегата.
Если объявить локальную функцию и сослаться на нее только путем вызова этой функции в качестве метода, она не будет преобразована в делегат.
Захват переменной
Правила определенного назначения также влияют на любые переменные, захватываемые локальной функцией или лямбда-выражением. Компилятор может выполнять статический анализ, что позволяет локальным функциям определенно назначать захватываемые переменные во включающей области. Рассмотрим следующий пример:
int M()
{
int y;
LocalFunction();
return y;
void LocalFunction() => y = 0;
}
Компилятор может определить, что LocalFunction
определенно назначает y
при вызове. Поскольку LocalFunction
вызывается перед оператором return
, y
определенно назначается в операторе return
.
Обратите внимание, что если локальная функция захватывает переменные в заключающей области, локальная функция реализуется с помощью закрытия, например типы делегатов.
Распределение куч
В зависимости от использования при работе с локальными функциями можно избежать распределения куч, которое всегда необходимо выполнять при работе с лямбда-выражениями. Если локальная функция никогда не преобразуется в делегат и ни одна из переменных, захваченных локальной функцией, не захвачена другими лямбда-выражениями или локальными функциями, которые преобразуются в делегаты, компилятор может избежать распределения куч.
Рассмотрим следующий асинхронный пример:
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 non-negative");
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();
}
Замыкание для этого лямбда-выражения содержит переменные address
, index
и name
. При использовании локальных функций объект, который реализует замыкание, может иметь тип struct
. Этот тип структуры будет передан в локальную функцию посредством ссылки. Эта разница в реализации позволяет избежать распределения.
Создание экземпляра, необходимое для лямбда-выражений, означает выделение дополнительной памяти, что в критически важном коде может ухудшить производительность. Локальные функции не создают этой перегрузки. В приведенном выше примере в версии с локальной функцией используется на две операции выделения памяти меньше по сравнению с версией на основе лямбда-выражения.
Если известно, что локальная функция не будет преобразована в делегат и ни одна из захватываемых ею переменных не захватывается другими лямбда-выражениями или локальными функциями, которые преобразуются в делегаты, то можно гарантировать, что локальная функция не будет распределяться в куче за счет объявления в качестве локальной функции static
.
Совет
Включите правило стиля кода .NET IDE0062 , чтобы гарантировать, что локальные функции всегда помечены static
.
Примечание.
В эквивалентном этому методе на основе локальной функции также используется класс для замыкания. Реализация замыкания для локальной функции в формате class
или struct
зависит от особенностей реализации. Локальная функция может использовать struct
, тогда как в лямбда-выражениях всегда используется 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 non-negative");
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.";
}
}
Использование ключевого слова yield
Еще одно преимущество локальных функций, которое не показано в этом примере, заключается в том, что они могут быть реализованы в качестве итераторов с использованием синтаксиса yield return
для создания последовательности значений.
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
. Дополнительные сведения см. в разделе об ошибке компилятора CS1621.
Локальные функции могут показаться избыточными для лямбда-выражений, поскольку обычно применяются иначе и в других целях. Локальные функции более эффективны в случаях, когда вам нужно написать функцию, которая будет вызываться только из контекста другого метода.
Спецификация языка C#
Дополнительные сведения см. в разделе "Объявления локальных функций" спецификации языка C#.