Bagikan melalui


Warisan dalam C# dan .NET

Tutorial ini memperkenalkan Anda pada warisan dalam C#. Warisan adalah fitur bahasa pemrograman berorientasi objek yang memungkinkan Anda mendefinisikan kelas dasar yang menyediakan fungsionalitas tertentu (data dan perilaku) serta untuk menentukan kelas turunan yang mewarisi atau mengesampingkan fungsionalitas tersebut.

Prasyarat

Menjalankan contoh

Untuk membuat dan menjalankan contoh dalam tutorial ini, Anda menggunakan utilitas dotnet dari baris perintah. Ikuti langkah-langkah ini untuk setiap contoh:

  1. Buat direktori untuk menyimpan contoh.

  2. Masukkan perintah dotnet new console pada perintah untuk membuat proyek .NET Core baru.

  3. Salin dan tempel kode dari contoh ke editor kode Anda.

  4. Masukkan perintah dotnet restore dari baris perintah untuk memuat atau memulihkan dependensi proyek.

    Anda tidak harus menjalankan dotnet restore karena dijalankan secara implisit oleh semua perintah yang memerlukan terjadinya pemulihan, seperti dotnet new, dotnet build, dotnet run, dotnet test, dotnet publish, dan dotnet pack. Untuk menonaktifkan pemulihan implisit, gunakan opsi --no-restore.

    Perintah dotnet restore masih berguna dalam skenario tertentu di mana pemulihan secara eksplisit masuk akal, seperti pembangunan integrasi berkelanjutan di Azure DevOps Services atau dalam sistem pembangunan yang perlu secara eksplisit mengontrol saat pemulihan terjadi.

    Untuk informasi tentang cara mengelola umpan NuGet, lihat dotnet restore dokumentasi.

  5. Masukkan perintah dotnet run untuk mengompilasi dan menjalankan contoh.

Latar Belakang: Apa itu warisan?

Warisan adalah salah satu atribut dasar pemrograman berorientasi objek. Hal ini memungkinkan Anda mendefinisikan kelas turunan yang menggunakan kembali (mewarisi), memperluas, atau memodifikasi perilaku kelas induk. Kelas yang anggotanya diwarisi disebut kelas dasar. Kelas yang mewarisi anggota kelas dasar disebut kelas turunan.

C# dan .NET hanya mendukung warisan tunggal. Artinya, sebuah kelas hanya dapat mewarisi dari satu kelas. Namun, warisan bersifat transitif, yang memungkinkan Anda menentukan hierarki warisan untuk sekumpulan jenis. Dengan kata lain, jenis D dapat mewarisi dari jenis C, yang mewarisi dari jenis B, yang mewarisi dari jenis kelas dasar A. Karena warisan bersifat transitif, anggota jenis A tersedia untuk jenis D.

Tidak semua anggota kelas dasar diwarisi oleh kelas turunan. Anggota berikut tidak diwariskan:

  • Konstruktor statis, yang menginisialisasi data statis suatu kelas.

  • Konstruktor instans, yang Anda panggil untuk membuat instans baru dari kelas. Setiap kelas harus mendefinisikan konstruktornya sendiri.

  • Finalizer, yang dipanggil oleh pengumpul sampah runtime untuk menghancurkan instans kelas.

Sementara semua anggota lain dari kelas dasar diwariskan oleh kelas turunan, terlihat atau tidaknya tergantung pada aksesibilitas mereka. Aksesibilitas anggota memengaruhi visibilitasnya untuk kelas turunan sebagai berikut:

  • Anggota privat hanya terlihat di kelas turunan yang disarangkan di kelas dasarnya. Jika tidak, mereka tidak akan terlihat di kelas turunan. Dalam contoh berikut, A.B adalah kelas berlapis yang berasal dari A, dan C berasal dari A. Bidang A._value privat terlihat di AB Namun, jika Anda menghapus komentar dari metode C.GetValue dan mencoba mengompilasi contoh, itu menghasilkan kesalahan pengompilasi CS0122: "'A._value' tidak dapat diakses karena tingkat perlindungannya."

    public class A
    {
        private int _value = 10;
    
        public class B : A
        {
            public int GetValue()
            {
                return _value;
            }
        }
    }
    
    public class C : A
    {
        //    public int GetValue()
        //    {
        //        return _value;
        //    }
    }
    
    public class AccessExample
    {
        public static void Main(string[] args)
        {
            var b = new A.B();
            Console.WriteLine(b.GetValue());
        }
    }
    // The example displays the following output:
    //       10
    
  • Anggota yang dilindungi hanya terlihat di kelas turunan.

  • Anggota internal hanya terlihat di kelas turunan yang terletak di rakitan yang sama dengan kelas dasar. Mereka tidak terlihat di kelas turunan yang terletak di rakitan yang berbeda dari kelas dasar.

  • Anggota publik terlihat di kelas turunan dan merupakan bagian dari antarmuka publik kelas turunan. Anggota yang diwariskan publik dapat dipanggil seolah-olah mereka didefinisikan di kelas turunan. Dalam contoh berikut, kelas A menentukan metode bernama Method1, dan kelas B mewarisi dari kelas A. Contohnya kemudian memanggil seolah-olah itu adalah metode instans Method1 pada B.

    public class A
    {
        public void Method1()
        {
            // Method implementation.
        }
    }
    
    public class B : A
    { }
    
    public class Example
    {
        public static void Main()
        {
            B b = new ();
            b.Method1();
        }
    }
    

