Lokale functies (C#-programmeerhandleiding)

Lokale functies zijn methoden van een type dat in een ander lid is genest. Ze kunnen alleen worden aangeroepen vanuit het betreffende lid. Lokale functies kunnen worden gedeclareerd en aangeroepen vanuit:

  • Methoden, met name iteratormethoden en asynchrone methoden
  • Constructors
  • Eigenschapstoegangsors
  • Gebeurtenistoegangsors
  • Anonieme methoden
  • Lambda-expressies
  • Finalizers
  • Andere lokale functies

Lokale functies kunnen echter niet worden gedeclareerd binnen een expressie-lid.

Notitie

In sommige gevallen kunt u een lambda-expressie gebruiken om functionaliteit te implementeren die ook wordt ondersteund door een lokale functie. Zie Lokale functies versus lambda-expressies voor een vergelijking.

Met lokale functies wordt de intentie van uw code duidelijk. Iedereen die uw code leest, kan zien dat de methode niet kan worden aangeroepen, behalve door de met deze methode. Voor teamprojecten maken ze het ook onmogelijk dat een andere ontwikkelaar per ongeluk de methode rechtstreeks vanuit een andere klasse of struct aanroept.

Syntaxis van lokale functie

Een lokale functie wordt gedefinieerd als een geneste methode binnen een bevatd lid. De definitie heeft de volgende syntaxis:

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

Notitie

De <parameter-list> parameters met de naam met contextueel trefwoordvalue mogen niet worden vermeld. De compiler maakt de tijdelijke variabele 'waarde', die de buitenste variabelen bevat waarnaar later wordt verwezen, wat later dubbelzinnigheid veroorzaakt en kan ook onverwacht gedrag veroorzaken.

U kunt de volgende modifiers gebruiken met een lokale functie:

  • async
  • unsafe
  • static Een statische lokale functie kan geen lokale variabelen of instantiestatus vastleggen.
  • extern Een externe lokale functie moet zijn static.

Alle lokale variabelen die zijn gedefinieerd in het betreffende lid, inclusief de bijbehorende methodeparameters, zijn toegankelijk in een niet-statische lokale functie.

In tegenstelling tot een methodedefinitie kan een lokale functiedefinitie de wijziging voor lidtoegang niet bevatten. Omdat alle lokale functies privé zijn, met inbegrip van een toegangsaanpassing, zoals het private trefwoord, genereert compilerfout CS0106, 'De modifier 'privé' is niet geldig voor dit item.

In het volgende voorbeeld wordt een lokale functie gedefinieerd AppendPathSeparator die privé is voor een methode met de naam 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 + @"\";
     }
}

U kunt kenmerken toepassen op een lokale functie, de bijbehorende parameters en typeparameters, zoals in het volgende voorbeeld wordt weergegeven:

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

In het voorgaande voorbeeld wordt een speciaal kenmerk gebruikt om de compiler te helpen bij statische analyse in een null-context.

Lokale functies en uitzonderingen

Een van de handige functies van lokale functies is dat ze uitzonderingen onmiddellijk kunnen toestaan. Voor iteratormethoden worden uitzonderingen alleen weergegeven wanneer de geretourneerde reeks wordt geïnventariseerd en niet wanneer de iterator wordt opgehaald. Voor asynchrone methoden worden eventuele uitzonderingen in een asynchrone methode waargenomen wanneer de geretourneerde taak wordt gewacht.

In het volgende voorbeeld wordt een OddSequence methode gedefinieerd waarmee oneven getallen in een opgegeven bereik worden opgesomd. Omdat het een getal groter dan 100 doorgeeft aan de OddSequence enumerator-methode, genereert de methode een ArgumentOutOfRangeException. Zoals in de uitvoer van het voorbeeld wordt weergegeven, wordt de uitzondering alleen weergegeven wanneer u de getallen curseert en niet wanneer u de opsomming ophaalt.

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

Als u iteratorlogica in een lokale functie plaatst, worden uitzonderingen voor argumentvalidatie gegenereerd wanneer u de enumerator ophaalt, zoals in het volgende voorbeeld wordt weergegeven:

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

Lokale functies versus lambda-expressies

Op het eerste gezicht zijn lokale functies en lambda-expressies vergelijkbaar. In veel gevallen is de keuze tussen het gebruik van lambda-expressies en lokale functies een kwestie van stijl en persoonlijke voorkeur. Er zijn echter echte verschillen in waar u een of de andere kunt gebruiken waar u rekening mee moet houden.

Laten we eens kijken naar de verschillen tussen de implementaties van de lokale functie en lambda-expressies van het factoriële algoritme. Hier volgt de versie met behulp van een lokale functie:

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

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

In deze versie worden lambda-expressies gebruikt:

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

Naamgeving

Lokale functies worden expliciet benoemd als methoden. Lambda-expressies zijn anonieme methoden en moeten worden toegewezen aan variabelen van een delegate type, meestal ofwel ActionFunc typen. Wanneer u een lokale functie declareert, lijkt het proces op het schrijven van een normale methode; u declareert een retourtype en een functiehandtekening.

Functiehandtekeningen en lambda-expressietypen

Lambda-expressies zijn afhankelijk van het type variabele Action/Func waaraan ze zijn toegewezen om het argument te bepalen en retourtypen te retourneren. Omdat de syntaxis in lokale functies vergelijkbaar is met het schrijven van een normale methode, maken argumenttypen en retourtype al deel uit van de functiedeclaratie.

Vanaf C# 10 hebben sommige lambda-expressies een natuurlijk type, waardoor de compiler het retourtype en parametertypen van de lambda-expressie kan afleiden.

