Bagaimana Anda mencegah IDisposable menyebar ke semua kelas Anda?

138

Mulailah dengan kelas-kelas sederhana ini ...

Katakanlah saya memiliki satu set kelas sederhana seperti ini:

class Bus
{
    Driver busDriver = new Driver();
}

class Driver
{
    Shoe[] shoes = { new Shoe(), new Shoe() };
}

class Shoe
{
    Shoelace lace = new Shoelace();
}

class Shoelace
{
    bool tied = false;
}

A Busmemiliki a Driver, Drivermemiliki dua Shoes, masing-masing Shoememiliki a Shoelace. Semuanya sangat konyol.

Tambahkan objek IDisposable ke Tali Sepatu

Kemudian saya memutuskan bahwa beberapa operasi pada Shoelacedapat menjadi multi-utas, jadi saya menambahkan EventWaitHandleuntuk utas untuk berkomunikasi. Jadi Shoelacesekarang terlihat seperti ini:

class Shoelace
{
    private AutoResetEvent waitHandle = new AutoResetEvent(false);
    bool tied = false;
    // ... other stuff ..
}

Menerapkan IDisposable di Shoelace

Tapi sekarang Microsoft FxCop akan mengeluh: "Implementasikan IDisposable di 'Shoelace' karena itu membuat anggota IDisposable type berikut: 'EventWaitHandle'."

Oke, saya menerapkan IDisposablepada Shoelacedan rapi kelas kecil saya menjadi kekacauan ini mengerikan:

class Shoelace : IDisposable
{
    private AutoResetEvent waitHandle = new AutoResetEvent(false);
    bool tied = false;
    private bool disposed = false;

    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this);
    }

    ~Shoelace()
    {
        Dispose(false);
    }

    protected virtual void Dispose(bool disposing)
    {
        if (!this.disposed)
        {
            if (disposing)
            {
                if (waitHandle != null)
                {
                    waitHandle.Close();
                    waitHandle = null;
                }
            }
            // No unmanaged resources to release otherwise they'd go here.
        }
        disposed = true;
    }
}

Atau (seperti yang ditunjukkan oleh komentator) karena Shoelaceitu sendiri tidak memiliki sumber daya yang tidak terkelola, saya mungkin menggunakan implementasi pembuangan yang lebih sederhana tanpa memerlukan Dispose(bool)dan Destructor:

class Shoelace : IDisposable
{
    private AutoResetEvent waitHandle = new AutoResetEvent(false);
    bool tied = false;

    public void Dispose()
    {
        if (waitHandle != null)
        {
            waitHandle.Close();
            waitHandle = null;
        }
        GC.SuppressFinalize(this);
    }
}

Saksikan dengan ngeri saat ID sekali pakai menyebar

Benar itu sudah diperbaiki. Tapi sekarang FxCop akan komplain yang Shoemembuat Shoelace, jadi Shoepasti IDisposablejuga.

Dan Drivermenciptakan harus Shoebegitu . Dan menciptakan begitu harus dan seterusnya.DriverIDisposableBusDriverBusIDisposable

Tiba-tiba perubahan kecil saya menjadi Shoelacemenyebabkan saya banyak pekerjaan dan bos saya bertanya-tanya mengapa saya perlu membayar Busuntuk membuat perubahan Shoelace.

Pertanyaan

Bagaimana Anda mencegah penyebaran ini IDisposable, tetapi masih memastikan bahwa objek yang tidak dikelola dibuang dengan benar?

