PRAKTIK TERBAIK

Kode Ramah Kompiler: Kata Kunci Tersegel di .NET C#

Mengapa & Kapan Kata Kunci Tersegel dapat meningkatkan kinerja di .NET C#

Apa artinya menulis kode yang ramah kompiler?

Kode .NET apa pun melewati lebih dari satu fase hingga akhirnya mencapai kode mesin. Karena banyak faktor yang terlibat dalam proses ini, mungkin ada banyak detail yang kita lewatkan saat pertama kali menulis kode.

Namun, semakin jelas dan deterministik kode yang kita tulis, semakin banyak kompiler yang dapat membantu kita dan menghasilkan kode mesin yang dioptimalkan.

Pada artikel ini, kita akan membahas salah satu contoh cara yang dapat kita gunakan untuk membantu compiler mengoptimalkan kode kita. Cara ini adalah; menggunakan Kata Kunci Tersegel.

Cukup dengan pembahasannya dan mari kita lihat contohnya…



Pemeriksaan latar belakang

Jika Anda seorang pengembang .NET, bahkan seorang pemula, Anda harus tahu sekarang bahwa ada kata kunci dalam kerangka .NET yang disebut sealed.

Kata kunci ini dapat digunakan dalam definisi kelas dan artinya kelas tersebut tidak dapat diwarisi oleh kelas lain mana pun. Ini terlihat seperti ini:

public sealed class MyClass {}

Atau bahkan dalam deklarasi metode dan itu berarti bahwa metode tersebut tidak dapat ditimpa -lagi- oleh metode lain mana pun di kelas anak. Dengan kata lain, ini memutus rangkaian penggantian metode pada tingkat penggunaannya. Ini terlihat seperti ini:

public sealed override void MyMethod() {}

Oleh karena itu, yang dapat kita pahami dari sini adalah ketika kita menggunakan kata kunci sealed, kita sebenarnya menjanjikan compiler bahwa kita tidak mempunyai niat untuk mewarisi suatu kelas atau mengganti suatu metode.

Karena itu, sekarang mari kita lihat apakah itu berarti apa pun bagi kompiler.

Mari kita mulai dengan beberapa kode dasar yang akan digunakan sepanjang penjelasan.

Kelas Dasar

public class BaseClass
{
    public virtual void DoSomething()
    {
    }

    public void DoSomethingElse()
    {
    }
}

Ini adalah kelas dasar yang akan kita gunakan sebagai induk teratas.

Di kelas ini, kami telah mendefinisikan anggota berikut:

  • public virtual void DoSomething() metode.
  • public void DoSomethingElse() metode.

Kelasku

public class MyClass : BaseClass
{
    public override void DoSomething()
    {
    }
}

Ini adalah kelas yang akan mewarisi dari BaseClass tetapi tanpa menggunakan kata kunci sealed dalam definisinya.

Di kelas ini, kami mengganti metode DoSomething yang diwarisi dari kelas induk BaseClass.

Kelas Tersegel Saya

public sealed class MySealedClass : BaseClass
{
    public override void DoSomething()
    {
    }
}

Ini adalah kelas yang akan mewarisi dari BaseClass tetapi kali ini kita menggunakan kata kunci sealed dalam definisinya.

Di kelas ini, kami mengganti metode DoSomething yang diwarisi dari kelas induk BaseClass.

Sekarang, mari kita lanjutkan dan lihat apakah akan ada perbedaan -dari sudut pandang kompiler- antara penggunaan kelas MyClass dan MySealedClass.

Memanggil Metode Virtual

Untuk memvalidasi apakah akan ada perbedaan -dari sudut pandang compiler- antara pemanggilan metode virtual pada kelas MyClass dan MySealedClass, kita akan membuat proyek Benchmark.

[MemoryDiagnoser(false)]
public class Benchmarking
{
    private readonly int NumberOfTrials = 10;
    private MyClass _myClassObject = new MyClass();
    private MySealedClass _mySealedClassObject = new MySealedClass();

    [Benchmark]
    public void CallingVirtualMethodOnMyClass()
    {
        for (var i = 0; i < NumberOfTrials; i++)
        {
            _myClassObject.DoSomething();
        }
    }

    [Benchmark]
    public void CallingVirtualMethodOnMySealedClass()
    {
        for (var i = 0; i < NumberOfTrials; i++)
        {
            _mySealedClassObject.DoSomething();
        }
    }
}

Sekarang, dengan menjalankan proyek Benchmark ini kita akan mendapatkan hasil sebagai berikut.

