Fungsi lokal (Panduan Pemrograman C#)

Fungsi lokal adalah metode dari jenis yang disarangkan di anggota lain. Mereka hanya bisa dipanggil dari anggota yang berisi. Fungsi lokal dapat dideklarasikan dalam dan dipanggil dari:

  • Metode, terutama metode iterator dan metode asinkron
  • Konstruktor
  • Pengakses properti
  • Pengakses peristiwa
  • Metode anonim
  • Ekspresi Lambda
  • Finalizer
  • Fungsi lokal lain

Namun, fungsi lokal tidak dapat dideklarasikan di dalam anggota bertubuh ekspresi.

Catatan

Dalam beberapa kasus, Anda bisa menggunakan ekspresi lambda untuk mengimplementasikan fungsionalitas yang juga didukung oleh fungsi lokal. Sebagai perbandingannya, lihat Fungsi lokal vs. ekspresi lambda.

Fungsi lokal membuat niat dari kode Anda menjadi jelas. Siapa pun yang membaca kode Anda bisa melihat bahwa metode tidak dapat dipanggil kecuali dengan metode yang berisi. Untuk proyek tim, mereka juga membuatnya mustahil bagi pengembang lain untuk keliru memanggil metode langsung dari tempat lain di kelas atau struktur.

Sintaks fungsi lokal

Fungsi lokal didefinisikan sebagai metode berlapis di dalam anggota yang berisi. Definisinya memiliki sintaks sebagai berikut:

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

Catatan

<parameter-list> tidak boleh berisi parameter bernama dengan kata kuncivalue kontekstual. Kompilator membuat variabel sementara "nilai", yang berisi variabel luar yang direferensikan, yang kemudian menyebabkan ambiguitas dan juga dapat menyebabkan perilaku yang tidak terduga.

Anda bisa menggunakan pengubah berikut dengan fungsi lokal:

  • async
  • unsafe
  • static Fungsi lokal statis tidak dapat mengambil variabel lokal atau status instans.
  • extern Fungsi lokal eksternal harus static.

Semua variabel lokal yang didefinisikan dalam anggota yang berisi, termasuk parameter metodenya, bisa diakses dalam fungsi lokal non-statik.

Tidak seperti definisi metode, definisi fungsi lokal tidak bisa menyertakan pengubah akses anggota. Karena semua fungsi lokal bersifat privat, termasuk juga pengubah akses, seperti kata kunci private, menghasilkan kesalahan pengompilasi CS0106, "Pengubah 'privat' tidak valid untuk item ini."

Contoh berikut mendefinisikan fungsi lokal bernama AppendPathSeparator yang bersifat privat untuk metode bernama 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 + @"\";
     }
}

Anda dapat menerapkan atribut ke fungsi lokal, parameternya, dan parameter jenisnya, seperti yang ditunjukkan contoh berikut:

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

Contoh sebelumnya menggunakan atribut khusus untuk membantu pengompilasi dalam analisis statik dalam konteks nullable.

Fungsi lokal serta pengecualian

Salah satu fitur fungsi lokal yang berguna adalah mereka bisa memungkinkan pengecualian untuk segera muncul. Untuk metode iterator, pengecualian hanya muncul ketika urutan yang dikembalikan dijumlahkan, dan bukan ketika iterator diambil. Untuk metode asinkron, pengecualian apa pun yang dilemparkan dalam metode asinkron diamati saat tugas yang dikembalikan ditunggu.

Contoh berikut mendefinisikan metode OddSequence yang menghitung angka ganjil dalam rentang tertentu. Karena melewati angka yang lebih besar dari 100 ke metode enumerator OddSequence, metode melemparkan ArgumentOutOfRangeException. Seperti yang ditunjukkan oleh output dari contoh, pengecualian hanya muncul ketika Anda melakukan iterasi angka saja, dan bukan saat Anda mengambil enumerator.

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

