Поделиться через


Локальные функции (руководство по программированию на 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, которой они назначаются, для определения типов аргументов и возвращаемых значений. Поскольку синтаксис локальных функций во многом аналогичен обычному методу, типы аргументов и возвращаемых значений уже входят в объявление функции.

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

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

Лямбда-выражения — это объекты, которые объявляются и назначаются во время выполнения. Для того чтобы лямбда-выражение могло быть использовано, оно должно быть определенно назначено: переменная 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#.

См. также