Megosztás a következőn keresztül:


Helyi függvények (C# programozási útmutató)

A helyi függvények olyan típusú metódusok, amelyek egy másik tagba vannak ágyazva. Csak a tagjuktól hívhatók meg. A helyi függvények deklarálhatók és meghívhatók a következőből:

  • Metódusok, különösen iterátor- és aszinkron metódusok
  • Konstruktorok
  • Tulajdonságkiegészítők
  • Eseménykiegészítők
  • Névtelen metódusok
  • Lambda-kifejezések
  • Döntősök
  • Egyéb helyi függvények

A helyi függvények azonban nem deklarálhatók egy kifejezés által testesített tagon belül.

Feljegyzés

Bizonyos esetekben lambda kifejezéssel implementálhatja a helyi függvények által is támogatott funkciókat. Összehasonlításért tekintse meg a Helyi függvények és a lambda kifejezések című témakört.

A helyi függvények egyértelművé teszik a kód szándékát. A kód olvasói láthatják, hogy a metódus nem hívható meg, kivéve a kódot tartalmazó metódust. A csapatprojektek esetében azt is lehetetlenné teszik, hogy egy másik fejlesztő tévedésből közvetlenül az osztály vagy a strustruktúra más helyről hívja meg a metódust.

Helyi függvény szintaxisa

A helyi függvények beágyazott metódusként vannak definiálva egy tagon belül. Definíciója a következő szintaxist tartalmazza:

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

Feljegyzés

A <parameter-list> paraméter nem tartalmazza a környezeti kulcsszóval valueelnevezett paramétereket. A fordító létrehozza az "érték" ideiglenes változót, amely tartalmazza a hivatkozott külső változókat, ami később kétértelműséget okoz, és váratlan viselkedést is okozhat.

A következő módosítókat használhatja helyi függvényekkel:

  • async
  • unsafe
  • static A statikus helyi függvények nem tudnak helyi változókat vagy példányállapotokat rögzíteni.
  • extern Külső helyi függvénynek kell lennie static.

Az adott tagban definiált helyi változók, beleértve a metódusparamétereket is, nem statikus helyi függvényekben érhetők el.

A metódusdefiníciókkal ellentétben a helyi függvénydefiníciók nem tartalmazhatják a taghozzáférés-módosítót. Mivel minden helyi függvény privát, beleértve egy hozzáférés-módosítót, például a private kulcsszót, a CS0106 fordítási hibát generálja, "A módosító "private" érvénytelen ehhez az elemhez."

Az alábbi példa egy olyan helyi függvényt AppendPathSeparator határoz meg, amely egy nevesített GetTextmetódushoz tartozik:

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 + @"\";
     }
}

Attribútumokat alkalmazhat egy helyi függvényre, annak paramétereire és típusparaméterekre, ahogy az alábbi példa mutatja:

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

Az előző példa egy speciális attribútummal segíti a fordítót a null értékű környezetben történő statikus elemzésben.

Helyi függvények és kivételek

A helyi függvények egyik hasznos funkciója, hogy lehetővé teszik a kivételek azonnali felszínre hozását. Az iterátor metódusok esetében a kivételek csak akkor jelennek meg, ha a visszaadott sorozat számba van adva, és nem az iterátor lekérésekor. Az aszinkron metódusok esetében az aszinkron metódusban alkalmazott kivételek akkor figyelhetők meg, amikor a visszaadott feladatra várni kell.

Az alábbi példa egy metódust OddSequence határoz meg, amely egy megadott tartományban számba adja a páratlan számokat. Mivel 100-nál nagyobb számot ad át az OddSequence enumerátor metódusnak, a metódus egy ArgumentOutOfRangeException. Ahogy a példa kimenete is mutatja, a kivétel csak akkor jelenik meg, ha iterálja a számokat, és nem az enumerátor lekérésekor.

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

Ha iterátorlogikát helyez egy helyi függvénybe, az argumentumérvényesítési kivételek az enumerátor lekérésekor jelennek meg, ahogy az alábbi példa mutatja:

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

Helyi függvények és lambdakifejezések

Első pillantásra a helyi függvények és a lambda kifejezések nagyon hasonlóak . Sok esetben a lambda kifejezések és a helyi függvények használata közötti választás a stílus és a személyes preferencia kérdése. Vannak azonban valós különbségek abban, hogy hol használhatja az egyiket vagy a másikat, amellyel tisztában kell lennie.

Vizsgáljuk meg a helyi függvény és a lambda kifejezés implementációi közötti különbségeket a faktoriális algoritmusban. A helyi függvényt használó verzió:

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

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

Ez a verzió lambdakifejezéseket használ:

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

Elnevezés

A helyi függvények neve kifejezetten metódusként van elnevezve. A Lambda-kifejezések névtelen metódusok, és egy delegate típus változóihoz kell hozzárendelni, általában vagy Action Func típusonként. Ha helyi függvényt deklarál, a folyamat olyan, mint egy normál metódus írása; deklarál egy visszatérési típust és egy függvény-aláírást.

Függvény-aláírások és lambda-kifejezéstípusok

A Lambda-kifejezések a hozzárendelt változó típusára Action/Func támaszkodnak az argumentumok és a visszatérési típusok meghatározásához. A helyi függvényekben, mivel a szintaxis hasonlít egy normál metódus írásához, az argumentumtípusok és a visszatérési típus már a függvénydeklaráció részét képezik.

A C# 10-től kezdődően egyes lambdakifejezések természetes típusúak, így a fordító a lambda kifejezés visszatérési típusát és paramétertípusait is kikövetkezheti.