Kelas turunan juga dapat mengganti anggota yang diwarisi dengan menyediakan implementasi alternatif. Agar dapat mengganti anggota, anggota di kelas dasar harus ditandai dengan kata kunci virtual. Secara default, anggota kelas dasar tidak ditandai sebagai virtual dan tidak dapat diganti. Mencoba mengambil alih anggota non-virtual, seperti contoh berikut, menghasilkan kesalahan kompilator CS0506: "<anggota> tidak dapat mengambil alih anggota> yang <diwariskan karena tidak ditandai virtual, abstrak, atau mengambil alih."

public class A
{
    public void Method1()
    {
        // Do something.
    }
}

public class B : A
{
    public override void Method1() // Generates CS0506.
    {
        // Do something else.
    }
}

Dalam beberapa kasus, kelas turunan harus mengganti implementasi kelas dasar. Anggota kelas dasar yang ditandai dengan kata kunci abstrak mengharuskan kelas turunan menggantinya. Mencoba mengompilasi contoh berikut menghasilkan kesalahan pengompilasi CS0534, "<kelas> tidak mengimplementasikan anggota abstrak yang diwarisi <anggota>", karena kelas B tidak menyediakan implementasi untuk A.Method1.

public abstract class A
{
    public abstract void Method1();
}

public class B : A // Generates CS0534.
{
    public void Method3()
    {
        // Do something.
    }
}

Warisan hanya berlaku untuk kelas dan antarmuka. Kategori jenis lain (struktur, delegasi, dan enum) tidak mendukung warisan. Karena aturan ini, mencoba mengompilasi kode seperti contoh berikut, menghasilkan kesalahan pengompilasi CS0527: "Jenis 'ValueType' di daftar antarmuka bukan antarmuka." Pesan kesalahan menunjukkan bahwa, meskipun Anda dapat menentukan antarmuka yang diimplementasikan struktur, warisan tidak didukung.

public struct ValueStructure : ValueType // Generates CS0527.
{
}

Warisan implisit

Selain jenis apa pun yang dapat mereka warisi melalui warisan tunggal, semua jenis dalam sistem jenis .NET secara implisit mewarisi dari Object atau jenis yang berasal darinya. Fungsi umum Object tersedia untuk semua jenis.

Untuk melihat arti warisan implisit, mari kita tentukan kelas baru, SimpleClass, itu hanyalah definisi kelas kosong:

public class SimpleClass
{ }

Anda kemudian dapat menggunakan refleksi (yang memungkinkan Anda memeriksa metadata jenis untuk mendapatkan informasi tentang jenis tersebut) untuk mendapatkan daftar anggota yang termasuk dalam jenis SimpleClass. Meskipun Anda belum menentukan anggota apa pun di kelas SimpleClass, output dari contoh menunjukkan bahwa kelas tersebut sebenarnya memiliki sembilan anggota. Salah satu anggota ini adalah konstruktor tanpa parameter (atau default) yang secara otomatis disediakan untuk jenis SimpleClass oleh pengompilasi C#. Delapan sisanya adalah anggota Object, jenis yang darinya semua kelas dan antarmuka dalam sistem jenis .NET akhirnya secara implisit mewarisi.

using System.Reflection;

public class SimpleClassExample
{
    public static void Main()
    {
        Type t = typeof(SimpleClass);
        BindingFlags flags = BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public |
                             BindingFlags.NonPublic | BindingFlags.FlattenHierarchy;
        MemberInfo[] members = t.GetMembers(flags);
        Console.WriteLine($"Type {t.Name} has {members.Length} members: ");
        foreach (MemberInfo member in members)
        {
            string access = "";
            string stat = "";
            var method = member as MethodBase;
            if (method != null)
            {
                if (method.IsPublic)
                    access = " Public";
                else if (method.IsPrivate)
                    access = " Private";
                else if (method.IsFamily)
                    access = " Protected";
                else if (method.IsAssembly)
                    access = " Internal";
                else if (method.IsFamilyOrAssembly)
                    access = " Protected Internal ";
                if (method.IsStatic)
                    stat = " Static";
            }
            string output = $"{member.Name} ({member.MemberType}): {access}{stat}, Declared by {member.DeclaringType}";
            Console.WriteLine(output);
        }
    }
}
// The example displays the following output:
//	Type SimpleClass has 9 members:
//	ToString (Method):  Public, Declared by System.Object
//	Equals (Method):  Public, Declared by System.Object
//	Equals (Method):  Public Static, Declared by System.Object
//	ReferenceEquals (Method):  Public Static, Declared by System.Object
//	GetHashCode (Method):  Public, Declared by System.Object
//	GetType (Method):  Public, Declared by System.Object
//	Finalize (Method):  Internal, Declared by System.Object
//	MemberwiseClone (Method):  Internal, Declared by System.Object
//	.ctor (Constructor):  Public, Declared by SimpleClass

