Metode Ekstensi (Panduan Pemrograman C#)

Metode ekstensi memungkinkan Anda untuk “menambahkan” metode ke jenis yang ada tanpa membuat jenis turunan baru, kompilasi ulang, atau mengubah jenis aslinya. Metode ekstensi merupakan metode statis, tetapi dipanggil seolah-olah metode tersebut adalah metode instans pada jenis yang diperluas. Untuk kode klien yang ditulis dalam C#, F# dan Visual Basic, tidak ada perbedaan yang jelas antara pemanggilan metode ekstensi dan metode yang ditentukan dalam jenis.

Metode ekstensi yang paling umum adalah operator kueri standar LINQ yang menambahkan fungsionalitas kueri ke jenis System.Collections.IEnumerable dan System.Collections.Generic.IEnumerable<T> yang sudah ada. Untuk menggunakan operator kueri standar, pertama-tama bawa ke dalam cakupan dengan direktif using System.Linq. Kemudian jenis apa pun yang menerapkan IEnumerable<T> yang tampaknya memiliki metode instans seperti GroupBy, OrderBy, Average, dan sebagainya. Anda dapat melihat metode tambahan ini dalam penyelesaian pernyataan IntelliSense saat Anda mengetik "titik" setelah instans jenis IEnumerable<T> seperti List<T> atau Array.

Contoh OrderBy

Contoh berikut menunjukkan cara memanggil operator kueri standar metode OrderBy pada array bilangan bulat. Ekspresi dalam tanda kurung merupakan ekspresi lambda. Banyak operator kueri standar menggunakan ekspresi lambda sebagai parameter, tetapi ini bukan persyaratan untuk metode ekstensi. Untuk informasi selengkapnya, lihat Ekspresi Lambda.

class ExtensionMethods2
{

    static void Main()
    {
        int[] ints = [10, 45, 15, 39, 21, 26];
        var result = ints.OrderBy(g => g);
        foreach (var i in result)
        {
            System.Console.Write(i + " ");
        }
    }
}
//Output: 10 15 21 26 39 45

Metode ekstensi didefinisikan sebagai metode statis tetapi dipanggil dengan menggunakan sintaks metode instans. Parameter pertamanya menentukan jenis metode yang dioperasikan. Parameter mengikuti pengubah ini . Metode ekstensi hanya berada dalam cakupan saat Anda secara eksplisit mengimpor namespace layanan ke dalam kode sumber Anda dengan direktif using.

Contoh berikut menunjukkan metode ekstensi yang ditentukan untuk kelas System.String. Ini didefinisikan di dalam kelas statis tidak berlapis dan bukan generik:

namespace ExtensionMethods
{
    public static class MyExtensions
    {
        public static int WordCount(this string str)
        {
            return str.Split(new char[] { ' ', '.', '?' },
                             StringSplitOptions.RemoveEmptyEntries).Length;
        }
    }
}

Metode ekstensi WordCount dapat dibawa ke dalam cakupan dengan direktif using ini:

using ExtensionMethods;

Dan ia dapat dipanggil dari aplikasi dengan menggunakan sintaks ini:

string s = "Hello Extension Methods";
int i = s.WordCount();

Anda memanggil metode ekstensi dalam kode Anda dengan sintaks metode instans. Bahasa perantara (IL) yang dihasilkan oleh kompilator menerjemahkan kode Anda ke dalam panggilan pada metode statis. Prinsip enkapulasi tidak benar-benar dilanggar. Metode ekstensi tidak dapat mengakses variabel privat dalam jenis yang diperluas.

Baik kelas MyExtensions maupun metode WordCount merupakan static, dan dapat diakses seperti semua anggota static lainnya. Metode WordCount ini dapat dipanggil seperti metode static lainnya dengan cara berikut:

string s = "Hello Extension Methods";
int i = MyExtensions.WordCount(s);

Kode C# sebelumnya:

  • Deklarasikan dan tetapkan string baru bernama s dengan nilai "Hello Extension Methods".
  • Memanggil argumen syang MyExtensions.WordCount diberikan .

Untuk informasi selengkapnya, lihat Cara menerapkan dan memanggil metode ekstensi kustom.

Umumnya, Anda mungkin akan memanggil metode ekstensi jauh lebih sering daripada menerapkan metode Anda sendiri. Karena metode ekstensi dipanggil dengan menggunakan sintaks metode instans, tidak ada pengetahuan khusus yang diperlukan untuk menggunakannya dari kode klien. Untuk mengaktifkan metode ekstensi untuk jenis tertentu, cukup tambahkan direktif using untuk namespace layanan tempat metode ditentukan. Misalnya, untuk menggunakan operator kueri standar, tambahkan direktif using ini ke kode Anda:

using System.Linq;

(Anda mungkin juga perlu menambahkan referensi ke System.Core.dll.) Anda akan melihat bahwa operator kueri standar sekarang muncul di IntelliSense sebagai metode tambahan yang tersedia untuk sebagian besar jenis IEnumerable<T>.

Metode Ekstensi Pengikatan pada Waktu Kompilasi

Anda dapat menggunakan metode ekstensi untuk memperluas kelas atau antarmuka, tetapi tidak untuk mengambil alihnya. Metode ekstensi dengan nama dan tanda tangan yang sama dengan antarmuka atau metode kelas tidak akan pernah dipanggil. Pada waktu kompilasi, metode ekstensi selalu memiliki prioritas yang lebih rendah daripada metode instans yang ditentukan dalam jenis itu sendiri. Dengan kata lain, jika jenis memiliki metode bernama Process(int i), dan Anda memiliki metode ekstensi dengan tanda tangan yang sama, kompilator akan selalu mengikat ke metode instans. Ketika kompilator menemukan pemanggilan metode, pertama-tama ia mencari kecocokan dalam metode instans jenis. Jika tidak ada kecocokan yang ditemukan, ia mencari metode ekstensi apa pun yang ditentukan untuk jenis tersebut, dan mengikat ke metode ekstensi pertama yang ditemukannya.

Contoh

Contoh berikut menunjukkan aturan yang diikuti kompilator C# dalam menentukan apakah akan mengikat panggilan metode ke metode instans pada jenis, atau ke metode ekstensi. Kelas statis Extensions berisi metode ekstensi yang ditentukan untuk semua jenis yang menerapkan IMyInterface. Kelas A, B, dan C semua menerapkan antarmuka.

Metode ekstensi MethodB tidak pernah dipanggil karena nama dan tanda tangannya sama persis dengan metode yang sudah diterapkan oleh kelas.

Ketika kompilator tidak dapat menemukan metode instans dengan tanda tangan yang cocok, pengikatannya akan mengikat ke metode ekstensi yang cocok jika ada.

// Define an interface named IMyInterface.
namespace DefineIMyInterface
{
    public interface IMyInterface
    {
        // Any class that implements IMyInterface must define a method
        // that matches the following signature.
        void MethodB();
    }
}

// Define extension methods for IMyInterface.
namespace Extensions
{
    using System;
    using DefineIMyInterface;

    // The following extension methods can be accessed by instances of any
    // class that implements IMyInterface.
    public static class Extension
    {
        public static void MethodA(this IMyInterface myInterface, int i)
        {
            Console.WriteLine
                ("Extension.MethodA(this IMyInterface myInterface, int i)");
        }

        public static void MethodA(this IMyInterface myInterface, string s)
        {
            Console.WriteLine
                ("Extension.MethodA(this IMyInterface myInterface, string s)");
        }

        // This method is never called in ExtensionMethodsDemo1, because each
        // of the three classes A, B, and C implements a method named MethodB
        // that has a matching signature.
        public static void MethodB(this IMyInterface myInterface)
        {
            Console.WriteLine
                ("Extension.MethodB(this IMyInterface myInterface)");
        }
    }
}

// Define three classes that implement IMyInterface, and then use them to test
// the extension methods.
namespace ExtensionMethodsDemo1
{
    using System;
    using Extensions;
    using DefineIMyInterface;

    class A : IMyInterface
    {
        public void MethodB() { Console.WriteLine("A.MethodB()"); }
    }

    class B : IMyInterface
    {
        public void MethodB() { Console.WriteLine("B.MethodB()"); }
        public void MethodA(int i) { Console.WriteLine("B.MethodA(int i)"); }
    }

    class C : IMyInterface
    {
        public void MethodB() { Console.WriteLine("C.MethodB()"); }
        public void MethodA(object obj)
        {
            Console.WriteLine("C.MethodA(object obj)");
        }
    }

    class ExtMethodDemo
    {
        static void Main(string[] args)
        {
            // Declare an instance of class A, class B, and class C.
            A a = new A();
            B b = new B();
            C c = new C();

            // For a, b, and c, call the following methods:
            //      -- MethodA with an int argument
            //      -- MethodA with a string argument
            //      -- MethodB with no argument.

            // A contains no MethodA, so each call to MethodA resolves to
            // the extension method that has a matching signature.
            a.MethodA(1);           // Extension.MethodA(IMyInterface, int)
            a.MethodA("hello");     // Extension.MethodA(IMyInterface, string)

            // A has a method that matches the signature of the following call
            // to MethodB.
            a.MethodB();            // A.MethodB()

            // B has methods that match the signatures of the following
            // method calls.
            b.MethodA(1);           // B.MethodA(int)
            b.MethodB();            // B.MethodB()

            // B has no matching method for the following call, but
            // class Extension does.
            b.MethodA("hello");     // Extension.MethodA(IMyInterface, string)

            // C contains an instance method that matches each of the following
            // method calls.
            c.MethodA(1);           // C.MethodA(object)
            c.MethodA("hello");     // C.MethodA(object)
            c.MethodB();            // C.MethodB()
        }
    }
}
/* Output:
    Extension.MethodA(this IMyInterface myInterface, int i)
    Extension.MethodA(this IMyInterface myInterface, string s)
    A.MethodB()
    B.MethodA(int i)
    B.MethodB()
    Extension.MethodA(this IMyInterface myInterface, string s)
    C.MethodA(object obj)
    C.MethodA(object obj)
    C.MethodB()
 */

Pola Penggunaan Umum

Fungsionalitas Koleksi

Di masa lalu, adalah umum untuk membuat "Kelas Koleksi" yang menerapkan antarmuka System.Collections.Generic.IEnumerable<T> untuk jenis tertentu dan berisi fungsionalitas yang bertindak pada koleksi jenis tersebut. Meskipun tidak ada yang salah dalam pembuatan jenis objek koleksi ini, fungsionalitas yang sama dapat dicapai dengan menggunakan ekstensi pada System.Collections.Generic.IEnumerable<T>. Ekstensi memiliki keuntungan dengan memungkinkan fungsionalitas dipanggil dari koleksi apa pun seperti System.Array atau System.Collections.Generic.List<T> yang menerapkan System.Collections.Generic.IEnumerable<T> pada jenis tersebut. Contoh penggunaan Array Int32 ini dapat ditemukan sebelumnya di artikel ini.

Fungsionalitas Lapisan-Spesifik

Saat menggunakan Arsitektur Bawang atau desain aplikasi berlapis lainnya, umumnya memiliki sekumpulan Entitas Domain atau Objek Transfer Data yang dapat digunakan untuk berkomunikasi di seluruh batas aplikasi. Berbagai objek ini umumnya tidak berisi fungsionalitas, atau hanya fungsionalitas minimal yang berlaku untuk semua lapisan aplikasi. Metode ekstensi dapat digunakan untuk menambahkan fungsionalitas yang spesifik untuk setiap lapisan aplikasi tanpa memuat objek ke bawah dengan metode yang tidak diperlukan atau diinginkan di lapisan lain.

public class DomainEntity
{
    public int Id { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }
}

static class DomainEntityExtensions
{
    static string FullName(this DomainEntity value)
        => $"{value.FirstName} {value.LastName}";
}

Memperluas Jenis yang Telah Ditentukan Sebelumnya

Daripada membuat objek baru ketika fungsionalitas yang dapat digunakan kembali perlu dibuat, kita biasanya dapat memperluas jenis yang ada, seperti jenis .NET atau CLR. Sebagai contoh, jika kita tidak menggunakan metode ekstensi, kita mungkin membuat kelas Engine atau Query untuk melakukan pekerjaan eksekusi kueri pada SQL Server yang mungkin dipanggil dari beberapa tempat dalam kode kita. Namun kita dapat memperluas kelas System.Data.SqlClient.SqlConnection menggunakan metode ekstensi untuk melakukan kueri tersebut dari mana saja saat kita memiliki koneksi ke SQL Server. Contoh lain mungkin adalah menambahkan fungsionalitas umum ke System.String kelas, memperluas kemampuan System.IO.Stream pemrosesan data objek, dan System.Exception objek untuk fungsionalitas penanganan kesalahan tertentu. Berbagai jenis kasus penggunaan ini hanya dibatasi oleh imajinasi dan pemahaman yang baik dari Anda.

Perluasan jenis yang telah ditentukan sebelumnya bisa sulit dengan jenis struct karena diteruskan oleh nilai ke metode. Itu berarti setiap perubahan pada struct dilakukan pada salinan struct. Perubahan tersebut tidak terlihat setelah metode ekstensi keluar. Anda dapat menambahkan pengubah ref ke argumen pertama yang ref menjadikannya metode ekstensi. Kata ref kunci dapat muncul sebelum atau sesudah this kata kunci tanpa perbedaan semantik. Menambahkan pengubah ref menunjukkan bahwa argumen pertama diteruskan oleh referensi. Ini memungkinkan Anda menulis metode ekstensi yang mengubah status struktur yang diperluas (perhatikan bahwa anggota privat tidak dapat diakses). Hanya jenis nilai atau jenis generik yang dibatasi untuk distruksi (lihat struct batasan untuk informasi selengkapnya) yang diizinkan sebagai parameter pertama dari ref metode ekstensi. Contoh berikut menunjukkan cara menggunakan ref metode ekstensi untuk memodifikasi jenis bawaan secara langsung tanpa perlu menetapkan ulang hasilnya atau meneruskannya melalui fungsi dengan ref kata kunci:

public static class IntExtensions
{
    public static void Increment(this int number)
        => number++;

    // Take note of the extra ref keyword here
    public static void RefIncrement(this ref int number)
        => number++;
}

public static class IntProgram
{
    public static void Test()
    {
        int x = 1;

        // Takes x by value leading to the extension method
        // Increment modifying its own copy, leaving x unchanged
        x.Increment();
        Console.WriteLine($"x is now {x}"); // x is now 1

        // Takes x by reference leading to the extension method
        // RefIncrement changing the value of x directly
        x.RefIncrement();
        Console.WriteLine($"x is now {x}"); // x is now 2
    }
}

Contoh berikutnya ini menunjukkan ref metode ekstensi untuk jenis struct yang ditentukan pengguna:

public struct Account
{
    public uint id;
    public float balance;

    private int secret;
}

public static class AccountExtensions
{
    // ref keyword can also appear before the this keyword
    public static void Deposit(ref this Account account, float amount)
    {
        account.balance += amount;

        // The following line results in an error as an extension
        // method is not allowed to access private members
        // account.secret = 1; // CS0122
    }
}

public static class AccountProgram
{
    public static void Test()
    {
        Account account = new()
        {
            id = 1,
            balance = 100f
        };

        Console.WriteLine($"I have ${account.balance}"); // I have $100

        account.Deposit(50f);
        Console.WriteLine($"I have ${account.balance}"); // I have $150
    }
}

Panduan Umum

Meskipun masih dianggap lebih baik untuk menambahkan fungsionalitas dengan mengubah kode objek atau memperoleh jenis baru setiap kali masuk akal dan memungkinkan untuk melakukannya, metode ekstensi telah menjadi opsi penting untuk pembuatan fungsionalitas yang dapat digunakan kembali di seluruh ekosistem .NET. Untuk kesempatan seperti itu ketika sumber asli tidak berada di bawah kendali Anda, ketika objek turunan tidak pantas atau tidak mungkin, atau ketika fungsionalitas tidak boleh diekspos di luar cakupan yang berlaku, metode Ekstensi adalah pilihan yang sangat baik.

Untuk informasi selengkapnya tentang jenis turunan, lihat Warisan.

Saat menggunakan metode ekstensi untuk memperluas jenis yang kode sumbernya tidak Anda kendalikan, Anda mengambil risiko kalau perubahan dalam penerapan jenis akan menyebabkan metode ekstensi Anda rusak.

Jika Anda menerapkan metode ekstensi untuk jenis tertentu, ingat poin-poin berikut:

  • Metode ekstensi tidak dipanggil jika memiliki tanda tangan yang sama dengan metode yang ditentukan dalam jenis .
  • Metode ekstensi dibawa ke dalam cakupan di tingkat namespace layanan. Misalnya, jika Anda memiliki beberapa kelas statis yang berisi metode ekstensi dalam satu namespace layanan bernama Extensions, semuanya akan dibawa ke dalam cakupan oleh direktif using Extensions;.

Untuk pustaka kelas yang Anda terapkan, Anda tidak boleh menggunakan metode ekstensi untuk menghindari penambahan nomor versi perakitan. Jika Anda ingin menambahkan fungsionalitas signifikan ke pustaka lokasi kode sumber Anda, ikuti panduan .NET untuk penerapan versi perakitan. Untuk informasi selengkapnya, lihat Penerapan Versi Rakitan.

Lihat juga