Menetapkan objek ke null vs Dispose ()

108

Saya terpesona dengan cara kerja CLR dan GC (saya sedang mengembangkan pengetahuan saya tentang hal ini dengan membaca CLR melalui C #, buku / postingan Jon Skeet, dan banyak lagi).

Bagaimanapun, apa perbedaan antara mengatakan:

MyClass myclass = new MyClass();
myclass = null;

Atau, dengan membuat MyClass mengimplementasikan IDisposable dan destruktor dan memanggil Dispose ()?

Juga, jika saya memiliki blok kode dengan pernyataan using (misalnya di bawah), jika saya melangkah melalui kode dan keluar dari blok using, apakah objek tersebut akan dibuang atau ketika terjadi pengumpulan sampah? Apa yang akan terjadi jika saya memanggil Dispose () di blok penggunaan anyay?

using (MyDisposableObj mydispobj = new MyDisposableObj())
{

}

Kelas aliran (misalnya BinaryWriter) memiliki metode Finalisasi? Mengapa saya ingin menggunakan itu?

GurdeepS
sumber

Jawaban:

210

Penting untuk memisahkan pembuangan dari pengumpulan sampah. Mereka benar-benar hal yang terpisah, dengan satu kesamaan yang akan saya bahas sebentar lagi.

Dispose, pengumpulan dan penyelesaian sampah

Saat Anda menulis usingpernyataan, itu hanyalah gula sintaksis untuk blok coba / akhirnya sehingga yang Disposedipanggil bahkan jika kode di badan usingpernyataan itu mengeluarkan pengecualian. Ini tidak berarti bahwa objek tersebut adalah sampah yang dikumpulkan di akhir blok.

Pembuangan adalah tentang sumber daya yang tidak dikelola ( sumber daya non-memori). Ini bisa berupa pegangan UI, koneksi jaringan, pegangan file, dll. Ini adalah sumber daya yang terbatas, jadi Anda biasanya ingin merilisnya secepat mungkin. Anda harus mengimplementasikan IDisposablesetiap kali tipe Anda "memiliki" sumber daya yang tidak dikelola, baik secara langsung (biasanya melalui IntPtr) atau tidak langsung (misalnya melalui a Stream, a SqlConnectiondll).

Pengumpulan sampah itu sendiri hanya tentang memori - dengan satu putaran kecil. Pengumpul sampah dapat menemukan objek yang tidak lagi dapat dirujuk, dan membebaskannya. Ia tidak mencari sampah sepanjang waktu - hanya ketika mendeteksi bahwa ia perlu (misalnya jika satu "generasi" dari heap kehabisan memori).

Perubahan itu adalah finalisasi . Pengumpul sampah menyimpan daftar objek yang tidak lagi dapat dijangkau, tetapi memiliki finalizer (ditulis seperti ~Foo()dalam C #, agak membingungkan - tidak seperti C ++ destructors). Ini menjalankan finalizer pada objek-objek ini, kalau-kalau mereka perlu melakukan pembersihan ekstra sebelum memori mereka dibebaskan.

Finalizer hampir selalu digunakan untuk membersihkan sumber daya jika pengguna jenis tersebut lupa membuangnya secara tertib. Jadi jika Anda membuka FileStreamtetapi lupa untuk memanggil Disposeatau Close, finalizer pada akhirnya akan melepaskan pegangan file yang mendasarinya untuk Anda. Dalam program yang ditulis dengan baik, finalisator seharusnya tidak pernah menyala menurut saya.

Menetapkan variabel ke null

Satu hal kecil tentang menyetel variabel ke null- ini hampir tidak pernah diperlukan demi pengumpulan sampah. Terkadang Anda mungkin ingin melakukannya jika itu adalah variabel anggota, meskipun menurut pengalaman saya jarang "bagian" dari suatu objek tidak lagi diperlukan. Ketika itu adalah variabel lokal, JIT biasanya cukup pintar (dalam mode rilis) untuk mengetahui kapan Anda tidak akan menggunakan referensi lagi. Sebagai contoh:

StringBuilder sb = new StringBuilder();
sb.Append("Foo");
string x = sb.ToString();

// The string and StringBuilder are already eligible
// for garbage collection here!
int y = 10;
DoSomething(y);

// These aren't helping at all!
x = null;
sb = null;

// Assume that x and sb aren't used here

Satu waktu di mana mungkin layak untuk mengatur variabel lokal nulladalah ketika Anda berada dalam loop, dan beberapa cabang dari loop perlu menggunakan variabel tetapi Anda tahu Anda telah mencapai titik di mana Anda tidak melakukannya. Sebagai contoh:

SomeObject foo = new SomeObject();

for (int i=0; i < 100000; i++)
{
    if (i == 5)
    {
        foo.DoSomething();
        // We're not going to need it again, but the JIT
        // wouldn't spot that
        foo = null;
    }
    else
    {
        // Some other code 
    }
}

Menerapkan IDisposable / finalizers

Jadi, haruskah tipe Anda menerapkan finalizer? Hampir pasti tidak. Jika Anda hanya memiliki sumber daya yang tidak dikelola secara tidak langsung (misalnya Anda memiliki FileStreamvariabel sebagai anggota), maka menambahkan finalizer Anda sendiri tidak akan membantu: aliran hampir pasti memenuhi syarat untuk pengumpulan sampah saat objek Anda, jadi Anda dapat mengandalkan FileStreammemiliki finalizer (jika perlu - ini mungkin merujuk ke sesuatu yang lain, dll). Jika Anda ingin memiliki sumber daya yang tidak dikelola "hampir" secara langsung, SafeHandleitu teman Anda - dibutuhkan sedikit waktu untuk memulai, tetapi itu berarti Anda hampir tidak perlu menulis finalizer lagi . Anda biasanya hanya memerlukan finalizer jika Anda memiliki pegangan yang benar-benar langsung pada sumber daya (an IntPtr) dan Anda harus melihat untuk pindah keSafeHandlesecepat yang kamu bisa. (Ada dua tautan di sana - baca keduanya, idealnya.)

Joe Duffy memiliki seperangkat pedoman yang sangat panjang seputar finalisator dan IDisposable (ditulis bersama dengan banyak orang pintar) yang layak dibaca. Perlu diketahui bahwa jika Anda menyegel kelas Anda, itu membuat hidup jauh lebih mudah: pola penimpaan Disposeuntuk memanggil Dispose(bool)metode virtual baru dll hanya relevan ketika kelas Anda dirancang untuk warisan.

Ini memang sedikit bertele-tele, tapi tolong tanyakan klarifikasi di mana Anda ingin :)

Jon Skeet
sumber
Re "Satu kali di mana mungkin ada gunanya menyetel variabel lokal ke null" - mungkin juga beberapa skenario "tangkap" yang lebih rumit (beberapa tangkapan dari variabel yang sama) - tetapi mungkin tidak ada gunanya memperumit posting! +1 ...
Marc Gravell
@ Marc: Itu benar - saya bahkan belum memikirkan variabel yang ditangkap. Hmm. Ya, saya pikir saya akan membiarkannya;)
Jon Skeet
dapatkah Anda mengetahui bahwa apa yang akan terjadi jika Anda menyetel "foo = null" dalam cuplikan kode Anda di atas? Sejauh yang saya tahu, baris itu hanya menghapus nilai variabel yang menunjuk ke objek foo di heap terkelola? jadi pertanyaannya adalah apa yang akan terjadi pada objek foo disana? bukankah seharusnya kita sebut membuang itu?
odiseh
@odiseh: Jika benda itu bisa dibuang, maka ya - Anda harus membuangnya. Bagian jawaban itu hanya berurusan dengan pengumpulan sampah, yang sepenuhnya terpisah.
Jon Skeet
1
Saya sedang mencari klarifikasi tentang beberapa masalah IDisposable, jadi saya mencari "IDisposable Skeet" di Google dan menemukan ini. Bagus! : D
Maciej Wozniak
22