Warisan implisit dari kelas Object membuat metode ini tersedia untuk kelas SimpleClass:

  • Metode publik ToString, yang mengonversi objek SimpleClass ke representasi stringnya, mengembalikan nama jenis yang sepenuhnya memenuhi syarat. Dalam hal ini, metode ToString mengembalikan string "SimpleClass".

  • Tiga metode yang menguji kesetaraan dua objek: metode Equals(Object) instans publik, metode Equals(Object, Object) statis publik, dan metode ReferenceEquals(Object, Object) statis publik. Secara default, metode ini menguji kesetaraan referensi; yaitu, agar sama, dua variabel objek harus merujuk ke objek yang sama.

  • Metode publik GetHashCode, yang menghitung nilai yang memungkinkan instans jenis digunakan dalam koleksi yang di-hash.

  • Metode publik GetType, yang mengembalikan objek Type yang mewakili jenis SimpleClass.

  • Finalize yang dilindungi, yang dirancang untuk melepaskan sumber daya yang tidak dikelola sebelum memori objek diambil kembali oleh pengumpul sampah.

  • Metode MemberwiseClone yang dilindungi, yang membuat klon dangkal dari objek saat ini.

Karena warisan implisit, Anda dapat memanggil anggota yang diwarisi dari objek SimpleClass seolah-olah itu benar-benar anggota yang ditentukan dalam kelas SimpleClass. Misalnya, contoh berikut memanggil metode SimpleClass.ToString, yang SimpleClass diwarisi dari Object.

public class EmptyClass
{ }

public class ClassNameExample
{
    public static void Main()
    {
        EmptyClass sc = new();
        Console.WriteLine(sc.ToString());
    }
}
// The example displays the following output:
//        EmptyClass

Tabel berikut mencantumkan kategori jenis yang bisa Anda buat di C# dan jenis yang secara implisit mewarisi. Setiap jenis dasar membuat kumpulan anggota yang berbeda tersedia melalui warisan ke jenis yang diturunkan secara implisit.

Jenis kategori Secara implisit mewarisi dari
kelas Object
struktur ValueType, Object
enum Enum, , ValueTypeObject
delegasi MulticastDelegate, , DelegateObject

Warisan dan hubungan "is a"

Biasanya, warisan digunakan untuk menyatakan hubungan "is a" antara kelas dasar dan satu atau beberapa kelas turunan, di mana kelas turunan adalah versi khusus dari kelas dasar; kelas turunan adalah jenis kelas dasar. Misalnya, kelas Publication mewakili jenis publikasi apa pun, serta kelas Book dan Magazine mewakili jenis publikasi tertentu.

Catatan

Kelas atau struktur dapat mengimplementasikan satu atau beberapa antarmuka. Sementara implementasi antarmuka sering disajikan sebagai solusi untuk warisan tunggal atau sebagai cara menggunakan warisan dengan struktur, hal ini dimaksudkan untuk mengekspresikan hubungan yang berbeda (hubungan "can do") antara antarmuka dan jenis implementasinya daripada warisan. Antarmuka mendefinisikan subset fungsionalitas (seperti kemampuan untuk menguji kesetaraan, untuk membandingkan atau mengurutkan objek, atau untuk mendukung penguraian dan pemformatan peka budaya) yang disediakan antarmuka untuk jenis implementasinya.

Perhatikan bahwa "is a" juga menyatakan hubungan antara jenis dan instantiasi tertentu dari jenis tersebut. Dalam contoh berikut, Automobile adalah kelas yang memiliki tiga properti baca-saja yang unik: Make, pabrikan mobil; Model, jenis mobil; dan Year, tahun pembuatannya. Automobile Anda juga memiliki konstruktor yang argumennya ditetapkan ke nilai properti, dan mengambil alih metode Object.ToString untuk menghasilkan string yang secara unik mengidentifikasi instans Automobile, bukan kelas Automobile.