Definitieve opdracht

Lambda-expressies zijn objecten die tijdens runtime worden gedeclareerd en toegewezen. Om een lambda-expressie te kunnen gebruiken, moet deze zeker worden toegewezen: de Action/Func variabele waaraan deze wordt toegewezen, moet worden gedeclareerd en de lambda-expressie die eraan is toegewezen. U ziet dat LambdaFactorial u de lambda-expressie nthFactorial moet declareren en initialiseren voordat u deze definieert. Als u dit niet doet, treedt er een compilatietijdfout op voordat u deze nthFactorial toewijst.

Lokale functies worden gedefinieerd tijdens het compileren. Omdat ze niet zijn toegewezen aan variabelen, kunnen ze worden verwezen vanaf elke codelocatie waar deze zich binnen het bereik bevindt. In ons eerste voorbeeld LocalFunctionFactorialkunnen we onze lokale functie boven of onder de return instructie declareren en geen compilerfouten activeren.

Deze verschillen betekenen dat recursieve algoritmen eenvoudiger kunnen worden gemaakt met behulp van lokale functies. U kunt een lokale functie declareren en definiëren die zichzelf aanroept. Lambda-expressies moeten worden gedeclareerd en een standaardwaarde toegewezen voordat ze opnieuw kunnen worden toegewezen aan een hoofdtekst die verwijst naar dezelfde lambda-expressie.

Implementatie als gemachtigde

Lambda-expressies worden geconverteerd naar gemachtigden wanneer ze worden gedeclareerd. Lokale functies zijn flexibeler omdat ze kunnen worden geschreven als een traditionele methode of als gemachtigde. Lokale functies worden alleen geconverteerd naar gedelegeerden wanneer ze worden gebruikt als gemachtigde.

Als u een lokale functie declareert en er alleen naar verwijst door deze aan te roepen als een methode, wordt deze niet geconverteerd naar een gemachtigde.

Variabele vastleggen

De regels van definitieve toewijzing zijn ook van invloed op variabelen die worden vastgelegd door de lokale functie of lambda-expressie. De compiler kan statische analyses uitvoeren waarmee lokale functies zeker vastgelegde variabelen in het bereik kunnen toewijzen. Kijk eens naar dit voorbeeld:

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

    void LocalFunction() => y = 0;
}

De compiler kan bepalen dat dit LocalFunction zeker wordt toegewezen y wanneer deze wordt aangeroepen. Omdat LocalFunction wordt aangeroepen vóór de return instructie, y wordt zeker toegewezen aan de return instructie.

Wanneer een lokale functie variabelen vastlegt in het bereik insluiten, wordt de lokale functie geïmplementeerd als een gemachtigde.

Heap-toewijzingen

Afhankelijk van hun gebruik kunnen lokale functies heap-toewijzingen voorkomen die altijd nodig zijn voor lambda-expressies. Als een lokale functie nooit wordt geconverteerd naar een gemachtigde en geen van de variabelen die door de lokale functie zijn vastgelegd, worden vastgelegd door andere lambdas- of lokale functies die worden geconverteerd naar gemachtigden, kan de compiler heap-toewijzingen voorkomen.

Bekijk dit asynchrone voorbeeld:

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

De sluiting voor deze lambda-expressie bevat de addressen nameindex variabelen. In het geval van lokale functies kan het object dat de sluiting implementeert een struct type zijn. Dit structtype wordt doorgegeven door verwijzing naar de lokale functie. Dit verschil in implementatie bespaart op een toewijzing.

De instantiëring die nodig is voor lambda-expressies, betekent extra geheugentoewijzingen, wat een prestatiefactor kan zijn in tijdkritische codepaden. Voor lokale functies wordt deze overhead niet in rekening gebracht. In het bovenstaande voorbeeld heeft de lokale functieversie twee minder toewijzingen dan de lambda-expressieversie.

Als u weet dat uw lokale functie niet wordt geconverteerd naar een gemachtigde en geen van de variabelen die erin zijn vastgelegd, worden vastgelegd door andere lambdas- of lokale functies die worden geconverteerd naar gemachtigden, kunt u garanderen dat uw lokale functie niet wordt toegewezen aan de heap door deze als een static lokale functie te declareren.

Tip

Schakel de .NET-codestijlregel in IDE0062 om ervoor te zorgen dat lokale functies altijd zijn gemarkeerd static.

Notitie

Het equivalent van de lokale functie van deze methode maakt ook gebruik van een klasse voor de sluiting. Of de sluiting voor een lokale functie wordt geïmplementeerd als een class of een struct implementatiedetails. Een lokale functie kan een struct terwijl een lambda altijd een 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.";
    }
}

Gebruik van het yield trefwoord

Een laatste voordeel dat niet in dit voorbeeld wordt gedemonstreerd, is dat lokale functies kunnen worden geïmplementeerd als iterators, met behulp van de yield return syntaxis om een reeks waarden te produceren.

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

De yield return instructie is niet toegestaan in lambda-expressies. Zie compilerfout CS1621 voor meer informatie.

Hoewel lokale functies misschien overbodig lijken te zijn voor lambda-expressies, dienen ze eigenlijk verschillende doeleinden en hebben ze verschillende toepassingen. Lokale functies zijn efficiënter voor het geval wanneer u een functie wilt schrijven die alleen wordt aangeroepen vanuit de context van een andere methode.

C#-taalspecificatie

Zie de sectie Declaraties van lokale functies van de C#-taalspecificatie voor meer informatie.

Zie ook