Határozott hozzárendelés

A Lambda-kifejezések olyan objektumok, amelyek futásidőben deklarálva és hozzárendelve vannak. Ahhoz, hogy egy lambda kifejezést használhasson, határozottan hozzá kell rendelni: a Action/Func hozzárendelni kívánt változót deklarálni kell, és hozzá kell rendelni a lambda kifejezést. Figyelje meg, hogy LambdaFactorial definiálása előtt deklarálnia és inicializálnia kell a lambda kifejezést nthFactorial . Ha ezt nem teszi meg, fordítási időhiba lép fel a hivatkozáskor nthFactorial a hozzárendelés előtt.

A helyi függvények fordítási időben vannak definiálva. Mivel nincsenek változókhoz rendelve, bármely kódhelyről hivatkozhatunk rájuk, ahol a hatókörben van. Az első példában LocalFunctionFactorialdeklarálhatjuk a helyi függvényt az return utasítás felett vagy alatt, és nem válthatunk ki fordítóhibákat.

Ezek a különbségek azt jelentik, hogy a rekurzív algoritmusok könnyebben hozhatók létre helyi függvények használatával. Deklarálhat és definiálhat egy helyi függvényt, amely meghívja magát. A Lambda-kifejezéseket deklarálni kell, és hozzá kell rendelni egy alapértelmezett értéket, mielőtt újra hozzárendelhetők lennének egy olyan törzshez, amely ugyanarra a lambda kifejezésre hivatkozik.

Megvalósítás meghatalmazottként

A Lambda-kifejezések deklarálásakor a rendszer meghatalmazottakká alakítja őket. A helyi függvények rugalmasabbak, mivel hagyományos módszerként vagy meghatalmazottként is írhatók. A helyi függvények csak meghatalmazottként való használat esetén lesznek delegáltakká alakítva .

Ha deklarál egy helyi függvényt, és csak metódusként hívja meg, a rendszer nem konvertálja delegálttá.

Változó rögzítése

A határozott hozzárendelés szabályai a helyi függvény vagy lambda kifejezés által rögzített változókat is érintik. A fordító képes statikus elemzést végezni, amely lehetővé teszi, hogy a helyi függvények biztosan hozzárendeljenek rögzített változókat a belefoglaló hatókörbe. Vegye fontolóra ezt a példát:

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

    void LocalFunction() => y = 0;
}

A fordító meghatározhatja, hogy a LocalFunction híváskor biztosan hozzárendeli-e y . Mivel LocalFunction az return utasítás előtt van meghívva, y a rendszer mindenképpen hozzá van rendelve az return utasításhoz.

Vegye figyelembe, hogy ha egy helyi függvény rögzíti a változókat a belefoglaló hatókörbe, a helyi függvény egy lezárással lesz implementálva, például a delegálási típusok.

Halomfoglalások

Használatuktól függően a helyi függvények elkerülhetik a lambdakifejezésekhez mindig szükséges halomfoglalásokat. Ha a helyi függvények soha nem lesznek delegáltak, és a helyi függvény által rögzített változók egyikét sem rögzítik más lambdák vagy helyi függvények, amelyeket delegáltakká konvertálnak, a fordító elkerülheti a halomfoglalásokat.

Tekintse meg ezt az aszinkron példát:

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

A lambda kifejezés bezárása tartalmazza a , index és name a addressváltozókat. Helyi függvények esetén a lezárást megvalósító objektum típus lehet struct . Ezt a szerkezettípust a rendszer a helyi függvényre hivatkozva adja át. Ez a megvalósítási különbség megtakarítana egy foglalást.

A lambda-kifejezésekhez szükséges példányosítás további memóriafoglalásokat jelent, ami teljesítménytényező lehet az idő szempontjából kritikus kódútvonalakban. A helyi függvények nem járnak ezzel a többletterheléssel. A fenti példában a helyi függvények verziója két kevesebb foglalással rendelkezik, mint a lambda kifejezés verziója.

Ha tudja, hogy a helyi függvény nem lesz delegálttá alakítva, és az általa rögzített változók egyikét sem rögzíti más lambdas vagy helyi függvény, amelyeket delegáltakká alakít, akkor garantálhatja, hogy a helyi függvény ne legyen lefoglalva a halomra, ha deklarálja azt helyi függvényként static .

Tipp.

Engedélyezze a .NET-kódstílus-szabály IDE0062 , hogy a helyi függvények mindig meg legyenek jelölve static.

Feljegyzés

A metódus helyi függvénye egy osztályt is használ a lezáráshoz. Azt, hogy egy helyi függvény bezárása implementálva class van-e, vagy implementálási struct részlet. A helyi függvények használhatnak egy struct , míg a lambda mindig egy 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.";
    }
}

A yield kulcsszó használata

A mintában nem bemutatott végső előny az, hogy a helyi függvények iterátorként implementálhatók, a yield return szintaxis használatával pedig értéksorozatot hozhatnak létre.

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

Az yield return utasítás nem engedélyezett a lambda kifejezésekben. További információ: CS1621 fordítói hiba.

Bár a helyi függvények redundánsnak tűnhetnek a lambda kifejezésekre, valójában különböző célokat szolgálnak, és különböző felhasználási módokkal rendelkeznek. A helyi függvények hatékonyabbak abban az esetben, ha olyan függvényt szeretne írni, amelyet csak egy másik metódus környezetéből hívunk meg.

C# nyelvspecifikáció

További információ: A C# nyelv specifikációjának Helyi függvénydeklarációk szakasza.

Lásd még