public class Automobile
{
    public Automobile(string make, string model, int year)
    {
        if (make == null)
            throw new ArgumentNullException(nameof(make), "The make cannot be null.");
        else if (string.IsNullOrWhiteSpace(make))
            throw new ArgumentException("make cannot be an empty string or have space characters only.");
        Make = make;

        if (model == null)
            throw new ArgumentNullException(nameof(model), "The model cannot be null.");
        else if (string.IsNullOrWhiteSpace(model))
            throw new ArgumentException("model cannot be an empty string or have space characters only.");
        Model = model;

        if (year < 1857 || year > DateTime.Now.Year + 2)
            throw new ArgumentException("The year is out of range.");
        Year = year;
    }

    public string Make { get; }

    public string Model { get; }

    public int Year { get; }

    public override string ToString() => $"{Year} {Make} {Model}";
}

Dalam hal ini, Anda tidak boleh mengandalkan warisan untuk mewakili merek dan model mobil tertentu. Misalnya, Anda tidak perlu menentukan jenis Packard untuk mewakili mobil yang diproduksi oleh Packard Motor Car Company. Sebagai gantinya, Anda dapat merepresentasikannya dengan membuat objek Automobile menggunakan nilai yang sesuai yang diteruskan ke konstruktor kelasnya, seperti yang dilakukan contoh berikut.

using System;

public class Example
{
    public static void Main()
    {
        var packard = new Automobile("Packard", "Custom Eight", 1948);
        Console.WriteLine(packard);
    }
}
// The example displays the following output:
//        1948 Packard Custom Eight

Hubungan is-a berdasarkan warisan paling baik diterapkan ke kelas dasar dan kelas turunan yang menambahkan anggota tambahan ke kelas dasar atau yang memerlukan fungsionalitas tambahan yang tidak ada di kelas dasar.

Merancang kelas dasar dan kelas turunan

Mari kita lihat proses mendesain kelas dasar dan kelas turunannya. Di bagian ini, Anda akan menentukan kelas dasar, Publication, yang mewakili publikasi dalam bentuk apa pun, seperti buku, majalah, surat kabar, jurnal, artikel, dll. Anda juga akan mendefinisikan kelas Book yang diturunkan dari Publication. Anda dapat dengan mudah memperluas contoh untuk mendefinisikan kelas turunan lainnya, seperti Magazine, Journal, Newspaper, dan Article.

Kelas Publikasi dasar