Saat Anda membuang suatu objek, sumber daya dibebaskan. Saat Anda menetapkan null ke variabel, Anda hanya mengubah referensi.

myclass = null;

Setelah Anda menjalankan ini, objek yang dirujuk oleh myclass masih ada, dan akan berlanjut hingga GC membersihkannya. Jika Buang secara eksplisit dipanggil, atau berada dalam blok penggunaan, sumber daya apa pun akan dibebaskan secepat mungkin.

rekursif
sumber
7
Ini mungkin tidak masih ada setelah menjalankan baris itu - mungkin sampah dikumpulkan sebelum baris itu. JIT itu cerdas - membuat garis seperti ini hampir selalu tidak relevan.
Jon Skeet
6
Menyetel ke null dapat berarti bahwa resource yang dipegang oleh objek tersebut tidak pernah dibebaskan. GC tidak membuang, hanya menyelesaikan, jadi jika objek secara langsung menyimpan sumber daya yang tidak dikelola dan finalisasinya tidak membuang (atau tidak memiliki finalizer) maka sumber daya tersebut akan bocor. Sesuatu yang harus diperhatikan.
LukeH
6

Kedua operasi tersebut tidak banyak berhubungan satu sama lain. Saat Anda menyetel referensi ke null, itu akan melakukannya. Itu sendiri tidak mempengaruhi kelas yang dirujuk sama sekali. Variabel Anda tidak lagi menunjuk ke objek yang dulu, tetapi objek itu sendiri tidak berubah.

Saat Anda memanggil Dispose (), itu adalah panggilan metode pada objek itu sendiri. Apapun yang dilakukan metode Buang, sekarang sudah selesai pada objek. Tetapi ini tidak memengaruhi referensi Anda ke objek tersebut.

Satu-satunya area yang tumpang tindih adalah ketika tidak ada lagi referensi ke suatu objek, pada akhirnya akan mengumpulkan sampah. Dan jika kelas mengimplementasikan antarmuka IDisposable, maka Dispose () akan dipanggil pada objek sebelum sampah dikumpulkan.

Tapi itu tidak akan terjadi segera setelah Anda menyetel referensi ke null, karena dua alasan. Pertama, referensi lain mungkin ada, sehingga tidak akan mengumpulkan sampah sama sekali, dan kedua, meskipun itu adalah referensi terakhir, sehingga sekarang siap untuk pengumpulan sampah, tidak akan terjadi apa-apa hingga pengumpul sampah memutuskan untuk menghapus objek.

Memanggil Dispose () pada objek tidak "membunuh" objek dengan cara apa pun. Ini biasanya digunakan untuk membersihkan sehingga objek dapat dihapus dengan aman setelahnya, tetapi pada akhirnya, tidak ada yang ajaib tentang Buang, ini hanya metode kelas.

jalf
sumber
Saya pikir jawaban ini memuji atau merupakan detail dari jawaban "rekursif".
dance2die