Funções locais (Guia de Programação em C#)

Funções locais são métodos de um tipo que são aninhados em outro membro. Eles só podem ser chamados a partir de seu membro que os contém. As funções locais podem ser declaradas e chamadas de:

  • Métodos, especialmente métodos iteradores e métodos assíncronos
  • Construtores
  • Acessórios de propriedade
  • Acessadores de eventos
  • Métodos anónimos
  • Expressões lambda
  • Finalizadores
  • Outras funções locais

No entanto, as funções locais não podem ser declaradas dentro de um membro com corpo de expressão.

Nota

Em alguns casos, você pode usar uma expressão lambda para implementar a funcionalidade também suportada por uma função local. Para uma comparação, consulte Funções locais vs. expressões lambda.

As funções locais deixam clara a intenção do seu código. Qualquer pessoa que leia seu código pode ver que o método não é chamável, exceto pelo método que contém. Para projetos de equipe, eles também tornam impossível para outro desenvolvedor chamar erroneamente o método diretamente de outro lugar na classe ou struct.

Sintaxe da função local

Uma função local é definida como um método aninhado dentro de um membro que contém. A sua definição tem a seguinte sintaxe:

<modifiers> <return-type> <method-name> <parameter-list>

Nota

O <parameter-list> não deve conter os parâmetros nomeados com palavra-chavevalue contextual. O compilador cria a variável temporária "value", que contém as variáveis externas referenciadas, o que mais tarde causa ambiguidade e também pode causar um comportamento inesperado.

Você pode usar os seguintes modificadores com uma função local:

  • async
  • unsafe
  • static Uma função local estática não pode capturar variáveis locais ou o estado da instância.
  • extern Uma função local externa deve ser static.

Todas as variáveis locais que são definidas no membro que contém, incluindo seus parâmetros de método, são acessíveis em uma função local não estática.

Ao contrário de uma definição de método, uma definição de função local não pode incluir o modificador de acesso de membro. Como todas as funções locais são privadas, incluindo um modificador de acesso, como a palavra-chave, gera erro private de compilador CS0106, "O modificador 'private' não é válido para este item."

O exemplo a seguir define uma função local chamada AppendPathSeparator que é privada para um método chamado 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 + @"\";
     }
}

Você pode aplicar atributos a uma função local, seus parâmetros e parâmetros de tipo, como mostra o exemplo a seguir:

#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;
    }
}

O exemplo anterior usa um atributo especial para ajudar o compilador na análise estática em um contexto anulável.

Funções locais e exceções

Uma das características úteis das funções locais é que elas podem permitir que exceções surjam imediatamente. Para métodos iteradores, as exceções são exibidas somente quando a sequência retornada é enumerada, e não quando o iterador é recuperado. Para métodos assíncronos, quaisquer exceções lançadas em um método assíncrono são observadas quando a tarefa retornada é aguardada.

O exemplo a seguir define um OddSequence método que enumera números ímpares em um intervalo especificado. Como ele passa um número maior que 100 para o OddSequence método enumerador, o método lança um ArgumentOutOfRangeException. Como mostra a saída do exemplo, a exceção aparece somente quando você itera os números, e não quando recupera o enumerador.

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

Se você colocar a lógica do iterador em uma função local, as exceções de validação de argumento serão lançadas quando você recuperar o enumerador, como mostra o exemplo a seguir:

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

Funções locais vs. expressões lambda

À primeira vista, as funções locais e as expressões lambda são muito semelhantes. Em muitos casos, a escolha entre usar expressões lambda e funções locais é uma questão de estilo e preferência pessoal. No entanto, existem diferenças reais em onde você pode usar um ou outro que você deve estar ciente.

Vamos examinar as diferenças entre a função local e as implementações de expressão lambda do algoritmo fatorial. Aqui está a versão usando uma função local:

public static int LocalFunctionFactorial(int n)
{
    return nthFactorial(n);

    int nthFactorial(int number) => number < 2 
        ? 1 
        : number * nthFactorial(number - 1);
}

Esta versão usa expressões lambda:

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);
}

Atribuição de nomes

As funções locais são explicitamente nomeadas como métodos. As expressões lambda são métodos anônimos e precisam ser atribuídas a variáveis de um delegate tipo, normalmente um Action ou Func tipos. Quando você declara uma função local, o processo é como escrever um método normal; Você declara um tipo de retorno e uma assinatura de função.

Assinaturas de função e tipos de expressão lambda

As expressões do Lambda dependem do tipo da variável que lhes é atribuída para determinar os tipos de Action/Func argumento e retorno. Em funções locais, como a sintaxe é muito parecida com escrever um método normal, os tipos de argumento e o tipo de retorno já fazem parte da declaração de função.