Dalam mendesain kelas Publication, Anda perlu membuat beberapa keputusan desain:

  • Anggota apa yang akan disertakan dalam kelas Publication dasar Anda, dan apakah anggota Publication menyediakan implementasi metode atau apakah Publication adalah kelas dasar abstrak yang berfungsi sebagai template untuk kelas turunannya.

    Dalam hal ini, kelas Publication akan menyediakan implementasi metode. Bagian Merancang kelas dasar abstrak dan kelas turunannya berisi contoh yang menggunakan kelas dasar abstrak untuk menentukan metode yang harus diambil alih oleh kelas turunan. Kelas turunan bebas menyediakan implementasi apa pun yang cocok untuk jenis turunan.

    Kemampuan untuk menggunakan kembali kode (yaitu, beberapa kelas turunan berbagi deklarasi dan implementasi metode kelas dasar dan tidak perlu mengambil alihnya) adalah keuntungan dari kelas dasar non-abstrak. Oleh karena itu, Anda harus menambahkan anggota ke Publication jika kode mereka kemungkinan besar digunakan bersama oleh beberapa atau sebagian besar jenis Publication khusus. Jika Anda gagal menyediakan implementasi kelas dasar secara efisien, Anda akhirnya harus menyediakan implementasi anggota yang sebagian besar identik di kelas turunan, bukan implementasi tunggal di kelas dasar. Kebutuhan untuk mempertahankan kode duplikat di beberapa lokasi adalah sumber potensial bug.

    Baik untuk memaksimalkan penggunaan kembali kode serta untuk membuat hierarki warisan yang logis dan intuitif, Anda ingin memastikan bahwa Anda hanya menyertakan Publication data dan fungsionalitas yang umum untuk semua atau ke sebagian besar publikasi. Kelas turunan kemudian mengimplementasikan anggota yang unik untuk jenis publikasi tertentu yang mereka wakili.

  • Seberapa jauh untuk memperluas hierarki kelas Anda. Apakah Anda ingin mengembangkan hierarki tiga kelas atau lebih, daripada hanya kelas dasar dan satu atau beberapa kelas turunan? Misalnya, Publication bisa menjadi kelas Periodicaldasar, yang pada gilirannya adalah kelas dasar Magazine, Journal dan Newspaper.

    Sebagai contoh, Anda akan menggunakan hierarki kecil dari kelas Publication dan satu kelas turunan, Book. Anda dapat dengan mudah memperluas contoh untuk membuat sejumlah kelas tambahan yang berasal dari Publication, seperti Magazine dan Article.

  • Apakah masuk akal untuk membuat instans kelas dasar. Jika tidak, Anda harus menerapkan kata kunci abstrak ke kelas. Jika tidak, kelas Publication Anda dapat dipakai dengan memanggil konstruktor kelasnya. Jika upaya dilakukan untuk membuat instans kelas yang ditandai dengan kata kunci menggunakan abstractpanggilan langsung ke konstruktor kelasnya, pengompilasi C# menghasilkan kesalahan CS0144, "Tidak dapat membuat instans kelas atau antarmuka abstrak." Jika upaya dilakukan untuk membuat instans kelas dengan menggunakan pantulan, metode pantulan akan menampilkan MemberAccessException.

    Secara default, kelas dasar dapat dibuat dengan memanggil konstruktor kelasnya. Anda tidak harus secara eksplisit mendefinisikan konstruktor kelas. Jika tidak ada dalam kode sumber kelas dasar, pengompilasi C# secara otomatis menyediakan konstruktor default (tanpa parameter).

    Sebagai contoh, Anda akan menandai kelas Publication sebagai abstrak sehingga tidak dapat dipakai. Kelas abstract tanpa metode abstract apa pun menunjukkan bahwa kelas ini mewakili konsep abstrak yang dibagikan di antara beberapa kelas konkret (seperti Book, Journal).

  • Apakah kelas turunan harus mewarisi implementasi kelas dasar dari anggota tertentu, apakah mereka memiliki opsi untuk menimpa implementasi kelas dasar, atau apakah mereka harus menyediakan implementasi. Anda menggunakan kata kunci abstrak untuk memaksa kelas turunan menyediakan implementasi. Anda menggunakan kata kunci virtual untuk mengizinkan kelas turunan menggantikan metode kelas dasar. Secara default, metode yang ditentukan dalam kelas dasar tidak dapat diganti.

    Kelas Publication tidak memiliki metode abstract, tetapi kelas itu sendiri adalah abstract.

  • Apakah kelas turunan mewakili kelas akhir dalam hierarki warisan dan tidak dapat digunakan sendiri sebagai kelas dasar untuk kelas turunan tambahan. Secara default, setiap kelas dapat berfungsi sebagai kelas dasar. Anda dapat menerapkan kata kunci disegel untuk menunjukkan bahwa kelas tidak dapat berfungsi sebagai kelas dasar untuk kelas tambahan apa pun. Mencoba untuk berasal dari kelas tertutup yang menghasilkan kesalahan kompilator CS0509, "tidak dapat berasal dari typeName> yang <disegel."

    Sebagai contoh, Anda akan menandai kelas turunan Anda sebagai sealed.

Contoh berikut menunjukkan kode sumber untuk kelas Publication, serta enumerasi PublicationType yang dikembalikan oleh properti Publication.PublicationType. Selain anggota yang diwarisi dari Object, kelas Publication mendefinisikan anggota unik berikut dan penggantian anggota:


public enum PublicationType { Misc, Book, Magazine, Article };

public abstract class Publication
{
    private bool _published = false;
    private DateTime _datePublished;
    private int _totalPages;

    public Publication(string title, string publisher, PublicationType type)
    {
        if (string.IsNullOrWhiteSpace(publisher))
            throw new ArgumentException("The publisher is required.");
        Publisher = publisher;

        if (string.IsNullOrWhiteSpace(title))
            throw new ArgumentException("The title is required.");
        Title = title;

        Type = type;
    }

    public string Publisher { get; }

    public string Title { get; }

    public PublicationType Type { get; }

    public string? CopyrightName { get; private set; }

    public int CopyrightDate { get; private set; }

    public int Pages
    {
        get { return _totalPages; }
        set
        {
            if (value <= 0)
                throw new ArgumentOutOfRangeException(nameof(value), "The number of pages cannot be zero or negative.");
            _totalPages = value;
        }
    }

    public string GetPublicationDate()
    {
        if (!_published)
            return "NYP";
        else
            return _datePublished.ToString("d");
    }

    public void Publish(DateTime datePublished)
    {
        _published = true;
        _datePublished = datePublished;
    }

    public void Copyright(string copyrightName, int copyrightDate)
    {
        if (string.IsNullOrWhiteSpace(copyrightName))
            throw new ArgumentException("The name of the copyright holder is required.");
        CopyrightName = copyrightName;

        int currentYear = DateTime.Now.Year;
        if (copyrightDate < currentYear - 10 || copyrightDate > currentYear + 2)
            throw new ArgumentOutOfRangeException($"The copyright year must be between {currentYear - 10} and {currentYear + 1}");
        CopyrightDate = copyrightDate;
    }

