using dan IDisposable

Pembersihan deterministik dan implementasi Dispose.

Daftar isi

Sumber daya seperti berkas yang terbuka, koneksi jaringan, dan memori yang dialokasikan langsung dari sistem operasi tidak dibebaskan oleh garbage collector segera setelah selesai dipakai, karena pengumpul memori hanya menjanjikan waktu yang tidak dapat diprediksi. Keadaan semacam ini memerlukan pembersihan deterministik, yaitu pelepasan yang terjadi pada saat yang pasti. Topik ini membahas interface IDisposable sebagai kontrak pembersihan, pernyataan using sebagai bentuk pemakaiannya, serta implementasi metode Dispose yang benar. Setelah topik ini, sumber daya yang dipinjam program dapat dibebaskan dengan pasti, termasuk ketika exception terjadi di tengah pekerjaan.

Kontrak IDisposable#

IDisposable adalah interface dengan satu anggota, yaitu metode Dispose tanpa parameter dan tanpa nilai kembali. Kelas yang mengelola sumber daya menerapkan interface ini untuk menyatakan bahwa pemakainya wajib memanggil Dispose setelah selesai bekerja. Contoh sumber dayanya adalah berkas yang terbuka melalui FileStream, koneksi database, dan blok memori yang dialokasikan langsung dari sistem operasi. Pelepasan memori terkelola tetap tugas garbage collector, sedangkan sumber daya di luar memori terkelola memerlukan panggilan yang pasti waktunya.

Cara paling ringkas memastikan pemanggilan tersebut adalah pernyataan using. Ketika blok using berakhir, baik karena alur normal maupun karena exception, runtime memanggil Dispose atas objek yang dideklarasikannya. Bentuk ini setara dengan try dan finally berikut, yang memang menjadi bentuk hasil kompilasinya.

csharp Contoh: bentuk try/finally yang menjadi dasar using
// using sebenarnya dikompilasi menjadi try/finally yang memanggil Dispose.
var sumber = new SumberDayaUji();
try
{
    Console.WriteLine("Bekerja dengan sumber daya.");
}
finally
{
    sumber.Dispose();
}

class SumberDayaUji : IDisposable
{
    public void Dispose()
    {
        Console.WriteLine("Dipanggil dari finally.");
    }
}
text Keluaran
Bekerja dengan sumber daya.
Dipanggil dari finally.

Blok try menampung pekerjaan, sedangkan finally memanggil Dispose tanpa syarat. Bentuk kompilasi ini menjelaskan mengapa using dijamin berjalan bahkan ketika exception dilempar di tengah pekerjaan, karena runtime menjamin finally selalu dieksekusi.

Pernyataan using#

Pernyataan using memiliki dua bentuk. Bentuk klasik menyertakan blok kurung kurawal, sedangkan bentuk using var yang disebut using declaration mendeklarasikan variabel tanpa blok, dan Dispose-nya dipanggil pada akhir blok yang melingkupinya.

Program berikut memakai kedua bentuk tersebut pada kelas Pencatat yang menerapkan IDisposable. Percobaan kedua sengaja melempar exception di tengah blok, sehingga keluarannya memperlihatkan apakah Dispose tetap dipanggil. Salin programnya ke berkas Program.cs, lalu jalankan dengan dotnet run --file Program.cs.

csharp Program.cs
// Program dasar using: Dispose dijalankan di akhir blok, juga saat exception terjadi.
using (var pencatat1 = new Pencatat("pesan-1.txt"))
{
    pencatat1.Tulis("Kunjungan pertama");
    pencatat1.Tulis("Kunjungan kedua");
}
Console.WriteLine("Blok using pertama telah berakhir.");

try
{
    using var pencatat2 = new Pencatat("pesan-2.txt");
    pencatat2.Tulis("Catatan terakhir sebelum kegagalan");
    throw new InvalidOperationException("Penulisan gagal karena sumber daya penuh.");
}
catch (InvalidOperationException ex)
{
    Console.WriteLine($"Exception tertangkap: {ex.Message}");
}

// Kelas pemakai sumber daya wajib menerapkan IDisposable.
class Pencatat : IDisposable
{
    private readonly string namaBerkas;
    private bool sudahDispose;

    public Pencatat(string namaBerkas)
    {
        this.namaBerkas = namaBerkas;
        Console.WriteLine($"Pencatat untuk {namaBerkas} dibuka.");
    }

    public void Tulis(string pesan)
    {
        if (sudahDispose)
        {
            throw new ObjectDisposedException(nameof(Pencatat));
        }
        Console.WriteLine($"Menulis ke {namaBerkas}: {pesan}");
    }

    public void Dispose()
    {
        if (sudahDispose)
        {
            return;
        }
        sudahDispose = true;
        Console.WriteLine($"Dispose dipanggil untuk {namaBerkas}.");
    }
}
text Keluaran
Pencatat untuk pesan-1.txt dibuka.
Menulis ke pesan-1.txt: Kunjungan pertama
Menulis ke pesan-1.txt: Kunjungan kedua
Dispose dipanggil untuk pesan-1.txt.
Blok using pertama telah berakhir.
Pencatat untuk pesan-2.txt dibuka.
Menulis ke pesan-2.txt: Catatan terakhir sebelum kegagalan
Dispose dipanggil untuk pesan-2.txt.
Exception tertangkap: Penulisan gagal karena sumber daya penuh.