GrahamS
sumber
9
Sebuah pertanyaan yang sangat bagus, saya percaya jawabannya adalah meminimalkan penggunaannya dan mencoba untuk menjaga IDisposables tingkat tinggi berumur pendek dengan menggunakan, tetapi ini tidak selalu mungkin (terutama di mana IDisposables tersebut disebabkan interop dengan C ++ dll atau serupa). Lihat jawabannya.
annakata
Oke Dan, saya telah memperbarui pertanyaan untuk menunjukkan kedua metode penerapan IDisposable di Tali Sepatu.
GrahamS
Saya biasanya berhati-hati dalam mengandalkan detail implementasi kelas lain untuk melindungi saya. Tidak ada gunanya mengambil risiko jika saya dapat dengan mudah mencegahnya. Mungkin saya terlalu berhati-hati atau mungkin saya menghabiskan waktu terlalu lama sebagai programmer C, tapi saya lebih suka mengambil pendekatan Irlandia: "Yang pasti, untuk memastikan" :)
GrahamS
@Dan: Pemeriksaan null masih diperlukan untuk memastikan bahwa objek itu sendiri belum disetel ke null, dalam hal ini panggilan ke waitHandle.Dispose () akan memunculkan NullReferenceException.
Scott Dorman
1
Apa pun yang terjadi, Anda sebenarnya harus tetap menggunakan metode Buang (bool) seperti yang ditunjukkan di bagian "Terapkan ID yang Dapat Disposable di Tali Sepatu" karena itu (tanpa finalizer) adalah pola lengkapnya. Hanya karena sebuah kelas mengimplentasikan IDisposable tidak berarti ia membutuhkan finalizer.
Scott Dorman

Jawaban:

36

Anda tidak bisa benar-benar "mencegah" IDisposable dari penyebaran. Beberapa kelas perlu dibuang, seperti AutoResetEvent, dan cara yang paling efisien adalah melakukannya dalam Dispose()metode untuk menghindari overhead finalizer. Tetapi metode ini harus dipanggil entah bagaimana, jadi persis seperti dalam contoh Anda, kelas yang merangkum atau berisi IDisposable harus membuang ini, jadi mereka harus dibuang juga, dll. Satu-satunya cara untuk menghindarinya adalah dengan:

  • hindari menggunakan kelas IDisposable jika memungkinkan, kunci atau tunggu acara di satu tempat, simpan sumber daya mahal di satu tempat, dll
  • buat mereka hanya ketika Anda membutuhkannya dan buang setelah ( usingpola)

Dalam beberapa kasus IDisposable dapat diabaikan karena mendukung kasus opsional. Misalnya, WaitHandle mengimplementasikan IDisposable untuk mendukung Mutex bernama. Jika sebuah nama tidak digunakan, metode Buang tidak melakukan apa pun. MemoryStream adalah contoh lain, ia tidak menggunakan sumber daya sistem dan implementasi Buangnya juga tidak melakukan apa-apa. Pemikiran yang cermat tentang apakah sumber daya yang tidak dikelola sedang digunakan atau tidak dapat menjadi instruksional. Jadi dapat memeriksa sumber yang tersedia untuk pustaka .net atau menggunakan dekompiler.

Grzenio
sumber
4
Saran bahwa Anda dapat mengabaikannya karena implementasi hari ini tidak melakukan apa-apa adalah buruk, karena versi yang akan datang mungkin benar-benar melakukan sesuatu yang penting dalam Buang, dan sekarang Anda sulit melacak kebocoran.
Andy
1
"simpan sumber daya yang mahal", IDisposable tidak selalu mahal, memang contoh yang menggunakan pegangan tunggu ini adalah tentang "bobot yang ringan" seperti yang Anda dapatkan.
markmnl
20

Dalam hal kebenaran, Anda tidak dapat mencegah penyebaran IDisposable melalui hubungan objek jika objek induk membuat dan pada dasarnya memiliki objek turunan yang sekarang harus dibuang. FxCop benar dalam situasi ini dan orang tua harus IDisposable.

Apa yang dapat Anda lakukan adalah menghindari menambahkan IDisposable ke kelas daun dalam hierarki objek Anda. Ini tidak selalu merupakan tugas yang mudah tetapi ini merupakan latihan yang menarik. Dari perspektif logis, tidak ada alasan bahwa ShoeLace perlu sekali pakai. Alih-alih menambahkan WaitHandle di sini, apakah mungkin juga menambahkan asosiasi antara ShoeLace dan WaitHandle pada titik itu digunakan. Cara termudah adalah melalui instance Dictionary.