    public override string ToString() => Title;
}
  • Konstruktor

    Karena kelas Publication adalah abstract, kelas tidak dapat dibuat langsung dari kode seperti contoh berikut:

    var publication = new Publication("Tiddlywinks for Experts", "Fun and Games",
                                      PublicationType.Book);
    

    Namun, konstruktor instansnya dapat dipanggil langsung dari konstruktor kelas turunan, seperti yang ditunjukkan oleh kode sumber untuk kelas Book.

  • Dua properti terkait publikasi

    Title adalah properti String baca-saja yang nilainya disediakan dengan memanggil konstruktor Publication.

    Pages adalah properti baca-tulis Int32 yang menunjukkan jumlah total halaman yang dimiliki publikasi. Nilai disimpan dalam bidang privat bernama totalPages. Nilai harus berupa angka positif atau ArgumentOutOfRangeException akan ditampilkan.

  • Anggota terkait penerbit

    Dua properti baca-saja, Publisher dan Type. Nilai awalnya disediakan oleh panggilan ke konstruktor kelas Publication.

  • Anggota terkait penerbitan

    Dua metode, Publish dan GetPublicationDate, mengatur dan mengembalikan tanggal publikasi. Metode Publish mengatur tanda published privat ke true saat dipanggil dan menetapkan tanggal yang diteruskan ke sana sebagai argumen ke bidang datePublished privat. Metode GetPublicationDate mengembalikan string "NYP" jika bendera published adalah false, dan nilai bidang datePublished jika true.

  • Anggota terkait hak cipta

    Metode Copyright menggunakan nama pemegang hak cipta dan tahun hak cipta sebagai argumen dan menetapkannya ke properti CopyrightName dan CopyrightDate.

  • Penggantian metode ToString

    Jika jenis tidak mengambil alih metode Object.ToString, jenis akan mengembalikan nama jenis yang sepenuhnya memenuhi syarat, yang sedikit digunakan dalam membedakan satu instans dari instans lain. Kelas Publication mengambil alih Object.ToString untuk mengembalikan nilai properti Title.

Gambar berikut mengilustrasikan hubungan antara kelas Publication dasar Anda dan kelas Object yang diwariskan secara implisit.

Kelas Objek dan Publikasi

Kelas Book

Kelas Book mewakili buku sebagai jenis publikasi khusus. Contoh berikut menunjukkan kode sumber untuk kelas Book.

using System;

public sealed class Book : Publication
{
    public Book(string title, string author, string publisher) :
           this(title, string.Empty, author, publisher)
    { }

    public Book(string title, string isbn, string author, string publisher) : base(title, publisher, PublicationType.Book)
    {
        // isbn argument must be a 10- or 13-character numeric string without "-" characters.
        // We could also determine whether the ISBN is valid by comparing its checksum digit
        // with a computed checksum.
        //
        if (!string.IsNullOrEmpty(isbn))
        {
            // Determine if ISBN length is correct.
            if (!(isbn.Length == 10 | isbn.Length == 13))
                throw new ArgumentException("The ISBN must be a 10- or 13-character numeric string.");
            if (!ulong.TryParse(isbn, out _))
                throw new ArgumentException("The ISBN can consist of numeric characters only.");
        }
        ISBN = isbn;

        Author = author;
    }

    public string ISBN { get; }

    public string Author { get; }

    public decimal Price { get; private set; }

    // A three-digit ISO currency symbol.
    public string? Currency { get; private set; }

    // Returns the old price, and sets a new price.
    public decimal SetPrice(decimal price, string currency)
    {
        if (price < 0)
            throw new ArgumentOutOfRangeException(nameof(price), "The price cannot be negative.");
        decimal oldValue = Price;
        Price = price;

        if (currency.Length != 3)
            throw new ArgumentException("The ISO currency symbol is a 3-character string.");
        Currency = currency;

        return oldValue;
    }

    public override bool Equals(object? obj)
    {
        if (obj is not Book book)
            return false;
        else
            return ISBN == book.ISBN;
    }

    public override int GetHashCode() => ISBN.GetHashCode();

    public override string ToString() => $"{(string.IsNullOrEmpty(Author) ? "" : Author + ", ")}{Title}";
}