Apabila Anda memasukkan logika iterator ke dalam fungsi lokal, pengecualian validasi argumen dilemparkan saat Anda mengambil enumerator, seperti yang ditunjukkan oleh contoh berikut:

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

Fungsi lokal vs. ekspresi lambda

Jika dilihat sekilas, fungsi lokal dan ekspresi lambda sangat mirip. Dalam banyak kasus, pilihan antara menggunakan ekspresi-ekspresi lambda dan fungsi lokal adalah tentang gaya dan preferensi pribadi. Namun, ada perbedaan nyata di mana Anda bisa menggunakan satu atau yang lainnya yang harus Anda waspadai.

Mari kita periksa perbedaan antara implementasi fungsi lokal serta ekspresi lambda dari algoritma faktorial. Berikut adalah versi menggunakan fungsi lokal:

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

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

Versi ini menggunakan ekspresi 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);
}

Penamaan

Fungsi lokal secara eksplisit dinamai seperti metode-metode. Ekspresi lambda adalah metode-metode anonim dan perlu ditetapkan ke variabel dari jenis delegate, biasanya Action atau Func. Ketika Anda mendeklarasikan fungsi lokal, prosesnya seperti menulis metode normal; Anda mendeklarasikan jenis pengembalian serta tanda tangan fungsi.

Tanda tangan fungsi serta jenis ekspresi lambda

Ekspresi lambda mengandalkan jenis dari variabel Action/Func yang ditetapkan untuk menentukan jenis argumen serta pengembalian. Dalam fungsi lokal, karena sintaksnya mirip seperti menulis metode normal, jenis argumen serta jenis pengembalian sudah menjadi bagian dari deklarasi fungsi.

Dimulai dengan C# 10, beberapa ekspresi lambda memiliki jenis alami, yang memungkinkan pengompilasi untuk menyimpulkan jenis pengembalian dan jenis parameter ekspresi lambda.

Penetapan yang pasti

Ekspresi lambda adalah objek yang dideklarasikan serta ditetapkan pada durasi. Agar ekspresi lambda dapat digunakan, ekspresi tersebut harus ditugaskan dengan pasti: variabel Action/Func yang akan ditetapkan harus dideklarasikan serta ekspresi lambda yang ditetapkan untuknya. Perhatikan bahwa LambdaFactorial harus mendeklarasikan serta menginisialisasi ekspresi lambda nthFactorial sebelum mendefinisikannya. Jika tidak melakukannya maka akan berakibat pada kesalahan waktu kompilasi untuk referensi nthFactorial sebelum menetapkannya.

Fungsi lokal didefinisikan pada waktu kompilasi. Karena tidak ditetapkan ke variabel, mereka dapat direferensikan dari lokasi kode mana pun di mana ia berada dalam cakupan; dalam contoh pertama kami LocalFunctionFactorial, kami bisa mendeklarasikan fungsi lokal kami baik di atas atau di bawah pernyataan return dan tidak memicu kesalahan pengompilasi apa pun.

Perbedaan ini berarti bahwa algoritma berulang lebih mudah dibuat menggunakan fungsi lokal. Anda bisa mendeklarasikan serta menentukan fungsi lokal yang memanggil dirinya sendiri. Ekspresi lambda harus dideklarasikan, dan diberi nilai default sebelum dapat ditetapkan kembali ke badan yang mereferensikan ekspresi lambda yang sama.

Implementasi sebagai delegasi

Ekspresi lambda dikonversi menjadi delegasi saat mereka dinyatakan. Fungsi lokal lebih fleksibel karena dapat ditulis seperti metode tradisional atau sebagai delegasi. Fungsi lokal hanya dikonversi ke delegasi saat digunakan sebagai delegasi.

Jika Anda mendeklarasikan fungsi lokal dan hanya mereferensikannya dengan memanggilnya seperti metode, fungsi tersebut tidak akan dikonversi ke delegasi.