Jika Anda dapat memindahkan WaitHandle ke asosiasi longgar melalui peta pada titik WaitHandle benar-benar digunakan maka Anda dapat memutuskan rantai ini.

JaredPar
sumber
2
Rasanya sangat aneh membuat asosiasi di sana. AutoResetEvent ini bersifat pribadi untuk penerapan Tali Sepatu, jadi memaparkannya secara publik menurut saya salah.
GrahamS
@GrahamS, saya tidak mengatakan mengeksposnya secara publik. Saya katakan dapatkah itu dipindahkan ke titik di mana tali sepatu diikat. Mungkin jika mengikat tali sepatu adalah tugas yang rumit, mereka harus menjadi kelas tingkat tali sepatu.
JaredPar
1
Bisakah Anda memperluas jawaban Anda dengan beberapa cuplikan kode JaredPar? Saya mungkin sedikit meregangkan contoh saya, tetapi saya membayangkan bahwa Tali Sepatu membuat dan memulai utas Tyer yang menunggu dengan sabar di waitHandle.WaitOne () Tali sepatu akan memanggil waitHandle.Set () ketika itu ingin utas mulai mengikat.
GrahamS
1
+1 ini. Saya pikir akan aneh bahwa hanya Tali Sepatu yang dipanggil secara bersamaan tetapi tidak yang lain. Pikirkan apa yang seharusnya menjadi akar agregat. Dalam hal ini seharusnya Bus, IMHO, meskipun saya tidak terbiasa dengan domainnya. Jadi, dalam hal ini, bus harus berisi waithandle dan semua operasi terhadap bus dan semua turunannya akan disinkronkan.
Johannes Gustafsson
16

Untuk mencegah IDisposablepenyebaran, Anda harus mencoba merangkum penggunaan objek sekali pakai di dalam satu metode. Coba desain secara Shoelaceberbeda:

class Shoelace { 
  bool tied = false; 

  public void Tie() {

    using (var waitHandle = new AutoResetEvent(false)) {

      // you can even pass the disposable to other methods
      OtherMethod(waitHandle);

      // or hold it in a field (but FxCop will complain that your class is not disposable),
      // as long as you take control of its lifecycle
      _waitHandle = waitHandle;
      OtherMethodThatUsesTheWaitHandleFromTheField();

    } 

  }
} 

Cakupan pegangan tunggu terbatas pada Tiemetode, dan kelas tidak perlu memiliki bidang sekali pakai, sehingga tidak perlu dibuang sendiri.

Karena pegangan tunggu adalah detail implementasi di dalam Shoelace, itu tidak boleh mengubah dengan cara apa pun antarmuka publiknya, seperti menambahkan antarmuka baru dalam deklarasinya. Apa yang akan terjadi kemudian ketika Anda tidak membutuhkan bidang sekali pakai lagi, apakah Anda akan menghapus IDisposabledeklarasi? Jika Anda memikirkan tentang Shoelace abstraksi , Anda akan segera menyadari bahwa abstraksi seharusnya tidak tercemar oleh ketergantungan infrastruktur, seperti IDisposable. IDisposableharus disediakan untuk kelas yang abstraksinya merangkum sumber daya yang memerlukan pembersihan deterministik; yaitu, untuk kelas di mana disposabilitas merupakan bagian dari abstraksi .