Selain anggota yang diwarisi dari Publication, kelas Book mendefinisikan anggota unik berikut dan penggantian anggota:

  • Dua konstruktor

    Kedua konstruktor Book berbagi tiga parameter umum. Dua, judul dan penerbit, sesuai dengan parameter konstruktor Publication. Yang ketiga adalah penulis, yang disimpan ke properti Author publik yang tidak dapat diubah. Satu konstruktor menyertakan parameter isbn, yang disimpan di properti otomatis ISBN.

    Konstruktor pertama menggunakan kata kunci this untuk memanggil konstruktor lainnya. Rantai konstruktor adalah pola umum dalam mendefinisikan konstruktor. Konstruktor dengan parameter yang lebih sedikit memberikan nilai default saat memanggil konstruktor dengan jumlah parameter terbanyak.

    Konstruktor kedua menggunakan kata kunci base untuk meneruskan judul dan nama penerbit ke konstruktor kelas dasar. Jika Anda tidak membuat panggilan eksplisit ke konstruktor kelas dasar dalam kode sumber Anda, pengompilasi C# secara otomatis menyediakan panggilan ke konstruktor default atau tanpa parameter kelas dasar.

  • ISBN baca-saja, yang mengembalikan Nomor Buku Standar Internasional objek Book, nomor unik 10 atau 13 digit. ISBN diberikan sebagai argumen ke salah satu konstruktor Book. ISBN disimpan dalam bidang dukungan privat, yang dibuat secara otomatis oleh pengompilasi.

  • Properti Author baca-saja. Nama penulis diberikan sebagai argumen untuk kedua konstruktor Book dan disimpan di properti.

  • Dua properti terkait harga baca-saja, Price dan Currency. Nilainya disediakan sebagai argumen dalam pemanggilan metode SetPrice. Properti Currency adalah simbol mata uang ISO tiga digit (misalnya, USD untuk dolar AS). Simbol mata uang ISO dapat diambil dari properti ISOCurrencySymbol. Kedua properti ini bersifat baca-saja secara eksternal, tetapi keduanya dapat diatur oleh kode di kelas Book.

  • Metode SetPrice, yang menetapkan nilai properti Price dan Currency. Nilai-nilai tersebut dikembalikan oleh properti yang sama.

  • Mengganti metode ToString (diwarisi dari Publication) serta metode Object.Equals(Object) dan GetHashCode (diwarisi dari Object).

    Kecuali jika diganti, metode Object.Equals(Object) menguji kesetaraan referensi. Artinya, dua variabel objek dianggap sama jika merujuk ke objek yang sama. Di kelas Book, sebaliknya, dua objek Book harus sama jika memiliki ISBN yang sama.

    Saat mengganti metode Object.Equals(Object), Anda juga harus mengganti metode GetHashCode, yang mengembalikan nilai yang digunakan runtime untuk menyimpan item dalam koleksi hash untuk pengambilan yang efisien. Kode hash harus mengembalikan nilai yang konsisten dengan pengujian kesetaraan. Karena Anda telah mengganti Object.Equals(Object) untuk mengembalikan true jika properti ISBN dari dua objek Book sama, Anda mengembalikan kode hash yang dihitung dengan memanggil metode GetHashCode dari string yang dikembalikan oleh properti ISBN.

Gambar berikut mengilustrasikan hubungan antara kelas Book dan Publication, kelas dasarnya.

Kelas Publikasi dan Buku

Anda sekarang dapat membuat instans objek Book, memanggil anggotanya yang unik dan yang diwarisi, dan meneruskannya sebagai argumen ke metode yang mengharapkan parameter jenis Publication atau jenis Book, seperti yang ditunjukkan contoh berikut.

public class ClassExample
{
    public static void Main()
    {
        var book = new Book("The Tempest", "0971655819", "Shakespeare, William",
                            "Public Domain Press");
        ShowPublicationInfo(book);
        book.Publish(new DateTime(2016, 8, 18));
        ShowPublicationInfo(book);

        var book2 = new Book("The Tempest", "Classic Works Press", "Shakespeare, William");
        Console.Write($"{book.Title} and {book2.Title} are the same publication: " +
              $"{((Publication)book).Equals(book2)}");
    }

    public static void ShowPublicationInfo(Publication pub)
    {
        string pubDate = pub.GetPublicationDate();
        Console.WriteLine($"{pub.Title}, " +
                  $"{(pubDate == "NYP" ? "Not Yet Published" : "published on " + pubDate):d} by {pub.Publisher}");
    }
}
// The example displays the following output:
//        The Tempest, Not Yet Published by Public Domain Press
//        The Tempest, published on 8/18/2016 by Public Domain Press
//        The Tempest and The Tempest are the same publication: False

Merancang kelas dasar abstrak dan kelas turunannya

Pada contoh sebelumnya, Anda mendefinisikan kelas dasar yang menyediakan implementasi untuk sejumlah metode agar kelas turunan dapat berbagi kode. Namun, dalam banyak kasus, kelas dasar tidak diharapkan untuk memberikan implementasi. Sebaliknya, kelas dasar adalah kelas abstrak yang menyatakan metode abstrak; ini berfungsi sebagai template yang mendefinisikan anggota yang harus diterapkan oleh setiap kelas turunan. Biasanya dalam kelas dasar abstrak, implementasi setiap jenis turunan bersifat unik untuk jenis tersebut. Anda menandai kelas dengan kata kunci abstrak karena tidak masuk akal untuk membuat instans objek Publication, meskipun kelas memang menyediakan implementasi fungsionalitas yang umum untuk publikasi.