Pada percobaan pertama, Dispose dipanggil tepat setelah baris terakhir blok using, sebelum pernyataan berikutnya dijalankan. Pada percobaan kedua, exception InvalidOperationException dilempar setelah satu pesan tertulis, dan keluarannya menunjukkan Dispose tetap dipanggil sebelum blok catch berjalan. Lingkup using var adalah blok yang melingkupinya, yaitu blok try pada percobaan kedua, sehingga pembersihannya terjadi pada akhir blok tersebut dan bukan pada akhir metode.

Satu pernyataan using menampung satu variabel, sedangkan dua sumber daya ditulis sebagai dua pernyataan yang bersarang atau dua deklarasi using var berurutan. Pembebasannya berlangsung urut terbalik, yaitu sumber daya yang dideklarasikan terakhir dibebaskan lebih dahulu. Urutan terbalik ini mengikuti logika penumpukan, karena sumber daya yang dibuka belakangan sering bergantung pada sumber daya yang dibuka lebih awal.

Menerapkan Dispose#

Implementasi Dispose yang benar bersifat idempoten, artinya dipanggil beberapa kali tidak menimbulkan error dan hanya membersihkan sekali. Cara bakunya adalah flag sudahDispose yang diperiksa di awal metode. Metode lain yang bergantung pada sumber dayanya sebaiknya juga memeriksa flag tersebut, lalu melempar ObjectDisposedException bila kelasnya sudah dibebaskan.

Untuk kelas yang memegang sumber daya tak terkelola, pola lengkapnya menambahkan metode Dispose(bool) yang dapat diwariskan serta panggilan GC.SuppressFinalize agar finalizer tidak melakukan pekerjaan yang sama dua kali. Program berikut mempraktikkan pola itu dengan memori yang dialokasikan langsung melalui Marshal.AllocHGlobal, yang berada di luar kendali garbage collector sehingga satu-satunya jalan pelepasannya adalah Dispose. Simpan programnya sebagai BlokMemori.cs, lalu jalankan dengan dotnet run --file BlokMemori.cs.

csharp BlokMemori.cs
// Pola Dispose dengan sumber daya tak terkelola sungguhan.
using System.Runtime.InteropServices;

using (var buffer = new BlokMemori(64))
{
    buffer.Tulis(0, 12345);
    Console.WriteLine($"Nilai terbaca kembali: {buffer.Baca(0)}");
}

class BlokMemori : IDisposable
{
    private IntPtr alamat;
    private bool sudahDispose;

    public BlokMemori(int ukuran)
    {
        alamat = Marshal.AllocHGlobal(ukuran);
        Console.WriteLine($"Blok {ukuran} byte dialokasikan.");
    }

    public void Tulis(int posisi, int nilai)
    {
        Marshal.WriteInt32(alamat, posisi, nilai);
    }

    public int Baca(int posisi)
    {
        return Marshal.ReadInt32(alamat, posisi);
    }

    public void Dispose()
    {
        if (sudahDispose)
        {
            return;
        }
        Marshal.FreeHGlobal(alamat);
        sudahDispose = true;
        Console.WriteLine("Blok memori dibebaskan.");
        GC.SuppressFinalize(this);
    }
}
text Keluaran
Blok 64 byte dialokasikan.
Nilai terbaca kembali: 12345
Blok memori dibebaskan.

Metode Dispose membebaskan blok memori sekali saja, lalu GC.SuppressFinalize menyatakan pekerjaan itu selesai sehingga finalizer dapat dilewati. Setelah blok using berakhir, alokasinya sudah kembali ke sistem operasi tanpa menunggu siklus garbage collector mana pun. Pada kelas yang hanya memegang sumber daya terkelola, pola Dispose(bool) dan finalizer umumnya tidak diperlukan, dan Dispose sederhana sudah memadai.

Kesalahan umum#

  • Objek setelah Dispose masih dipakai. Metode yang bergantung pada sumber daya yang sudah dibebaskan melempar ObjectDisposedException atau bekerja dengan keadaan yang rusak. Jaga flag sudahDispose dan periksa di awal setiap metode yang memakai sumber daya.
  • Mengandalkan garbage collector untuk sumber daya non-memori. Alokasi berkas dan koneksi jaringan tidak dibebaskan segera, sehingga berkas bisa tetap terkunci dan koneksi bisa menggantung. Terapkan IDisposable dan panggil Dispose melalui using.
  • Metode Dispose yang melempar exception. Pembersihan beruntun pada beberapa sumber daya dapat terputus di tengah jalan. Pola yang diharapkan adalah Dispose tidak melempar exception apa pun, dan kegagalan pembersihan hanya dicatat.

Ringkasan#

  • IDisposable menyatakan kontrak pelepasan sumber daya melalui metode Dispose, dan pernyataan using menjamin panggilannya pada akhir blok, termasuk saat exception terjadi.
  • Bentuk using var menyederhanakan penulisan dengan lingkup pembebasan pada akhir blok yang melingkupinya.
  • Implementasi Dispose yang benar bersifat idempoten, menjaga flag sudahDispose, dan memanggil GC.SuppressFinalize pada pola lengkapnya.
  • Sumber daya tak terkelola seperti memori dari Marshal.AllocHGlobal hanya dapat dibebaskan dengan pasti melalui Dispose.

Referensi#