Jordão
sumber
1
Saya sepenuhnya setuju bahwa detail implementasi Tali Sepatu tidak boleh mencemari antarmuka publik, tetapi maksud saya ini bisa jadi sulit untuk dihindari. Apa yang Anda sarankan di sini tidak selalu memungkinkan: tujuan AutoResetEvent () adalah untuk berkomunikasi antar utas, sehingga cakupannya biasanya melampaui metode tunggal.
GrahamS
@GrahamS: itulah mengapa saya mengatakan untuk mencoba mendesainnya seperti itu. Anda juga dapat meneruskan metode sekali pakai ke metode lain. Itu hanya rusak ketika panggilan eksternal ke kelas mengontrol siklus hidup sekali pakai. Saya akan memperbarui jawaban yang sesuai.
Jordão
2
maaf, saya tahu Anda dapat menyebarkan produk sekali pakai, tetapi saya masih tidak melihat itu berhasil. Dalam contoh saya, AutoResetEventdigunakan untuk berkomunikasi antara utas berbeda yang beroperasi dalam kelas yang sama, jadi itu harus menjadi variabel anggota. Anda tidak dapat membatasi cakupannya ke metode. (mis. bayangkan bahwa satu utas hanya duduk menunggu beberapa pekerjaan dengan memblokir waitHandle.WaitOne(). Utas utama kemudian memanggil shoelace.Tie()metode, yang hanya melakukan a waitHandle.Set()dan segera kembali).
GrahamS
3

Ini pada dasarnya apa yang terjadi ketika Anda mencampur Komposisi atau Agregasi dengan kelas sekali pakai. Seperti yang disebutkan, jalan keluar pertama adalah dengan memfaktor ulang waitHandle dari tali sepatu.

Karena itu, Anda dapat menghapus pola Disposable secara signifikan ketika Anda tidak memiliki resource yang tidak terkelola. (Saya masih mencari referensi resmi untuk ini.)

Tapi Anda bisa menghilangkan destructor dan GC.SuppressFinalize (this); dan mungkin sedikit membersihkan virtual void Dispose (bool disposing).

Henk Holterman
sumber
Terima kasih Henk. Jika saya mengambil kreasi waitHandle dari Tali Sepatu maka seseorang masih harus membuangnya di suatu tempat jadi bagaimana seseorang tahu bahwa Tali Sepatu sudah selesai dengannya (dan bahwa Tali Sepatu belum meneruskannya ke kelas lain)?
GrahamS
Anda tidak hanya harus memindahkan Penciptaan tetapi juga 'tanggung jawab' untuk pegangan tangan. Jawaban jaredPar memiliki awal yang menarik dari sebuah ide.
Henk Holterman
Blog ini membahas penerapan IDisposable, dan secara khusus membahas cara menyederhanakan tugas dengan menghindari pencampuran sumber daya yang dikelola dan tidak dikelola dalam satu kelas blog.stephencleary.com/2009/08/…
PaulS
3

Menariknya jika Driverdiartikan seperti di atas:

class Driver
{
    Shoe[] shoes = { new Shoe(), new Shoe() };
}

Kemudian saat Shoedibuat IDisposable, FxCop (v1.36) tidak komplain yang Driverseharusnya juga bisa IDisposable.

Namun jika diartikan seperti ini:

class Driver
{
    Shoe leftShoe = new Shoe();
    Shoe rightShoe = new Shoe();
}

maka ia akan mengeluh.

Saya curiga bahwa ini hanya batasan FxCop, bukan solusi, karena pada versi pertama, Shoeinstance masih dibuat oleh Driverdan masih perlu dibuang.