Misalnya, setiap bentuk geometris dua dimensi tertutup mencakup dua properti: area, tingkat dalam bentuk; dan perimeter, atau jarak di sepanjang tepi bentuk. Namun, cara penghitungan properti ini sepenuhnya bergantung pada bentuk tertentu. Rumus untuk menghitung perimeter (atau lingkar) lingkaran, misalnya, berbeda dari persegi. Kelas Shape adalah abstract kelas dengan metode abstract. Hal ini menunjukkan kelas turunan memiliki fungsionalitas yang sama, tetapi kelas turunan tersebut mengimplementasikan fungsionalitas tersebut secara berbeda.

Contoh berikut mendefinisikan kelas dasar abstrak bernama Shape yang mendefinisikan dua properti: Area dan Perimeter. Selain menandai kelas dengan kata kunci abstrak, setiap anggota instans juga ditandai dengan kata kunci abstrak. Dalam hal ini, Shape juga mengganti metode Object.ToString untuk mengembalikan nama jenis, bukan nama yang sepenuhnya memenuhi syarat. Dan mendefinisikan dua anggota statis, GetArea dan GetPerimeter, yang memungkinkan pemanggil untuk dengan mudah mengambil area dan perimeter instans dari kelas turunan apa pun. Saat meneruskan instans kelas turunan ke salah satu metode ini, runtime akan memanggil penggantian metode kelas turunan.

public abstract class Shape
{
    public abstract double Area { get; }

    public abstract double Perimeter { get; }

    public override string ToString() => GetType().Name;

    public static double GetArea(Shape shape) => shape.Area;

    public static double GetPerimeter(Shape shape) => shape.Perimeter;
}

Anda kemudian dapat memperoleh beberapa kelas dari Shape yang mewakili bentuk tertentu. Contoh berikut mendefinisikan tiga kelas, Square, Rectangle, dan Circle. Masing-masing menggunakan rumus yang unik untuk bentuk tertentu guna menghitung area dan perimeter. Beberapa kelas turunan juga mendefinisikan properti, seperti Rectangle.Diagonal dan Circle.Diameter, yang unik untuk bentuk yang diwakilinya.

using System;

public class Square : Shape
{
    public Square(double length)
    {
        Side = length;
    }

    public double Side { get; }

    public override double Area => Math.Pow(Side, 2);

    public override double Perimeter => Side * 4;

    public double Diagonal => Math.Round(Math.Sqrt(2) * Side, 2);
}

public class Rectangle : Shape
{
    public Rectangle(double length, double width)
    {
        Length = length;
        Width = width;
    }

    public double Length { get; }

    public double Width { get; }

    public override double Area => Length * Width;

    public override double Perimeter => 2 * Length + 2 * Width;

    public bool IsSquare() => Length == Width;

    public double Diagonal => Math.Round(Math.Sqrt(Math.Pow(Length, 2) + Math.Pow(Width, 2)), 2);
}

public class Circle : Shape
{
    public Circle(double radius)
    {
        Radius = radius;
    }

    public override double Area => Math.Round(Math.PI * Math.Pow(Radius, 2), 2);

    public override double Perimeter => Math.Round(Math.PI * 2 * Radius, 2);

    // Define a circumference, since it's the more familiar term.
    public double Circumference => Perimeter;

    public double Radius { get; }

    public double Diameter => Radius * 2;
}

Contoh berikut menggunakan objek yang berasal dari Shape. Ini membuat instans array objek yang berasal dari Shape dan memanggil metode statis kelas Shape, yang membungkus nilai properti pengembalian Shape. Runtime mengambil nilai dari properti yang diganti dari jenis turunan. Contohnya juga mentransmisikan setiap objek Shape dalam array ke jenis turunannya dan, jika transmisi berhasil, mengambil properti dari subkelas tertentu dari Shape.

using System;

public class Example
{
    public static void Main()
    {
        Shape[] shapes = { new Rectangle(10, 12), new Square(5),
                    new Circle(3) };
        foreach (Shape shape in shapes)
        {
            Console.WriteLine($"{shape}: area, {Shape.GetArea(shape)}; " +
                              $"perimeter, {Shape.GetPerimeter(shape)}");
            if (shape is Rectangle rect)
            {
                Console.WriteLine($"   Is Square: {rect.IsSquare()}, Diagonal: {rect.Diagonal}");
                continue;
            }
            if (shape is Square sq)
            {
                Console.WriteLine($"   Diagonal: {sq.Diagonal}");
                continue;
            }
        }
    }
}
// The example displays the following output:
//         Rectangle: area, 120; perimeter, 44
//            Is Square: False, Diagonal: 15.62
//         Square: area, 25; perimeter, 20
//            Diagonal: 7.07
//         Circle: area, 28.27; perimeter, 18.85