Akankah Pengumpul Sampah menyebut IDisposable. Buang untukku?

139

.NET IDisposable Pattern menyiratkan bahwa jika Anda menulis finalizer, dan mengimplementasikan IDisposable, finalizer Anda perlu secara eksplisit memanggil Dispose. Ini logis, dan itulah yang selalu saya lakukan dalam situasi langka di mana seorang finalis dijamin.

Namun, apa yang terjadi jika saya hanya melakukan ini:

class Foo : IDisposable
{
     public void Dispose(){ CloseSomeHandle(); }
}

dan tidak menerapkan finalizer, atau apa pun. Akankah kerangka kerja memanggil metode Buang untuk saya?

Ya, saya menyadari ini terdengar bodoh, dan semua logika menyiratkan bahwa itu tidak akan terjadi, tetapi saya selalu memiliki 2 hal di belakang kepala saya yang membuat saya tidak yakin.

  1. Seseorang beberapa tahun yang lalu pernah mengatakan kepada saya bahwa itu sebenarnya akan melakukan ini, dan orang itu memiliki rekam jejak yang sangat kuat dalam "mengetahui barang-barang mereka."

  2. Kompilator / kerangka kerja melakukan hal-hal 'ajaib' lain tergantung pada antarmuka apa yang Anda terapkan (misalnya: foreach, metode ekstensi, serialisasi berdasarkan atribut, dll), jadi masuk akal bahwa ini mungkin juga 'ajaib'.

Sementara saya sudah membaca banyak hal tentang hal itu, dan sudah ada banyak hal tersirat, aku tidak pernah bisa menemukan definitif Ya atau Tidak ada jawaban untuk pertanyaan ini.

Orion Edwards
sumber

Jawaban:

122

.Net Garbage Collector memanggil metode Object.Finalize dari sebuah objek pada pengumpulan sampah. Secara default, ini tidak melakukan apa pun dan harus diganti jika Anda ingin membebaskan sumber daya tambahan.

Buang TIDAK secara otomatis dipanggil dan harus secara eksplisit dipanggil jika sumber daya akan dilepaskan, seperti dalam blok 'menggunakan' atau 'coba akhirnya'

lihat http://msdn.microsoft.com/en-us/library/system.object.finalize.aspx untuk informasi lebih lanjut

Xian
sumber
36
Sebenarnya, saya tidak percaya GC memanggil Object. Selesaikan sama sekali jika tidak diganti. Objek ditentukan untuk secara efektif tidak memiliki finalizer, dan finalisasi disembunyikan - yang membuatnya lebih efisien, karena objek tidak perlu berada di antrian finalisasi / freachable.
Jon Skeet
8
Sesuai MSDN: msdn.microsoft.com/en-us/library/… Anda tidak dapat benar-benar "menimpa" metode Object.Finalize di C #, kompilator menghasilkan kesalahan: Jangan timpa objek.Finalize. Sebagai gantinya, sediakan destruktor. ; yaitu Anda harus menerapkan destruktor yang secara efektif bertindak sebagai Finalizer. [hanya ditambahkan di sini untuk kelengkapan karena ini adalah jawaban yang diterima dan kemungkinan besar dibaca]
Sudhanshu Mishra
1
GC tidak melakukan apa pun pada objek yang tidak menimpa Finalizer. Itu tidak diletakkan di antrian Finalisasi - dan tidak ada Finalizer yang dipanggil.
Dave Black
1
@dotnetguy - meskipun spesifikasi C # asli menyebutkan "destruktor", sebenarnya disebut Finalizer - dan mekanismenya sama sekali berbeda dari cara kerja "destruktor" yang sebenarnya untuk bahasa yang tidak dikelola.
Dave Black
68

Saya ingin menekankan poin Brian dalam komentarnya, karena itu penting.

Finalizer bukanlah destruktor deterministik seperti di C ++. Seperti orang lain telah menunjukkan, tidak ada jaminan ketika itu akan disebut, dan memang jika Anda memiliki cukup memori, jika akan pernah disebut.

Tetapi hal buruk tentang finalisator adalah, seperti yang dikatakan Brian, hal itu menyebabkan objek Anda tetap berada dalam pengumpulan sampah. Ini bisa jadi buruk. Mengapa?

Seperti yang Anda mungkin atau mungkin tidak tahu, GC dibagi menjadi beberapa generasi - Gen 0, 1 dan 2, ditambah Large Object Heap. Split adalah istilah yang longgar - Anda mendapatkan satu blok memori, tetapi ada petunjuk di mana objek Gen 0 dimulai dan diakhiri.