A partir do C# 10, algumas expressões lambda têm um tipo natural, que permite ao compilador inferir o tipo de retorno e os tipos de parâmetro da expressão lambda.

Atribuição definitiva

As expressões do Lambda são objetos declarados e atribuídos em tempo de execução. Para que uma expressão lambda seja usada, ela precisa ser definitivamente atribuída: a Action/Func variável à qual ela será atribuída deve ser declarada e a expressão lambda atribuída a ela. Observe que LambdaFactorial deve declarar e inicializar a expressão nthFactorial lambda antes de defini-la. Não fazer isso resulta em um erro de tempo de compilação para referenciar antes de nthFactorial atribuí-lo.

As funções locais são definidas em tempo de compilação. Como eles não são atribuídos a variáveis, eles podem ser referenciados a partir de qualquer local de código onde ele está no escopo, em nosso primeiro exemplo LocalFunctionFactorial, podemos declarar nossa função local acima ou abaixo da instrução e não acionar nenhum erro de return compilador.

Essas diferenças significam que os algoritmos recursivos são mais fáceis de criar usando funções locais. Você pode declarar e definir uma função local que se chama. As expressões do Lambda devem ser declaradas e receber um valor padrão antes que possam ser reatribuídas a um corpo que faça referência à mesma expressão lambda.

Implementação como delegado

As expressões do Lambda são convertidas em delegados quando são declaradas. As funções locais são mais flexíveis na medida em que podem ser escritas como um método tradicional ou como um delegado. As funções locais só são convertidas em delegados quando usadas como delegados.

Se você declarar uma função local e apenas fazer referência a ela chamando-a como um método, ela não será convertida em um delegado.

Captura variável

As regras de atribuição definida também afetam quaisquer variáveis capturadas pela função local ou expressão lambda. O compilador pode executar análise estática que permite que as funções locais atribuam definitivamente as variáveis capturadas no escopo de delimitação. Considere este exemplo:

int M()
{
    int y;
    LocalFunction();
    return y;

    void LocalFunction() => y = 0;
}

O compilador pode determinar que LocalFunction definitivamente atribui y quando chamado. Porque LocalFunction é chamado antes da return declaração, y é definitivamente atribuído na return declaração.

Observe que quando uma função local captura variáveis no escopo de delimitação, a função local é implementada usando um fechamento, como são os tipos delegados.

Alocações de heap

Dependendo de seu uso, as funções locais podem evitar alocações de heap que são sempre necessárias para expressões lambda. Se uma função local nunca for convertida em um delegado, e nenhuma das variáveis capturadas pela função local for capturada por outros lambdas ou funções locais que são convertidas em delegados, o compilador pode evitar alocações de heap.

Considere este exemplo assíncrono:

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();
}

O fechamento para essa expressão lambda contém as addressvariáveis , index e name . No caso de funções locais, o objeto que implementa o fechamento pode ser um struct tipo. Esse tipo de estrutura seria passado por referência à função local. Esta diferença na execução permitiria poupar numa dotação.

A instanciação necessária para expressões lambda significa alocações de memória extra, o que pode ser um fator de desempenho em caminhos de código críticos em termos de tempo. As funções locais não incorrem nessa sobrecarga. No exemplo acima, a versão de funções locais tem duas alocações a menos do que a versão de expressão lambda.

Se você souber que sua função local não será convertida em um delegado e nenhuma das variáveis capturadas por ela for capturada por outras lambdas ou funções locais que são convertidas em delegados, você pode garantir que sua função local evite ser alocada no heap declarando-a como uma static função local.

Gorjeta

Habilite a regra de estilo de código .NET IDE0062 para garantir que as funções locais estejam sempre marcadas static.

Nota

A função local equivalente deste método também usa uma classe para o fechamento. Se o encerramento de uma função local é implementado como um class ou um struct é um detalhe de implementação. Uma função local pode usar um struct enquanto um lambda sempre usará um 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.";
    }
}

Utilização da yield palavra-chave

Uma vantagem final não demonstrada neste exemplo é que as funções locais podem ser implementadas como iteradores, usando a yield return sintaxe para produzir uma sequência de valores.

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;
        }
    }
}

A yield return instrução não é permitida em expressões lambda. Para obter mais informações, consulte erro do compilador CS1621.

Embora as funções locais possam parecer redundantes para expressões lambda, elas na verdade servem a propósitos diferentes e têm usos diferentes. As funções locais são mais eficientes para o caso quando você deseja escrever uma função que é chamada apenas a partir do contexto de outro método.

Especificação da linguagem C#

Para obter mais informações, consulte a seção Declarações de função local da especificação da linguagem C#.

Consulte também