Fonctions locales (Guide de programmation C#)

Les fonctions locales sont des méthodes d’un type qui sont imbriqués dans un autre membre. Elles ne peuvent être appelées qu’à partir de leur membre conteneur. Les fonctions locales peuvent être déclarées et appelées dans et à partir des éléments suivants :

  • Méthodes, tout particulièrement les méthodes iterator et async
  • Constructeurs
  • Accesseurs de propriété
  • Accesseurs d’événement
  • Méthodes anonymes
  • Expressions lambda
  • Finaliseurs
  • Autres fonctions locales

En revanche, les fonctions locales ne peuvent pas être déclarées à l’intérieur d’un membre expression-bodied.

Notes

Dans certains cas, vous pouvez utiliser une expression lambda pour implémenter la fonctionnalité également prise en charge par une fonction locale. Pour obtenir une comparaison, consultez Fonctions locales et expressions lambda.

Les fonctions locales permettent de clarifier l’objectif de votre code. Quiconque lisant votre code peut voir que la méthode ne peut pas être appelée, sauf par la méthode conteneur. Pour les projets d’équipe, elles empêchent aussi à un autre développeur d’appeler par inadvertance la méthode directement à partir d’un autre emplacement dans la classe ou le struct.

Syntaxe des fonctions locales

Une fonction locale est définie en tant que méthode imbriquée à l’intérieur d’un membre conteneur. Sa définition présente la syntaxe suivante :

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

Notes

<parameter-list> ne doit pas contenir les paramètres nommés avec un mot clé contextuelvalue. Le compilateur crée la variable temporaire « value », qui contient les variables sortantes référencées, ce qui provoque ultérieurement une ambiguïté et peut également entraîner un comportement inattendu.

Vous pouvez utiliser les modificateurs suivants avec une fonction locale :

  • async
  • unsafe
  • static Une fonction locale statique ne peut pas capturer les variables locales ou l’état de l’instance.
  • extern Une fonction locale externe doit être static.

Toutes les variables locales définies dans le membre conteneur, y compris ses paramètres de méthode, sont accessibles dans une fonction locale non statique.

Contrairement à la définition d’une méthode, la définition d’une fonction locale ne peut pas inclure le modificateur d'accès au membre. Comme toutes les fonctions locales sont privées, l’inclusion d’un modificateur d’accès, tel que le mot clé private, génère l’erreur de compilateur CS0106 : « Le modificateur « private » n’est pas valide pour cet élément ».

L’exemple suivant définit une fonction locale nommée AppendPathSeparator qui est privée pour une méthode nommée 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 + @"\";
     }
}

Vous pouvez appliquer des attributs à une fonction locale, à ses paramètres et à ses paramètres de type, comme le montre l’exemple suivant :

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

L’exemple précédent utilise un attribut spécial pour aider le compilateur dans l’analyse statique dans un contexte pouvant accepter la valeur Null.

Fonctions locales et exceptions

L’une des caractéristiques intéressantes des fonctions locales est qu’elles permettent l’affichage immédiat des exceptions. Pour les méthodes d’itérateur, les exceptions apparaissent uniquement au moment où la séquence retournée est énumérée, et non à la récupération de l’itérateur. Pour les méthodes async, les exceptions levées dans une méthode async sont observées quand la tâche retournée est attendue.

L’exemple suivant définit une méthode OddSequence qui énumère les nombres impairs dans une plage spécifiée. Sachant qu’elle passe à la méthode d’énumérateur OddSequence un nombre supérieur à 100, la méthode lève une exception ArgumentOutOfRangeException. Comme le montre la sortie de l’exemple, l’exception apparaît uniquement au moment d’itérer les nombres, et non à la récupération de l’énumérateur.

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

Si vous placez une logique d’itérateur dans une fonction locale, des exceptions de validation d’argument sont levées lorsque vous récupérez l’énumérateur, comme le montre l’exemple suivant :

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

Fonctions locales et expressions lambda

À première vue, les fonctions locales et les expressions lambda sont très similaires. Souvent, le choix d’utiliser des expressions lambda ou des fonctions locales est une question de style et de préférences personnelles. Toutefois, il existe de réelles différences qui vous feront utiliser les unes ou les autres et que vous devez connaître.

Examinons les différences entre l’implémentation de l’algorithme factoriel avec une fonction locale et une expression lambda. Voici la version utilisant une fonction locale :

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

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

Cette version utilise des expressions 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);
}

Dénomination

Les fonctions locales sont explicitement nommées comme des méthodes. Les expressions lambda sont des méthodes anonymes qui doivent être affectées à des variables d’un type delegate, généralement de type Action ou Func. Lorsque vous déclarez une fonction locale, le processus est semblable à l’écriture d’une méthode normale ; vous déclarez un type de retour et une signature de fonction.

Signatures de fonction et types d’expressions lambda

Les expressions lambda s’appuient sur le type de la variable Action/Func qui leur est attribué pour déterminer les types d’argument et de retour. Dans les fonctions locales, étant donné que la syntaxe ressemble beaucoup à l’écriture d’une méthode normale, les types d’argument et le type de retour font déjà partie de la déclaration de la fonction.

