Bagikan melalui


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 dapat melihat bahwa metode itu tidak dapat dipanggil kecuali oleh metode yang mengandungnya. 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 kuncivaluekontekstual . 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 dapat menyertakan pengubah akses anggota. Karena semua fungsi lokal bersifat privat, termasuk pengubah akses, seperti kata kunci private, menghasilkan kesalahan kompilator 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

Sekilas, fungsi lokal dan ekspresi lambda serupa. 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.

Beberapa ekspresi lambda memiliki jenis alami , yang memungkinkan pengkompilasi 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 secara pasti: variabel Action/Func yang ditetapkan harus dideklarasikan dan 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 apa pun di mana ia berada dalam cakupan; dalam contoh pertama LocalFunctionFactorial, Anda dapat mendeklarasikan fungsi lokal baik sebelum atau sesudah pernyataan return dan tidak memicu kesalahan kompilator 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 isi 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 penetapan 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.

Ketika fungsi lokal menangkap variabel dalam cakupan di sekitarnya, fungsi lokal diimplementasikan menggunakan closure, seperti jenis tipe 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. Untuk fungsi lokal, objek yang mengimplementasikan penutupan dapat menjadi 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 yang kritis terhadap waktu. Fungsi lokal tidak menimbulkan overhead ini.

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.

Petunjuk / Saran

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 diizinkan dalam ekspresi lambda. Untuk informasi selengkapnya, lihat kesalahan pengkompilasi CS1621.

Meskipun fungsi lokal dapat tampak berlebihan untuk ekspresi lambda, fungsi tersebut benar-benar melayani tujuan yang berbeda dan 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