Proses berpikirnya adalah Anda kemungkinan besar akan menggunakan banyak objek yang berumur pendek. Jadi itu harus mudah dan cepat bagi GC untuk mendapatkan - objek Gen 0. Jadi ketika ada tekanan memori, hal pertama yang dilakukannya adalah koleksi Gen 0.

Sekarang, jika itu tidak menyelesaikan cukup tekanan, maka itu kembali dan melakukan sapuan Gen 1 (mengulangi Gen 0), dan kemudian jika masih belum cukup, ia melakukan sapuan Gen 2 (mengulangi Gen 1 dan Gen 0). Jadi membersihkan benda yang berumur panjang bisa memakan waktu cukup lama dan biayanya agak mahal (karena utas Anda mungkin tersangkut selama pengoperasian).

Artinya jika Anda melakukan sesuatu seperti ini:

~MyClass() { }

Objek Anda, apa pun yang terjadi, akan ditampilkan ke Generasi 2. Ini karena GC tidak memiliki cara untuk memanggil finalizer selama pengumpulan sampah. Jadi objek yang harus diselesaikan dipindahkan ke antrian khusus untuk dibersihkan oleh utas yang berbeda (utas finalizer - yang jika Anda membunuh membuat semua jenis hal buruk terjadi). Ini berarti objek Anda bertahan lebih lama, dan berpotensi memaksa lebih banyak pengumpulan sampah.

Jadi, semua itu hanya untuk menyampaikan poin bahwa Anda ingin menggunakan IDisposable untuk membersihkan sumber daya bila memungkinkan dan secara serius mencoba mencari cara untuk menggunakan finalizer. Ini demi kepentingan terbaik aplikasi Anda.

Cory Foy
sumber
8
Saya setuju bahwa Anda ingin menggunakan IDisposable bila memungkinkan, tetapi Anda juga harus memiliki finalizer yang memanggil metode pembuangan. Anda bisa memanggil GC.SuppressFinalize () di IDispose.Dispose setelah memanggil metode dispose Anda untuk memastikan objek Anda tidak dimasukkan ke dalam antrian finalizer.
jColeson
2
Generasi diberi nomor 0-2, bukan 1-3, tetapi pos Anda sebaliknya bagus. Saya akan menambahkannya, bahwa objek apa pun yang direferensikan oleh objek Anda, atau objek apa pun yang direferensikan oleh objek tersebut, dll. Juga akan dilindungi dari pengumpulan sampah (meskipun tidak terhadap penyelesaian) untuk generasi lain. Dengan demikian, objek dengan finalizer tidak boleh memiliki referensi ke apa pun yang tidak diperlukan untuk finalisasi.
supercat
Referensi (sejenis) untuk nomor generasi
Lightness Races di Orbit
3
Mengenai "Objek Anda, tidak peduli apa, akan hidup untuk Generasi 2." Ini adalah informasi yang SANGAT mendasar! Ini menghemat banyak waktu debugging sistem, di mana ada banyak objek Gen2 berumur pendek yang "disiapkan" untuk finalisasi, tetapi tidak pernah diselesaikan menyebabkan OutOfMemoryException karena penggunaan heap yang berat. Menghapus finalizer (bahkan kosong) dan memindahkan (mengerjakan) kode ke tempat lain, masalah menghilang dan GC mampu menangani beban.
rautan
@CoryFoy "Objek Anda, tidak peduli apa, akan hidup untuk Generasi 2" Apakah ada dokumentasi untuk ini?
Ashish Negi
33

Sudah ada banyak diskusi bagus di sini, dan saya sedikit terlambat ke pesta, tetapi saya ingin menambahkan beberapa poin sendiri.

  • Pengumpul sampah tidak akan pernah secara langsung menjalankan metode Buang untuk Anda.
  • GC akan mengeksekusi finalizer saat diinginkan.
  • Salah satu pola umum yang digunakan untuk objek yang memiliki finalizer adalah membuatnya memanggil metode yang menurut konvensi didefinisikan sebagai Dispose (bool disposing) lewat false untuk menunjukkan bahwa panggilan dilakukan karena penyelesaian daripada panggilan Buang eksplisit.
  • Ini karena tidak aman membuat asumsi apa pun tentang objek terkelola lainnya saat menyelesaikan objek (mungkin sudah diselesaikan).