Seperti yang bisa kita lihat di sini, performa pemanggilan metode virtual pada kelas tersegel jauh lebih baik daripada memanggilnya pada kelas tidak tersegel.

Tapi kenapa?!!! Biarkan aku memberitahu Anda.

Di Kelas Tidak Tersegel

Saat memanggil metode virtual pada objek yang dibuat dari kelas MyClass, pada saat itu kompiler tidak mengetahui apakah ada kode yang menginisialisasi ulang objek _myClassObject dengan instance baru dari kelas anak kelas MyClass atau tidak. Asumsi ini valid karena kelas MyClass tidak tersegel dan ini berarti dapat diwariskan.

Berdasarkan asumsi tersebut, kompiler tidak dapat memutuskan -pada waktu kompilasi- apakah implementasi sebenarnya dari metode DoSomething akan menjadi implementasi yang disediakan oleh kelas MyClass atau kelas turunan lainnya.

Dengan demikian, kompiler akan menulis beberapa instruksi -untuk dieksekusi saat runtime- untuk memeriksa pada saat mengeksekusi metode DoSomething, implementasi mana yang tepat. Hal ini pasti akan memerlukan lebih banyak pemrosesan dan waktu.

Catatan: seperti yang Anda perhatikan, kompiler akan mencurigai bahwa beberapa kode dapat menginisialisasi ulang objek. Anda mungkin berpikir bahwa menandai bidang sebagai readonly akan menyelesaikan masalah tetapi sebenarnya tidak karena objek masih dapat diinisialisasi ulang di dalam konstruktor.

Di Kelas Tertutup

Saat memanggil metode virtual pada objek yang dibuat dari kelas MySealedClass, pada saat itu kompiler tidak mengetahui apakah ada kode yang menginisialisasi ulang objek _mySealedClassObject dengan instance baru atau tidak. Namun, kompiler yakin bahwa bahkan jika ini terjadi, instance tersebut akan tetap berada di kelas MySealedClass sebagaimana adanya sealed dan ini berarti bahwa instance tersebut tidak akan pernah memiliki kelas turunan apa pun.

Berdasarkan hal tersebut, kompiler akan memutuskan -pada waktu kompilasi- implementasi sebenarnya dari metode DoSomething. Ini pastinya jauh lebih cepat daripada menunggu waktu proses.

Memanggil Metode Non-Virtual

Untuk memvalidasi apakah akan ada perbedaan -dari sudut pandang compiler- antara pemanggilan metode non-virtual pada kelas MyClass dan MySealedClass, kita akan membuat proyek Benchmark .

[MemoryDiagnoser(false)]
public class Benchmarking
{
    private readonly int NumberOfTrials = 10;
    private BaseClass _baseClassObject = new BaseClass();
    private MyClass _myClassObject = new MyClass();
    private MySealedClass _mySealedClassObject = new MySealedClass();
    private MyClass[] _myClassObjectsArray = new MyClass[1];
    private MySealedClass[] _mySealedClassObjectsArray = new MySealedClass[1];

    [Benchmark]
    public void CallingNonVirtualMethodOnMyClass()
    {
        for (var i = 0; i < NumberOfTrials; i++)
        {
            _myClassObject.DoSomethingElse();
        }
    }

    [Benchmark]
    public void CallingNonVirtualMethodOnMySealedClass()
    {
        for (var i = 0; i < NumberOfTrials; i++)
        {
            _mySealedClassObject.DoSomethingElse();
        }
    }
}

Sekarang, dengan menjalankan proyek Benchmark ini kita akan mendapatkan hasil sebagai berikut.

Seperti yang bisa kita lihat di sini, performa pemanggilan metode non-virtual pada kelas tersegel lebih baik daripada pemanggilan metode non-virtual pada kelas tak tersegel.

Namun, tidak ada bukti ilmiah mengapa hal ini terjadi dan menjalankan kembali proyek benchmark yang sama mungkin akan memberikan hasil yang sebaliknya.

Oleh karena itu, kemungkinan besar perbedaan ini disebabkan oleh kerangka benchmarking itu sendiri karena perbedaannya terlalu kecil sehingga dapat diabaikan.

Pengecekan Tipe

Untuk memvalidasi apakah akan ada perbedaan -dari sudut pandang compiler- antara memeriksa jenis objek menggunakan operator ispada kelas MyClass dan MySealedClass, kita akan membuat Benchmark proyek.

[MemoryDiagnoser(false)]
public class Benchmarking
{
    private readonly int NumberOfTrials = 10;
    private BaseClass _baseClassObject = new BaseClass();
    