GrahamS
sumber
2
Jika Show mengimplementasikan IDisposable, membuatnya di penginisialisasi bidang berbahaya. Menarik bahwa FXCop tidak mengizinkan inisialisasi seperti itu, karena di C # satu-satunya cara untuk melakukannya dengan aman adalah dengan variabel ThreadStatic (benar-benar mengerikan). Di vb.net, dimungkinkan untuk menginisialisasi objek IDisposable dengan aman tanpa variabel ThreadStatic. Kodenya masih jelek, tapi tidak terlalu mengerikan. Saya berharap MS akan memberikan cara yang baik untuk menggunakan penginisialisasi bidang tersebut dengan aman.
supercat
1
@supercat: terima kasih. Bisakah Anda menjelaskan mengapa itu berbahaya? Saya menduga bahwa jika pengecualian dilemparkan di salah satu Shoekonstruktor maka shoesakan dibiarkan dalam keadaan yang sulit?
GrahamS
3
Kecuali seseorang mengetahui bahwa jenis IDisposable tertentu dapat ditinggalkan dengan aman, seseorang harus memastikan bahwa setiap objek yang dibuat akan dibuang. Jika IDisposable dibuat di penginisialisasi bidang objek tetapi pengecualian dilemparkan sebelum objek sepenuhnya dibangun, objek dan semua bidangnya akan ditinggalkan. Meskipun konstruksi objek dibungkus dengan metode pabrik yang menggunakan blok "coba" untuk mendeteksi bahwa konstruksi gagal, pabrik sulit mendapatkan referensi ke objek yang perlu dibuang.
supercat
3
Satu-satunya cara yang saya tahu untuk mengatasinya di C # adalah dengan menggunakan variabel ThreadStatic untuk menyimpan daftar objek yang perlu dibuang jika konstruktor saat ini melempar. Lalu minta setiap penginisialisasi bidang mendaftarkan setiap objek saat dibuat, minta pabrik menghapus daftar jika berhasil diselesaikan, dan gunakan klausa "akhirnya" untuk Buang semua item dari daftar jika tidak dihapus. Itu akan menjadi pendekatan yang bisa diterapkan, tetapi variabel ThreadStatic jelek. Di vb.net, seseorang dapat menggunakan teknik serupa tanpa memerlukan variabel ThreadStatic.
supercat
3

Saya tidak berpikir ada cara teknis untuk mencegah ID sekali pakai menyebar jika Anda menjaga desain Anda begitu erat. Orang kemudian harus bertanya-tanya apakah desainnya benar.

Dalam contoh Anda, menurut saya masuk akal untuk memiliki sepatu yang memiliki tali sepatu, dan mungkin, pengemudi harus memiliki sepatunya sendiri. Namun, bus seharusnya tidak memiliki pengemudi. Biasanya, pengemudi bus tidak mengikuti bus ke tempat pembuangan sampah :) Dalam kasus pengemudi dan sepatu, pengemudi jarang membuat sepatu sendiri, artinya mereka tidak benar-benar "memilikinya".

Desain alternatif bisa berupa:

class Bus
{
   IDriver busDriver = null;
   public void SetDriver(IDriver d) { busDriver = d; }
}

class Driver : IDriver
{
   IShoePair shoes = null;
   public void PutShoesOn(IShoePair p) { shoes = p; }
}

class ShoePairWithDisposableLaces : IShoePair, IDisposable
{
   Shoelace lace = new Shoelace();
}

class Shoelace : IDisposable
{
   ...
}

Sayangnya, desain baru ini lebih rumit, karena memerlukan kelas tambahan untuk membuat dan membuang contoh konkret sepatu dan driver, tetapi kerumitan ini melekat pada masalah yang sedang dipecahkan. Hal baiknya adalah bus tidak perlu lagi dibuang hanya untuk tujuan membuang tali sepatu.