À compter de C# 10, certaines expressions lambda ont un type naturel, ce qui permet au compilateur d’inférer le type de retour et les types de paramètres de l’expression lambda.

Assignation définie

Les expressions lambda sont des objets qui sont déclarés et affectés au moment de l’exécution. Pour qu’une expression lambda soit utilisée, elle doit être définitivement affectée : la variable Action/Func à laquelle elle sera affectée doit être déclarée et l’expression lambda doit lui être attribuée. Notez que LambdaFactorial doit déclarer et initialiser l’expression lambda nthFactorial avant de la définir. Si ce n’est pas le cas, cela entraîne une erreur de compilation due au fait que vous référencez nthFactorial avant de lui affecter une valeur.

Les fonctions locales sont définies au moment de la compilation. Comme elles ne sont pas affectées à des variables, elles peuvent être référencées à partir de n’importe quel emplacement de code où elles se trouvent dans l’étendue. Dans notre premier exemple LocalFunctionFactorial, nous pourrions déclarer notre fonction locale au-dessus ou au-dessous de l’instruction return et ne pas déclencher d’erreurs du compilateur.

Ces différences font que les algorithmes récursifs sont plus faciles à créer en utilisant des fonctions locales. Vous pouvez déclarer et définir une fonction locale qui s’appelle elle-même. Les expressions lambda doivent être déclarées et une valeur par défaut doit leur être affectée avant qu’elles puissent être réaffectées à un corps référençant la même expression lambda.

Implémentation en tant que délégué

Les expressions lambda sont converties en délégués lorsqu’elles sont déclarées. Les fonctions locales sont plus flexibles, car elles peuvent être écrites comme une méthode traditionnelle ou en tant que délégué. Les fonctions locales sont converties en délégués uniquement lorsque a utilisé en tant que délégué.

Si vous déclarez une fonction locale et la référencez uniquement en l’appelant comme une méthode, elle ne sera pas convertie en délégué.

Capture de la variable

Les règles d’affectation définies s’appliquent également à toutes les variables qui sont capturées par la fonction locale ou l’expression lambda. Le compilateur peut effectuer une analyse statique qui active des fonctions locales de manière à affecter définitivement les variables capturées dans la portée englobante. Examinez cet exemple :

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

    void LocalFunction() => y = 0;
}

Le compilateur peut déterminer que LocalFunction affecte y de manière définitive lorsqu’elle est appelée. Dans la mesure où LocalFunction est appelée avant l’instruction return, y est affecté de manière définitive à l’instruction return.

Notez que lorsqu’une fonction locale capture des variables dans l’étendue englobante, la fonction locale est implémentée en tant que type délégué.

Allocations de tas

En fonction de leur utilisation, les fonctions locales peuvent éviter les allocations de tas qui sont toujours nécessaires pour les expressions lambda. Si une fonction locale n’est jamais convertie en délégué, et qu’aucune des variables capturées par la fonction locale n’est capturée par d’autres expressions lambda ou fonctions locales qui sont converties en délégués, le compilateur peut éviter les allocations de tas.

Penchons-nous sur cet exemple asynchrone :

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

La fermeture de cette expression lambda contient les variables address, index et name. Dans le cas des fonctions locales, l’objet qui implémente la fermeture peut être un type struct. Ce type de struct serait transmis par référence à la fonction locale. Cette différence d’implémentation évite une allocation.

L’instanciation nécessaire pour les expressions lambda signifie des allocations de mémoire supplémentaires, qui peuvent être un facteur influençant les performances dans les chemins de code critiques au niveau du temps. Les fonctions locales n’entraînent pas cette charge supplémentaire. Dans l’exemple ci-dessus, la version de fonctions locale a deux allocations de moins que la version à expression lambda.

Si vous savez que votre fonction locale ne sera pas convertie en délégué et qu’aucune des variables capturées par elle n’est capturée par d’autres fonctions lambdas ou fonctions locales converties en délégués, vous pouvez garantir que votre fonction locale évite d’être allouée sur le tas en la déclarant en tant que fonction locale static.

Conseil

Activez la règle du style de code .NET IDE0062 pour vous assurer que les fonctions locales sont toujours marquées static.

Notes

L’équivalent de cette méthode avec une fonction locale fait aussi appel à une classe pour la fermeture. Le fait que la fermeture d’une fonction locale soit implémentée en tant que class ou struct est un détail d’implémentation. Une fonction locale peut utiliser un type struct contrairement à une expression lambda qui utilise toujours un type 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.";
    }
}

Utilisation du mot clé yield

Ultime avantage non décrit dans cet exemple : les fonctions locales peuvent être implémentées en tant qu’itérateurs, en utilisant la syntaxe yield return pour produire une séquence de valeurs.

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

L’instruction yield return n’est pas autorisée dans les expressions lambda. Pour plus d’informations, consultez Erreur du compilateur CS1621.

Alors que les fonctions locales peuvent sembler redondantes par rapport aux expressions lambda, elles ont en réalité des objectifs différents et des utilisations différentes. Les fonctions locales sont plus efficaces dans le cas où vous voulez écrire une fonction qui est appelée seulement dans le contexte d’une autre méthode.

spécification du langage C#

Pour plus d’informations, consultez la section Déclarations de fonctions locales de la spécification du langage C#.

Voir aussi