    [Benchmark]
    public bool ObjectTypeIsMyClass()
    {
        for (var i = 0; i < NumberOfTrials; i++)
        {
            var x = _baseClassObject is MyClass;
        }

        return true;
    }

    [Benchmark]
    public bool ObjectTypeIsMySealedClass()
    {
        for (var i = 0; i < NumberOfTrials; i++)
        {
            var x = _baseClassObject is MySealedClass;
        }

        return true;
    }
}

Sekarang, dengan menjalankan proyek Benchmark ini kita akan mendapatkan hasil sebagai berikut.

Seperti yang dapat kita lihat di sini, kinerja pemeriksaan tipe objek pada kelas yang disegel lebih baik daripada memanggilnya pada kelas yang tidak disegel.

Tapi kenapa?!!! Biarkan aku memberitahu Anda.

Di Kelas Tidak Tersegel

Saat memeriksa apakah tipe objeknya adalah kelas MyClass, kompiler perlu memeriksa apakah objek tersebut bertipe kelas MyClass atau salah satu kelas turunannya.

Dengan demikian, ini menghasilkan lebih banyak instruksi dan tentunya lebih banyak pemrosesan dan waktu.

Di Kelas Tertutup

Saat memeriksa apakah tipe objeknya adalah kelas MySealedClass, kompiler perlu memeriksa apakah objek tersebut hanya bertipe kelas MySealedClass, tidak ada yang lain. Ini karena kelas MySealedClass disegel dan ini berarti kelas tersebut tidak akan pernah memiliki kelas turunan.

Dengan demikian, hal ini menyebabkan lebih sedikit instruksi dan tentu saja lebih sedikit proses dan waktu.

Tipe Transmisi

Untuk memvalidasi apakah akan ada perbedaan -dari sudut pandang compiler- antara mentransmisikan objek menggunakan operator aspada kelas MyClass dan MySealedClass, kita akan membuat Benchmark proyek.

[MemoryDiagnoser(false)]
public class Benchmarking
{
    private readonly int NumberOfTrials = 10;
    private BaseClass _baseClassObject = new BaseClass();
    
    [Benchmark]
    public void ObjectTypeAsMyClass()
    {
        for (var i = 0; i < NumberOfTrials; i++)
        {
            var x = _baseClassObject as MyClass;
        }
    }

    [Benchmark]
    public void ObjectTypeAsMySealedClass()
    {
        for (var i = 0; i < NumberOfTrials; i++)
        {
            var x = _baseClassObject as MySealedClass;
        }
    }
}

Sekarang, dengan menjalankan proyek Benchmark ini kita akan mendapatkan hasil sebagai berikut.

Seperti yang dapat kita lihat di sini, kinerja casting objek pada kelas yang tersegel lebih baik daripada memanggilnya pada kelas yang tidak tersegel.

Tapi kenapa?!!! Biarkan aku memberitahu Anda.

Di Kelas Tidak Tersegel

Saat mentransmisikan objek sebagai kelas MyClass, kompiler perlu memeriksa apakah objek tersebut bertipe kelas MyClass atau salah satu kelas turunannya.

Dengan demikian, ini menghasilkan lebih banyak instruksi dan tentunya lebih banyak pemrosesan dan waktu.

Di Kelas Tertutup

Saat mentransmisikan objek sebagai kelas MySealedClass, kompiler perlu memeriksa apakah objek tersebut hanya bertipe kelas MySealedClass, tidak ada yang lain. Ini karena kelas MySealedClass disegel dan ini berarti kelas tersebut tidak akan pernah memiliki kelas turunan.

Dengan demikian, hal ini menyebabkan lebih sedikit instruksi dan tentu saja lebih sedikit proses dan waktu.

Menyimpan Objek Dalam Array

Untuk memvalidasi apakah akan ada perbedaan -dari sudut pandang compiler- antara menyimpan objek dalam arraypada kelas MyClass dan MySealedClass, kita akan membuat proyek Benchmark .

[MemoryDiagnoser(false)]
public class Benchmarking
{
    private readonly int NumberOfTrials = 10;
    private MyClass _myClassObject = new MyClass();
    private MySealedClass _mySealedClassObject = new MySealedClass();
    private MyClass[] _myClassObjectsArray = new MyClass[1];
    private MySealedClass[] _mySealedClassObjectsArray = new MySealedClass[1];

    [Benchmark]
    public void StoringValuesInMyClassArray()
    {
        for (var i = 0; i < NumberOfTrials; i++)
        {
            _myClassObjectsArray[0] = _myClassObject;
        }
    }