class SomeObject : IDisposable {
 IntPtr _SomeNativeHandle;
 FileStream _SomeFileStream;

 // Something useful here

 ~ SomeObject() {
  Dispose(false);
 }

 public void Dispose() {
  Dispose(true);
 }

 protected virtual void Dispose(bool disposing) {
  if(disposing) {
   GC.SuppressFinalize(this);
   //Because the object was explicitly disposed, there will be no need to 
   //run the finalizer.  Suppressing it reduces pressure on the GC

   //The managed reference to an IDisposable is disposed only if the 
   _SomeFileStream.Dispose();
  }

  //Regardless, clean up the native handle ourselves.  Because it is simple a member
  // of the current instance, the GC can't have done anything to it, 
  // and this is the onlyplace to safely clean up

  if(IntPtr.Zero != _SomeNativeHandle) {
   NativeMethods.CloseHandle(_SomeNativeHandle);
   _SomeNativeHandle = IntPtr.Zero;
  }
 }
}

Itu adalah versi sederhana, tetapi ada banyak nuansa yang dapat membuat Anda tersandung pada pola ini.

  • Kontrak untuk IDisposable.Dispose menunjukkan bahwa itu harus aman untuk menelepon beberapa kali (memanggil Buang pada objek yang sudah dibuang seharusnya tidak melakukan apa-apa)
  • Dapat menjadi sangat rumit untuk mengelola hierarki pewarisan objek sekali pakai dengan benar, terutama jika lapisan yang berbeda memperkenalkan sumber daya sekali pakai dan tidak terkelola. Dalam pola di atas, Buang (bool) bersifat virtual untuk memungkinkannya diganti sehingga dapat dikelola, tetapi menurut saya ini rawan kesalahan.

Menurut pendapat saya, jauh lebih baik untuk sepenuhnya menghindari jenis apa pun yang secara langsung berisi referensi sekali pakai dan sumber daya asli yang mungkin memerlukan penyelesaian. SafeHandles menyediakan cara yang sangat bersih untuk melakukan ini dengan mengenkapsulasi sumber daya asli menjadi sekali pakai yang secara internal menyediakan penyelesaiannya sendiri (bersama dengan sejumlah manfaat lain seperti menghapus jendela selama P / Invoke di mana pegangan asli bisa hilang karena pengecualian asinkron) .

Hanya dengan mendefinisikan SafeHandle membuat ini Sepele:


private class SomeSafeHandle
 : SafeHandleZeroOrMinusOneIsInvalid {
 public SomeSafeHandle()
  : base(true)
  { }

 protected override bool ReleaseHandle()
 { return NativeMethods.CloseHandle(handle); }
}

Memungkinkan Anda untuk menyederhanakan tipe yang memuat menjadi:


class SomeObject : IDisposable {
 SomeSafeHandle _SomeSafeHandle;
 FileStream _SomeFileStream;
 // Something useful here
 public virtual void Dispose() {
  _SomeSafeHandle.Dispose();
  _SomeFileStream.Dispose();
 }
}
Andrew
sumber
1
Dari mana kelas SafeHandleZeroOrMinusOneIsInvalid berasal? Apakah itu tipe built in .net?
Orion Edwards
+1 untuk // Menurut pendapat saya, jauh lebih baik untuk sepenuhnya menghindari jenis apa pun yang secara langsung berisi referensi sekali pakai dan sumber daya asli yang mungkin memerlukan penyelesaian.// Satu-satunya kelas tak tersegel yang seharusnya memiliki finalisator adalah kelas yang tujuannya berfokus pada finalisasi.
supercat
1
@OrionEdwards ya lihat msdn.microsoft.com/en-us/library/…
Martin Capodici
1
Mengenai panggilan ke GC.SuppressFinalizedalam contoh ini. Dalam konteks ini, SuppressFinalize hanya boleh dipanggil jika Dispose(true)berhasil dijalankan. Jika Dispose(true)gagal di beberapa titik setelah finalisasi disembunyikan tetapi sebelum semua sumber daya (terutama yang tidak dikelola) dibersihkan, Anda masih ingin penyelesaian terjadi untuk melakukan pembersihan sebanyak mungkin. Lebih baik memindahkan GC.SuppressFinalizepanggilan ke Dispose()metode setelah panggilan ke Dispose(true). Lihat Pedoman Desain Kerangka dan posting ini .
BitMask777
6