Pengambilan variabel

Aturan dari penugasan yang pasti juga memengaruhi variabel apa pun yang ditangkap oleh fungsi lokal atau ekspresi lambda. Pengompilasi dapat melakukan analisis statik yang memungkinkan fungsi lokal untuk secara pasti menetapkan variabel yang diambil dalam cakupan penutup. Pertimbangkan contoh ini:

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

    void LocalFunction() => y = 0;
}

Pengompilasi bisa menentukan bahwa LocalFunction pasti menetapkan y ketika dipanggil. Karena LocalFunction dipanggil sebelum pernyataan return, y pasti ditugaskan pada pernyataan return.

Perhatikan bahwa ketika fungsi lokal menangkap variabel dalam cakupan penutup, fungsi lokal diimplementasikan sebagai jenis delegasi.

Alokasi timbunan

Tergantung pada penggunaannya, fungsi lokal bisa menghindari alokasi timbunan yang selalu diperlukan untuk ekspresi lambda. Jika fungsi lokal tidak pernah dikonversi ke delegasi, dan tidak ada variabel yang diambil oleh fungsi lokal yang ditangkap oleh lambda lain atau fungsi lokal yang dikonversi menjadi delegasi, pengompilasi bisa menghindari alokasi timbunan.

Pertimbangkan contoh asinkron ini:

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

Penutupan untuk ekspresi lambda ini berisi variabel address, index dan name. Dalam kasus fungsi lokal, objek yang mengimplementasikan penutupan mungkin merupakan jenis struct. Jenis struktur tersebut akan diteruskan oleh referensi ke fungsi lokal. Perbedaan pada implementasi ini akan menghemat alokasi.

Instansiasi yang diperlukan untuk ekspresi lambda berarti alokasi memori tambahan, yang mungkin merupakan faktor performa dalam jalur kode kritis waktu. Fungsi lokal tidak menimbulkan overhead ini. Dalam contoh di atas, versi fungsi lokal memiliki dua alokasi yang lebih sedikit daripada versi ekspresi lambda.

Apabila Anda tahu bahwa fungsi lokal Anda tidak akan dikonversi ke delegasi dan tidak ada variabel yang ditangkap oleh lambda atau fungsi lokal lain yang dikonversi menjadi delegasi, Anda dapat menjamin bahwa fungsi lokal Anda menghindari dialokasikan pada timbunan dengan menyatakannya sebagai fungsi lokal static.

Tip

Aktifkan aturan gaya IDE0062 kode .NET untuk memastikan bahwa fungsi lokal selalu ditandai static.

Catatan

Fungsi lokal yang setara dengan metode ini juga menggunakan kelas untuk penutupannya. Baik penutupan untuk fungsi lokal diimplementasikan sebagai class atau struct merupakan detail implementasi. Fungsi lokal dapat menggunakan struct sedangkan lambda akan selalu menggunakan 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.";
    }
}

Penggunaan dari kata kunci yield

Salah satu keuntungan akhir yang tidak ditunjukkan dalam sampel ini adalah bahwa fungsi lokal dapat diimplementasikan sebagai iterator, menggunakan sintaks yield return untuk menghasilkan urutan nilai.

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

Pernyataan yield return tidak diperbolehkan dalam ekspresi lambda. Untuk informasi selengkapnya, lihat kesalahan pengkompilasi CS1621.

Meskipun fungsi lokal mungkin tampak berlebihan untuk ekspresi lambda, fungsi tersebut benar-benar melayani tujuan yang berbeda serta memiliki penggunaan yang berbeda. Fungsi lokal lebih efisien untuk kasus ini ketika Anda ingin menulis fungsi yang hanya dipanggil dari konteks metode yang lain.

Spesifikasi bahasa C#

Untuk informasi selengkapnya, lihat bagian Deklarasi fungsi lokal dari spesifikasi bahasa C#.

Lihat juga