Foto oleh Myburgh Roux via Pexels

Polymorphism, yang berarti 'banyak bentuk', adalah kemampuan satu interface atau method yang sama untuk berperilaku berbeda tergantung pada object yang sebenarnya memanggilnya. Konsep ini dicapai melalui dua mekanisme utama di C#: overriding dan overloading.

Overriding mengganti implementasi method dari class induk dengan versi baru di class turunan (seperti dibahas pada artikel inheritance), sementara overloading adalah mendefinisikan beberapa method dengan nama sama namun signature parameter yang berbeda dalam class yang sama.

Artikel ini membahas perbedaan mendasar antara overriding dan overloading, bagaimana keduanya sama-sama disebut polymorphism namun bekerja dengan cara yang sangat berbeda, serta contoh praktis penerapannya dalam skenario nyata.

Runtime Polymorphism Lewat Override

Method Luas berperilaku berbeda sesuai tipe object sebenarnya

public class Bentuk { public virtual double Luas() => 0; } public class Lingkaran : Bentuk { public double Jari { get; set; } public override double Luas() => Math.PI * Jari * Jari; } public class Persegi : Bentuk { public double Sisi { get; set; } public override double Luas() => Sisi * Sisi; } List<Bentuk> bentukList = new List<Bentuk> { new Lingkaran { Jari = 5 }, new Persegi { Sisi = 4 } }; foreach (var b in bentukList) Console.WriteLine(b.Luas());
Meski variabel bentukList bertipe List, pemanggilan b.Luas() akan menjalankan implementasi yang sesuai dengan tipe sebenarnya dari setiap object, baik Lingkaran maupun Persegi, berkat mekanisme virtual dispatch yang menentukan implementasi mana yang dijalankan berdasarkan tipe runtime, bukan tipe deklarasi variabel.

Compile-Time Polymorphism Lewat Overload

Compiler memilih overload berdasarkan tipe dan jumlah parameter

public class Kalkulator { public int Tambah(int a, int b) => a + b; public double Tambah(double a, double b) => a + b; public int Tambah(int a, int b, int c) => a + b + c; } var k = new Kalkulator(); Console.WriteLine(k.Tambah(2, 3)); -- Expected: 5 Console.WriteLine(k.Tambah(2.5, 3.5)); -- Expected: 6.0 Console.WriteLine(k.Tambah(1, 2, 3)); -- Expected: 6
Method overloading memungkinkan nama method yang sama dipakai untuk menangani variasi tipe atau jumlah parameter yang berbeda, dengan compiler yang menentukan overload mana yang dipanggil berdasarkan argumen yang diberikan saat kompilasi, berbeda dengan override yang ditentukan saat runtime berdasarkan tipe object sebenarnya.

Abstract Method untuk Memaksa Override

Abstract method wajib di-override oleh semua class turunan