Saya rasa tidak. Anda memiliki kendali atas kapan Buang dipanggil, yang berarti Anda secara teori dapat menulis kode pembuangan yang membuat asumsi tentang (misalnya) keberadaan objek lain. Anda tidak memiliki kendali atas kapan finalizer dipanggil, jadi akan sulit jika finalizer secara otomatis memanggil Buang atas nama Anda.


EDIT: Saya pergi dan menguji, hanya untuk memastikan:

class Program
{
    static void Main(string[] args)
    {
        Fred f = new Fred();
        f = null;
        GC.Collect();
        GC.WaitForPendingFinalizers();
        Console.WriteLine("Fred's gone, and he's not coming back...");
        Console.ReadLine();
    }
}

class Fred : IDisposable
{
    ~Fred()
    {
        Console.WriteLine("Being finalized");
    }

    void IDisposable.Dispose()
    {
        Console.WriteLine("Being Disposed");
    }
}
Matt Bishop
sumber
Membuat asumsi tentang objek yang tersedia untuk Anda selama pembuangan bisa berbahaya dan rumit, terutama selama penyelesaian.
Scott Dorman
3

Tidak dalam kasus yang Anda gambarkan, Tapi GC akan memanggil Finalizer untuk Anda, jika Anda memilikinya.

NAMUN. Pengumpulan sampah berikutnya, alih-alih dikumpulkan, objek akan masuk ke antrian finalisasi, semuanya dikumpulkan, lalu finalisasinya dipanggil. Koleksi selanjutnya setelah itu akan dibebaskan.

Bergantung pada tekanan memori aplikasi Anda, Anda mungkin tidak memiliki gc untuk pembuatan objek tersebut untuk sementara waktu. Jadi dalam kasus, katakanlah, aliran file atau koneksi db, Anda mungkin harus menunggu beberapa saat agar sumber daya yang tidak dikelola dibebaskan dalam panggilan finalizer untuk sementara waktu, menyebabkan beberapa masalah.

Brian Leahy
sumber
1

Tidak, itu tidak disebut.

Tapi ini memudahkan untuk tidak lupa membuang benda Anda. Cukup gunakan usingkata kunci.

Saya melakukan tes berikut untuk ini:

class Program
{
    static void Main(string[] args)
    {
        Foo foo = new Foo();
        foo = null;
        Console.WriteLine("foo is null");
        GC.Collect();
        Console.WriteLine("GC Called");
        Console.ReadLine();
    }
}

class Foo : IDisposable
{
    public void Dispose()
    {

        Console.WriteLine("Disposed!");
    }
penyaskito
sumber
1
Ini adalah contoh bagaimana jika Anda TIDAK menggunakan kata kunci <code> menggunakan </code> maka tidak akan dipanggil ... dan cuplikan ini berisi 9 tahun, selamat ulang tahun!
penyaskito
1

GC tidak akan memanggil buang. Ini mungkin memanggil finalizer Anda, tetapi bahkan ini tidak dijamin dalam semua keadaan.

Lihat artikel ini untuk diskusi tentang cara terbaik menangani ini.

Rob Walker
sumber
0

Dokumentasi IDisposable memberikan penjelasan yang cukup jelas dan rinci tentang perilaku, serta contoh kode. GC TIDAK akan memanggil Dispose()metode pada antarmuka, tetapi akan memanggil finalizer untuk objek Anda.

Joseph Daigle
sumber
0

Pola IDisposable dibuat terutama untuk dipanggil oleh pengembang, jika Anda memiliki objek yang mengimplementasikan IDispose pengembang harus mengimplementasikan usingkata kunci di sekitar konteks objek atau memanggil metode Dispose secara langsung.

Keamanan gagal untuk pola tersebut adalah dengan mengimplementasikan finalizer yang memanggil metode Dispose (). Jika Anda tidak melakukannya, Anda dapat membuat beberapa kebocoran memori yaitu: Jika Anda membuat beberapa pembungkus COM dan tidak pernah memanggil System.Runtime.Interop.Marshall.ReleaseComObject (comObject) (yang akan ditempatkan di metode Buang).

Tidak ada keajaiban di clr untuk memanggil metode Buang secara otomatis selain melacak objek yang berisi finalizer dan menyimpannya di tabel Finalizer oleh GC dan memanggilnya ketika beberapa heuristik pembersihan dilakukan oleh GC.

Erick Sgarbi
sumber