    [Benchmark]
    public void StoringValuesInMySealedClassArray()
    {
        for (var i = 0; i < NumberOfTrials; i++)
        {
            _mySealedClassObjectsArray[0] = _mySealedClassObject;
        }
    }
}

Sekarang, dengan menjalankan proyek Benchmark ini kita akan mendapatkan hasil sebagai berikut.

Seperti yang dapat kita lihat di sini, kinerja menyimpan objek dalam array pada kelas yang tersegel lebih baik daripada memanggilnya pada kelas yang tidak tersegel.

Tapi kenapa?!!! Biarkan aku memberitahu Anda.

Sebelum membahas detailnya, izinkan saya mengingatkan Anda terlebih dahulu tentang satu poin penting; array adalah kovarian.

Ini berarti jika kita mendefinisikan kelas-kelas berikut:

public class A {}
public class B : A {}

Maka kode berikut akan valid:

A[] arrayOfA = new B[5];

Selain itu, kita dapat menyetel item di dalam arrayOfA ke instance B sebagai berikut:

arrayOfA[0] = new B();

Karena itu, sekarang mari kita lanjutkan ke topik utama kita.

Di Kelas Tidak Tersegel

Saat mengatur item di dalam array _myClassObjectsArray, kompiler perlu memeriksa apakah instance _myClassObject yang kita gunakan bertipe kelas MyClass atau salah satu kelas turunannya.

Dengan demikian, ini menghasilkan lebih banyak instruksi dan tentunya lebih banyak pemrosesan dan waktu.

Di Kelas Tertutup

Saat mengatur item di dalam array _mySealedClassObjectsArray, kompiler perlu memeriksa apakah instance _mySealedClassObject yang kita gunakan hanya bertipe kelas MySealedClass, tidak ada yang lain. Ini karena kelas MySealedClass disegel dan ini berarti kelas tersebut tidak akan pernah memiliki kelas turunan.

Dengan demikian, hal ini menyebabkan lebih sedikit instruksi dan tentu saja lebih sedikit proses dan waktu.

Deteksi Kegagalan Dini

Selain peningkatan kinerja yang dapat kita peroleh dengan menggunakan kata kunci sealed, kita juga dapat menghindari beberapa kegagalan runtime. Mari saya tunjukkan sebuah contoh.

Jika kita menulis kode berikut:

public void Run(MyClass obj)
{
    _ = _baseClassObject as IMyInterface;
}

Kompiler -pada waktu desain- tidak akan menampilkan peringatan atau kesalahan apa pun karena sebenarnya obj dapat bertipe kelas MyClass atau salah satu kelas turunannya. Oleh karena itu, kompiler perlu menunggu waktu proses untuk melakukan pemeriksaan terakhir.

Yang pasti, jika pada saat runtime tipe obj yang sebenarnya ternyata tidak mengimplementasikan IMyInterface, hal ini akan menyebabkan pengecualian runtime.

Namun jika kita menulis kode berikut:

public void Run(MySealedClass obj)
{
    _ = _baseClassObject as IMyInterface;
}

Kompiler akan menampilkan kesalahan (CS0039) pada waktu desain karena obj hanya dapat bertipe kelas MySealedClass, tidak ada yang lain. Oleh karena itu, kompiler dapat langsung memeriksa apakah kelas MySealedClass mengimplementasikan IMyInterface atau tidak.

Jadi, ini berarti bahwa penggunaan kata kunci yang disegel memungkinkan kompiler melakukan perubahan statis yang tepat pada waktu desain.

Pikiran Terakhir

Saya selalu merekomendasikan penggunaan kata kunci yang disegel bila memungkinkan.

Hal ini bukan hanya untuk peningkatan kinerja yang mungkin Anda peroleh tetapi juga karena ini merupakan praktik terbaik dari sudut pandang desain sejauh Microsoft benar-benar berpikir untuk membuat semua kelas disegel secara default.

Akhirnya, saya harap Anda menikmati membaca artikel ini seperti saya menikmati menulisnya.

Semoga konten ini bermanfaat bagi Anda. Jika Anda ingin mendukung:

▶ Jika Anda belum menjadi anggota Medium, Anda dapat menggunakan tautan referensi saya sehingga saya dapat memperoleh sebagian biaya Anda dari Medium, Anda tidak perlu membayar tambahan apa pun.
▶ Berlangganan Buletin saya untuk mendapatkan praktik terbaik, tutorial, petunjuk, kiat, dan banyak hal keren lainnya langsung ke kotak masuk Anda.

Sumber Daya Lainnya

Ini adalah sumber daya lain yang mungkin berguna bagi Anda.