public abstract class Bentuk { public abstract double Luas(); // tidak ada implementasi sama sekali } public class Segitiga : Bentuk { public double Alas { get; set; } public double Tinggi { get; set; } public override double Luas() => 0.5 * Alas * Tinggi; }
Abstract method tidak memiliki implementasi sama sekali di base class dan memaksa setiap class turunan untuk menyediakan implementasinya sendiri, berbeda dengan virtual method yang sifatnya opsional untuk di-override. Class yang memiliki abstract method otomatis harus dideklarasikan sebagai abstract class dan tidak bisa langsung dibuat objectnya.

Polymorphism Lewat Interface

Interface memungkinkan polymorphism antar class yang tidak sekerabat

public interface IBisaTerbang { void Terbang(); } public class Burung : IBisaTerbang { public void Terbang() => Console.WriteLine("Burung mengepakkan sayap"); } public class Pesawat : IBisaTerbang { public void Terbang() => Console.WriteLine("Pesawat menyalakan mesin jet"); } List<IBisaTerbang> bisaDiTerbangkan = new List<IBisaTerbang> { new Burung(), new Pesawat() }; foreach (var item in bisaDiTerbangkan) item.Terbang();
Interface memungkinkan polymorphism antar class yang bahkan tidak memiliki hubungan inheritance sama sekali, selama keduanya mengimplementasikan interface yang sama. Burung dan Pesawat sama sekali tidak berkerabat secara hierarki, namun keduanya bisa diperlakukan seragam lewat interface IBisaTerbang.

Dampak Lupa Kata Kunci override Terhadap Polymorphism

Tanpa override, pemanggilan lewat reference parent tidak bersifat polymorphic

class Hewan { public virtual string Suara() => "..."; } class Kucing : Hewan { // Lupa menuliskan 'override', hanya 'new' implisit public string Suara() => "Meong"; } Hewan h = new Kucing(); Console.WriteLine(h.Suara()); // Output: ... (BUKAN 'Meong', karena polymorphism tidak aktif)

Kesalahan yang sering tidak disadari saat menerapkan polymorphism adalah lupa menuliskan kata kunci override pada method turunan yang dimaksudkan untuk menimpa method virtual milik parent. Tanpa override, compiler akan menganggap method Suara() pada Kucing sebagai method baru yang berdiri sendiri (perilaku method hiding), bukan sebagai penimpaan method parent, sehingga ketika dipanggil melalui reference bertipe parent seperti variabel h, yang dieksekusi tetaplah versi milik Hewan.

Polymorphism yang sebenarnya hanya berjalan ketika method pada parent ditandai virtual (atau abstract) dan method pada class turunan ditandai override, karena kombinasi inilah yang membuat CLR melakukan dynamic dispatch, yaitu menentukan implementasi method mana yang dijalankan berdasarkan tipe objek sesungguhnya saat runtime, bukan berdasarkan tipe variabel yang dideklarasikan saat compile time.

Selain override, C# juga mengenal method overloading, yaitu beberapa method dengan nama sama namun signature parameter berbeda dalam class yang sama. Overloading diselesaikan oleh compiler saat compile time (static binding) berdasarkan tipe dan jumlah argumen yang dipakai saat pemanggilan, sehingga sifatnya sangat berbeda dari overriding yang diselesaikan saat runtime.

Operator Overloading untuk Kustomisasi Perilaku Operator pada Class

Operator + didefinisikan ulang agar bisa dipakai pada objek Uang

public class Uang { public decimal Nominal { get; } public Uang(decimal nominal) => Nominal = nominal; public static Uang operator +(Uang a, Uang b) => new Uang(a.Nominal + b.Nominal); public override string ToString() => $"Rp{Nominal:N0}"; } var dompet1 = new Uang(50000); var dompet2 = new Uang(25000); Uang total = dompet1 + dompet2; Console.WriteLine(total); // Output: Rp75.000

Operator overloading memungkinkan operator bawaan seperti +, -, ==, atau < didefinisikan ulang agar dapat bekerja secara bermakna pada tipe data kustom buatan sendiri, dengan cara mendeklarasikan method static bernama operator diikuti simbol operator yang ingin didefinisikan ulang. Pada contoh di atas, operator + pada class Uang didefinisikan agar menjumlahkan nilai Nominal dari dua objek Uang dan mengembalikan objek Uang baru sebagai hasilnya, alih-alih menimbulkan compile error seperti jika operator ini tidak didefinisikan.

Fitur ini merupakan bentuk polymorphism ad-hoc, karena operator yang sama (+) berperilaku berbeda tergantung tipe operand yang dipakai, baik untuk angka biasa, string yang digabungkan, maupun objek kustom seperti Uang yang perilakunya didefinisikan sendiri oleh developer. Beberapa operator seperti == dan != biasanya didefinisikan berpasangan, karena compiler akan memberikan warning jika hanya salah satunya yang dioverload tanpa pasangannya.

Operator overloading sebaiknya dipakai secukupnya dan hanya ketika maknanya benar-benar intuitif bagi pembaca kode, karena penyalahgunaannya dapat membuat kode menjadi sulit dipahami ketika operator yang terlihat sederhana ternyata menjalankan logika tersembunyi yang kompleks atau memiliki efek samping yang tidak terduga. Sebagai alternatif yang lebih eksplisit, method biasa seperti Tambah() seringkali lebih disarankan ketika operasi yang dilakukan tidak sepenuhnya mengikuti konvensi matematika umum. Selain operator aritmatika dan perbandingan, C# juga memperbolehkan overloading operator konversi implisit maupun eksplisit antar tipe kustom, sehingga objek seperti Uang bisa dikonversi otomatis ke decimal atau sebaliknya tanpa perlu method konversi terpisah.

Enum dan FlagsAttribute untuk Menggabungkan Beberapa Nilai

[Flags] public enum Akses { Baca = 1, Tulis = 2, Hapus = 4 } var hakAkses = Akses.Baca | Akses.Tulis; Console.WriteLine(hakAkses); Console.WriteLine(hakAkses.HasFlag(Akses.Hapus)); // Output: // Baca, Tulis // False

Enum biasa digunakan untuk merepresentasikan satu pilihan dari sekumpulan nilai tetap, namun dengan menambahkan atribut [Flags] dan memberi setiap anggota nilai berupa pangkat dua (1, 2, 4, 8, dan seterusnya), enum dapat digabungkan menggunakan operator bitwise OR (|) untuk merepresentasikan beberapa kombinasi nilai sekaligus dalam satu variabel, seperti hak akses Baca dan Tulis yang aktif bersamaan pada contoh di atas. Tanpa pola nilai pangkat dua ini, kombinasi antar anggota enum bisa saling tumpang tindih dan menghasilkan nilai yang ambigu.

Method HasFlag() memudahkan pengecekan apakah sebuah kombinasi mengandung nilai tertentu tanpa perlu menulis operasi bitwise AND manual, seperti pada contoh yang memeriksa apakah hakAkses mencakup Akses.Hapus dan mengembalikan false karena belum ditambahkan ke kombinasi. Pola ini umum dipakai untuk merepresentasikan hak akses pengguna, status gabungan suatu proses, atau opsi konfigurasi yang bisa aktif lebih dari satu secara bersamaan tanpa harus membuat banyak variabel boolean terpisah untuk setiap kemungkinan kombinasi yang ada.

Kesalahan Umum yang Sering Terjadi

Mengira Overloading dan Overriding Adalah Hal yang Sama

Overloading terjadi dalam satu class dengan signature parameter berbeda dan ditentukan saat kompilasi, sementara overriding terjadi antara base class dan derived class dengan signature yang sama persis dan ditentukan saat runtime; mencampurkan keduanya adalah kesalahan konsep yang umum terjadi.

Lupa Abstract Class Tidak Bisa Dibuat Objectnya Langsung

Mencoba membuat object langsung dari abstract class dengan new akan menghasilkan error kompilasi, karena abstract class memang dirancang hanya sebagai cetakan bagi class turunannya, bukan untuk dipakai langsung.

Overload yang Ambigu Antar Tipe yang Mirip

Mendefinisikan overload dengan parameter yang terlalu mirip, misalnya int dan long yang bisa saling dikonversi implisit, bisa menyebabkan ambiguitas yang membuat compiler bingung menentukan overload mana yang dimaksud.

Kapan Pendekatan Ini Tidak Cocok Dipakai

Override (runtime polymorphism) memberikan fleksibilitas untuk mengubah perilaku berdasarkan tipe object sebenarnya saat program berjalan, sangat berguna untuk desain yang perlu diperluas tanpa mengubah kode yang sudah ada (mengikuti Open-Closed Principle).

Overload (compile-time polymorphism) memberikan kenyamanan API yang lebih natural bagi pemanggil method, memungkinkan nama method yang sama dipakai untuk variasi parameter yang masuk akal, namun harus dirancang hati-hati agar tidak menimbulkan ambiguitas antar overload yang mirip.

Ringkasan

Polymorphism, baik lewat override maupun overload, memberikan fleksibilitas besar dalam desain OOP: override memungkinkan perilaku berbeda berdasarkan tipe runtime, sementara overload memberikan variasi parameter untuk method dengan nama sama. Pahami perbedaan keduanya dengan jelas, karena keduanya sering disebut dengan istilah yang sama namun bekerja dengan mekanisme yang sangat berbeda.

Seri Belajar C# Fundamental:

← Sebelumnya: Inheritance dan Penggunaannya di C#

→ Selanjutnya: Interface vs Abstract Class di C#