Joh
sumber
2
Ini tidak benar-benar menyelesaikan masalah aslinya. Ini mungkin membuat FxCop diam tetapi masih ada sesuatu yang harus membuang Tali Sepatu.
AndrewS
Saya pikir semua yang Anda lakukan adalah memindahkan masalah ke tempat lain. ShoePairWithDisposableLaces sekarang memiliki Shoelace (), jadi itu juga harus dibuat IDisposable - jadi siapa yang membuang sepatu? Apakah Anda menyerahkan ini ke beberapa wadah IoC untuk ditangani?
GrahamS
@AndrewS: Memang, pengemudi, sepatu, dan tali sepatu masih harus dibuang, tetapi pembuangannya tidak boleh dimulai dengan bus. @GrahamS: Anda benar, saya memindahkan masalah ke tempat lain, karena saya yakin masalahnya ada di tempat lain. Biasanya, akan ada kelas yang memberi contoh sepatu, yang juga bertanggung jawab untuk membuang sepatu. Saya telah mengedit kode untuk membuat ShoePairWithDisposableLaces sekali pakai juga.
Joh
@Joh: Terima kasih. Seperti yang Anda katakan, masalah dengan memindahkannya ke tempat lain adalah Anda menciptakan kerumitan yang jauh lebih banyak. Jika ShoePairWithDisposableLaces dibuat oleh kelas lain, bukankah kelas itu harus diberi tahu ketika Pengemudi telah selesai dengan Sepatunya, sehingga dapat dengan benar Dispose ()?
GrahamS
1
@Graham: ya, diperlukan semacam mekanisme pemberitahuan. Kebijakan pembuangan berdasarkan jumlah referensi dapat digunakan, misalnya. Ada beberapa kerumitan tambahan, tetapi seperti yang mungkin Anda ketahui, "Tidak ada yang namanya sepatu gratis" :)
Joh
1

Bagaimana kalau menggunakan Inversion of Control?

class Bus
{
    private Driver busDriver;

    public Bus(Driver busDriver)
    {
        this.busDriver = busDriver;
    }
}

class Driver
{
    private Shoe[] shoes;

    public Driver(Shoe[] shoes)
    {
        this.shoes = shoes;
    }
}

class Shoe
{
    private Shoelace lace;

    public Shoe(Shoelace lace)
    {
        this.lace = lace;
    }
}

class Shoelace
{
    bool tied;
    private AutoResetEvent waitHandle;

    public Shoelace(bool tied, AutoResetEvent waitHandle)
    {
        this.tied = tied;
        this.waitHandle = waitHandle;
    }
}

class Program
{
    static void Main(string[] args)
    {
        using (var leftShoeWaitHandle = new AutoResetEvent(false))
        using (var rightShoeWaitHandle = new AutoResetEvent(false))
        {
            var bus = new Bus(new Driver(new[] {new Shoe(new Shoelace(false, leftShoeWaitHandle)),new Shoe(new Shoelace(false, rightShoeWaitHandle))}));
        }
    }
}
Darragh
sumber
Sekarang Anda telah membuatnya menjadi masalah penelepon, dan Shoelace tidak dapat bergantung pada pegangan tunggu yang tersedia saat dibutuhkan.
Andy
@andy Apa yang Anda maksud dengan "Tali sepatu tidak dapat bergantung pada pegangan tunggu yang tersedia saat dibutuhkan"? Ini diteruskan ke konstruktor.
Darragh
Sesuatu yang lain mungkin telah mengacaukan status autoresetevents sebelum memberikannya ke Shoelace, dan itu mungkin dimulai dengan ARE dalam kondisi buruk; sesuatu yang lain mungkin akan merusak status ARE saat Tali Sepatu melakukan tugasnya, menyebabkan Tali Sepatu melakukan hal yang salah. Itu alasan yang sama Anda hanya mengunci anggota pribadi. "Secara umum, .. hindari penguncian pada instance di luar kendali kode Anda" docs.microsoft.com/en-us/dotnet/csharp/language-reference/…
Andy
Saya setuju dengan hanya mengunci anggota pribadi. Tapi sepertinya pegangan tunggu berbeda. Faktanya, tampaknya lebih berguna untuk meneruskannya ke objek karena instance yang sama perlu digunakan untuk berkomunikasi lintas utas. Meskipun demikian, menurut saya IoC memberikan solusi yang berguna untuk masalah OP.
Darragh
Mengingat contoh OP memiliki waithandle sebagai anggota pribadi, asumsi amannya adalah bahwa objek mengharapkan akses eksklusif ke sana. Membuat pegangan terlihat di luar contoh melanggar itu. Biasanya IoC bisa bagus untuk hal-hal seperti ini, tetapi tidak untuk penguliran.
Andy