Си шарп вызов функции

Локальные функции (руководство по программированию на C#)

Локальные функции — это методы типа, вложенные в другой член. Они могут вызываться только из того элемента, в который вложены. Ниже перечислены элементы, в которых можно объявлять и из которых можно вызывать локальные функции:

  • Методы, в частности методы итератора и асинхронные методы
  • Конструкторы
  • Методы доступа свойств
  • Методы доступа событий
  • Анонимные методы
  • Лямбда-выражения
  • Методы завершения
  • Другие локальные функции

Тем не менее локальные функции нельзя объявлять внутри элемента, воплощающего выражение.

В некоторых случаях для реализации возможностей, поддерживаемых локальными функциями, также можно использовать лямбда-выражения. Дополнительные сведения см. в разделе Локальные функции или лямбда-выражения.

Применение локальных функций позволяет сделать предназначение кода более понятным. Другие пользователи, читающие ваш код, смогут видеть, что соответствующий метод вызывается только внутри того метода, в который он вложен. В случае с командными проектами это также гарантирует, что другой разработчик не сможет ошибочно вызвать метод напрямую из любого другого места в классе или структуре.

Синтаксис локальной функции

Локальная функция определяется как вложенный метод внутри содержащего ее элемента. Ниже приведен синтаксис определения локальной функции:

Не должен содержать параметры с именами с контекстными ключевое слово value . Компилятор создает временную переменную value, которая содержит указанные переменные outter, что позже вызывает неоднозначность и может также привести к непредвиденному поведению.

С локальной функцией можно использовать следующие модификаторы:

  • async
  • unsafe
  • static Статическая локальная функция не может записывать локальные переменные или состояние экземпляра.
  • extern Внешняя локальная функция должна иметь значение static .
Читайте также:  Python logging with threads

Все локальные переменные, определенные в содержащем функцию элементе (включая параметры метода), доступны в нестатической локальной функции.

В отличие от определения метода, определение локальной функции не может содержать модификатор доступа к элементу. Поскольку все локальные функции являются частными, при использовании модификатора доступа (например, ключевого слова private ) возникает ошибка компилятора CS0106, «Модификатор «private» недопустим для этого элемента».

В следующем примере определяется локальная функция AppendPathSeparator , которая является частной для метода GetText :

private static string GetText(string path, string filename) < var reader = File.OpenText($""); var text = reader.ReadToEnd(); return text; string AppendPathSeparator(string filepath) < return filepath.EndsWith(@"\") ? filepath : filepath + @"\"; >> 

Начиная с C# 9.0 можно применять атрибуты к локальной функции, ее параметрам и параметрам типа, как показано в следующем примере:

#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() < IEnumerablexs = OddSequence(50, 110); Console.WriteLine("Retrieved enumerator. "); foreach (var x in xs) // line 11 < Console.Write($""); > > public static IEnumerable 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 > > // 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() < IEnumerablexs = OddSequence(50, 110); // line 8 Console.WriteLine("Retrieved enumerator. "); foreach (var x in xs) < Console.Write($""); > > public static IEnumerable 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 GetOddSequenceEnumerator() < for (int i = start; 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

В этой версии используются лямбда-выражения:

public static int LambdaFactorial(int n) < FuncnthFactorial = default(Func); nthFactorial = number => number

Именование

Локальные функции явно именуются как методы. Лямбда-выражения представляют собой анонимные методы и должны назначаться переменным типа delegate , как правило, типа Action или Func . Процесс объявления локальной функции аналогичен написанию обычного метода: вы объявляете тип возвращаемого значения и сигнатуру функции.

Сигнатуры функций и типы лямбда-выражений

Лямбда-выражения используют тип переменной Action / Func , которой они назначаются, для определения типов аргументов и возвращаемых значений. Поскольку синтаксис локальных функций во многом аналогичен обычному методу, типы аргументов и возвращаемых значений уже входят в объявление функции.

Начиная с C# 10, некоторые лямбда-выражения имеют естественный тип, который позволяет компилятору определить тип возвращаемого значения и типы параметров лямбда-выражения.

Определенное назначение

Лямбда-выражения — это объекты, которые объявляются и назначаются во время выполнения. Чтобы использовать лямбда-выражение, его необходимо определенно назначить: для этого необходимо объявить переменную Action / Func , которой оно будет назначено, и назначить ей лямбда-выражение. Обратите внимание на то, что LambdaFactorial должно объявить и инициализировать лямбда-выражение nthFactorial , прежде чем его определить. В противном случае возникает ошибка компилятора, связанная со ссылкой на объект nthFactorial , который еще не был назначен.

Локальные функции определяются во время компиляции. Они не назначаются переменным, в связи с чем на них можно ссылаться из любого расположения кода, в области действия которого они находятся. В первом примере LocalFunctionFactorial мы можем объявить локальную функцию выше или ниже инструкции return , и это не приведет к ошибкам компилятора.

Эти различия означают, что рекурсивные алгоритмы легче создавать, используя локальные функции. Можно объявить и определить локальную функцию, которая вызывает саму себя. Необходимо объявить лямбда-выражения и назначить им значение по умолчанию, прежде чем их можно будет переназначить телу, которое ссылается на то же лямбда-выражение.

Реализация в виде делегата

Лямбда-выражения преобразуются в делегаты при объявлении. Локальные функции являются более гибкими и могут определяться в виде традиционного метода или делегата. Локальные функции преобразуются в делегаты только при использовании в качестве делегата.

Если объявить локальную функцию и сослаться на нее только путем вызова этой функции в качестве метода, она не будет преобразована в делегат.

Захват переменной

Правила определенного назначения также влияют на любые переменные, захватываемые локальной функцией или лямбда-выражением. Компилятор может выполнять статический анализ, что позволяет локальным функциям определенно назначать захватываемые переменные во включающей области. Рассмотрим следующий пример.

Компилятор может определить, что LocalFunction определенно назначает y при вызове. Поскольку LocalFunction вызывается перед оператором return , y определенно назначается в операторе return .

Обратите внимание, что когда локальная функция захватывает переменные во включающей области, эта функция реализуется как тип делегата.

Распределение куч

В зависимости от использования при работе с локальными функциями можно избежать распределения куч, которое всегда необходимо выполнять при работе с лямбда-выражениями. Если локальная функция никогда не преобразуется в делегат и ни одна из переменных, захваченных локальной функцией, не захвачена другими лямбда-выражениями или локальными функциями, которые преобразуются в делегаты, компилятор может избежать распределения куч.

Рассмотрим следующий асинхронный пример:

public async Task 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 longRunningWorkImplementation = async () => < var interimResult = await FirstWork(address); var secondResult = await SecondStep(index, name); return $"The results are and . Enjoy."; >; return await longRunningWorkImplementation(); > 

Замыкание для этого лямбда-выражения содержит переменные address , index и name . При использовании локальных функций объект, который реализует замыкание, может иметь тип struct . Этот тип структуры будет передан в локальную функцию посредством ссылки. Эта разница в реализации позволяет избежать распределения.

Создание экземпляра, необходимое для лямбда-выражений, означает выделение дополнительной памяти, что в критически важном коде может ухудшить производительность. Локальные функции не создают этой перегрузки. В приведенном выше примере в версии с локальной функцией используется на две операции выделения памяти меньше по сравнению с версией на основе лямбда-выражения.

Если известно, что локальная функция не будет преобразована в делегат и ни одна из захватываемых ею переменных не захватывается другими лямбда-выражениями или локальными функциями, которые преобразуются в делегаты, то можно гарантировать, что локальная функция не будет распределяться в куче за счет объявления в качестве локальной функции static .

Включите правило стиля кода .NET IDE0062 , чтобы локальные функции всегда помечались static .

В эквивалентном этому методе на основе локальной функции также используется класс для замыкания. Реализация замыкания для локальной функции в формате class или struct зависит от особенностей реализации. Локальная функция может использовать struct , тогда как в лямбда-выражениях всегда используется class .

public async Task 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 TasklongRunningWorkImplementation() < var interimResult = await FirstWork(address); var secondResult = await SecondStep(index, name); return $"The results are and . Enjoy."; > > 

Использование ключевого слова yield

Еще одно преимущество локальных функций, которое не показано в этом примере, заключается в том, что они могут быть реализованы в качестве итераторов с использованием синтаксиса yield return для создания последовательности значений.

public IEnumerable SequenceToLowercase(IEnumerable input) < if (!input.Any()) < throw new ArgumentException("There are no items to convert to lowercase."); >return LowercaseIterator(); IEnumerable LowercaseIterator() < foreach (var output in input.Select(item =>item.ToLower())) < yield return output; >> > 

В лямбда-выражениях не допускается использование оператора yield return . Дополнительные сведения см. в разделе Ошибка компилятора CS1621.

Локальные функции могут показаться избыточными для лямбда-выражений, поскольку обычно применяются иначе и в других целях. Локальные функции более эффективны в случаях, когда вам нужно написать функцию, которая будет вызываться только из контекста другого метода.

Спецификация языка C#

См. также раздел

Источник

Оцените статью