IDisposable'ın tüm sınıflarınıza yayılmasını nasıl önlersiniz?


138

Bu basit sınıflarla başlayın ...

Diyelim ki böyle basit bir sınıf setim var:

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 Bus, bir Driver, Driveriki Shoes, her Shoebiri bir Shoelace. Hepsi çok aptalca.

Ayakkabı Bağına IDisposable nesnesi ekleme

Daha sonra, üzerinde bazı işlemlerin Shoelaceçok iş parçacıklı olabileceğine karar verdim , bu yüzden EventWaitHandleiletişim kurmak için iş parçacığı için bir ekledim. Yani Shoelaceşimdi şöyle görünür:

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

Ayakkabı Bağı Üzerinde Kullanılabilir ID

Ancak şimdi Microsoft'un FxCop şikayet edecek: "Aşağıdaki IDisposable türlerin üyelerini oluşturduğu için 'Shoelace' üzerinde IDisposable uygulayın: 'EventWaitHandle'."

Tamam, uygulamak IDisposableüzerinde Shoelaceve benim temiz küçük sınıf bu korkunç karmaşa haline gelir:

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;
    }
}

Veya (yorumcular tarafından belirtildiği gibi) Shoelacekendisi yönetilmeyen kaynak olmadığından Dispose(bool)ve Destructor'a ihtiyaç duymadan daha basit atma uygulamasını kullanabilirim :

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);
    }
}

Dezavantajlı yayılırken dehşet içinde izleyin

Doğru bu sabit. Ama şimdi FxCop Shoeyaratan bir şikayet edecek Shoelace, bu yüzden de Shoeolmalı IDisposable.

Ve Driveroluşturur Shoeböylece Driverolmalıdır IDisposable. Ve Busoluşturur Driverböylece Busolmalı IDisposablevb vb.

Birdenbire yaptığım küçük değişiklik Shoelacebana çok fazla iş çıkarıyor ve patronum neden Busdeğişiklik yapmak için ödeme yapmam gerektiğini merak ediyor Shoelace.

Soru

Bu yayılmayı nasıl önlersiniz IDisposable, ancak yine de yönetilmeyen nesnelerinizin uygun şekilde atılmasını sağlarsınız?


9
İstisnai olarak iyi bir soru, cevabın kullanımlarını en aza indirdiğine ve yüksek seviyeli IDisposable'ları kısa süreli kullanımda tutmaya çalıştığına inanıyorum, ancak bu her zaman mümkün değil (özellikle bu IDisposable'ların C ++ dll'leri veya benzeri ile birlikte çalışması nedeniyle). Cevaplara bakın.
annakata

Tamam Dan, her iki ayakkabı bağı üzerinde de uygulanamaz yöntemlerini göstermek için soruyu güncelledim.
GrahamS

Genellikle beni korumak için diğer sınıfların uygulama detaylarına güvenmeye dikkat ediyorum. Kolayca önleyebilirsem riske atmaya gerek yok. Belki aşırı ihtiyatlıyım veya belki de C programcısı olarak çok uzun zaman geçirdim, ama İrlandalı yaklaşımı tercih ederim: "Emin olmak için, emin olmak" :)
GrahamS

@Dan: Nesnenin kendisinin null olarak ayarlanmadığından emin olmak için null denetimi gereklidir; bu durumda waitHandle.Dispose () çağrısı bir NullReferenceException özel durumu oluşturur.
Scott Dorman

1
Ne olursa olsun, aslında (Ayakkabı Bağcında Kullanılamaz Kimliğini Uygula ”bölümünde gösterildiği gibi Dispose (bool) yöntemini kullanmalısınız (çünkü sonlandırıcı eksi) tam kalıptır. Bir sınıfın IDisposable'ı kullandığı için sonlandırıcıya ihtiyacı olduğu anlamına gelmez.
Scott Dorman

Yanıtlar:


36

IDisposable'ın yayılmasını gerçekten "engelleyemezsiniz". Bazı sınıfların atılması gerekir AutoResetEvent, ve en etkili yol, Dispose()finalizörlerin yükünü önlemek için bunu yöntemde yapmaktır . Ancak bu yöntemin bir şekilde çağrılması gerekir, bu yüzden tam olarak örneğinizde olduğu gibi IDisposable'ı içine alan veya içeren sınıflar bunları atmak zorundadır, bu yüzden de tek kullanımlık olmalılar, vb.

  • Mümkünse IDisposable sınıfları kullanmaktan kaçının, olayları tek bir yerde kilitleyin veya bekleyin, pahalı kaynakları tek bir yerde tutun vb
  • bunları yalnızca ihtiyacınız olduğunda oluşturun ve hemen ardından atın ( usingdesen)

Bazı durumlarda, isteğe bağlı bir durumu desteklediğinden IDisposable yok sayılabilir. Örneğin, WaitHandle, adlandırılmış bir Mutex'i desteklemek için IDisposable uygular. Bir ad kullanılmıyorsa, Dispose yöntemi hiçbir şey yapmaz. MemoryStream başka bir örnektir, sistem kaynağı kullanmaz ve Dispose uygulaması da hiçbir şey yapmaz. Yönetilmeyen bir kaynağın kullanılıp kullanılmadığı konusunda dikkatli düşünmek öğretici olabilir. Böylece .net kütüphaneleri için kullanılabilir kaynakları inceleyebilir veya bir kod çözücü kullanabilirsiniz.


4
Bugün uygulama hiçbir şey yapmadığından bunu göz ardı edebileceğiniz tavsiyesi kötüdür, çünkü gelecekteki bir sürüm Dispose'de gerçekten önemli bir şey yapabilir ve şimdi sızıntıyı izlemek zor.
Andy

1
"Pahalı kaynakları koru", IDisposable'ın mutlaka pahalı olması gerekmez, hatta eşit bekletme tutamacını kullanan bu örnek, aldığınız gibi "hafif" ile ilgilidir.
markmnl

20

Doğruluk açısından, bir üst nesnenin şimdi tek kullanımlık olması gereken bir alt nesne oluşturup sahip olması durumunda, IDisposable'ın bir nesne ilişkisi yoluyla yayılmasını önleyemezsiniz. FxCop bu durumda doğrudur ve üst öğe imkansız olmalıdır.

Yapabileceğiniz şey, nesne hiyerarşinizde bir yaprak sınıfına IDisposable eklenmesini önlemektir. Bu her zaman kolay bir iş değildir, ancak ilginç bir alıştırmadır. Mantıksal açıdan bakıldığında, bir ShoeLace'ın tek kullanımlık olması gerekmiyor. Buraya bir WaitHandle eklemek yerine, kullanıldığı noktada bir ShoeLace ve WaitHandle arasında bir ilişki eklemek de mümkündür. En basit yol Sözlük örneğidir.

WaitHandle'ı, WaitHandle'ın gerçekte kullanıldığı noktadaki bir harita aracılığıyla gevşek bir ilişkilendirmeye taşıyabilirseniz, bu zinciri kırabilirsiniz.


2
Orada bir ilişki kurmak çok garip geliyor. Bu AutoResetEvent, Shoelace uygulamasına özeldir, bu yüzden kamuoyuna açıklamak benim görüşüme göre yanlış olur.
GrahamS

@GrahamS, bunu kamuya açık olarak söylemiyorum. Diyorum ki ayakkabı bağcığı bağlanan noktaya taşınabilir. Belki de ayakkabı bağlarının bağlanması çok karmaşık bir işse, ayakkabı bağı katman sınıfı olmalıdır.
JaredPar

1
Cevabınızı bazı kod parçacıkları JaredPar ile genişletebilir misiniz? Biraz benim örnek germe olabilir ama ben Shoelace oluşturur ve waitHandle.WaitOne () sabırla bekleyen Tyer iş parçacığını başlatır düşünüyorum Ayakkabı bağı iplik başlatmak için beklemek zaman waitHandle.Set () çağırır.
Grahams

1
+1 bu. Bence sadece Shoelace'ın eşzamanlı olarak çağrılması garip olurdu, ama diğerleri değil. Toplam kökün ne olması gerektiğini düşünün. Bu durumda alan adı hakkında bilgim olmamasına rağmen Otobüs, IMHO olmalıdır. Yani, bu durumda, otobüs bekleme kolunu içermeli ve otobüse karşı tüm işlemler ve tüm çocukları senkronize edilecektir.
Johannes Gustafsson

16

Yayılmasını önlemek IDisposableiçin, tek bir yöntemin içindeki tek kullanımlık bir nesnenin kullanımını kapsüllemeye çalışmalısınız. ShoelaceFarklı tasarım yapmaya çalışın :

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();

    } 

  }
} 

Bekleme kolunun kapsamı Tie yöntemle ve sınıfın tek kullanımlık bir alana sahip olması gerekmez ve bu nedenle tek kullanımlık olması gerekmez.

Bekleme tutamacı, içinde bir uygulama ayrıntısı olduğundan Shoelace, bildirgesine yeni bir arabirim eklemek gibi genel arabirimini hiçbir şekilde değiştirmemelidir. Artık tek kullanımlık bir alana ihtiyacınız olmadığında ne olacak, IDisposablebeyanı kaldıracak mısınız? Shoelace Soyutlamayı düşünürseniz , bunun gibi altyapı bağımlılıkları tarafından kirletilmemesi gerektiğini çabucak fark edersiniz IDisposable. IDisposablesoyutlaması deterministik temizlik gerektiren bir kaynağı içine alan sınıflar için ayrılmalıdır; yani, atılabilirliğin soyutlamanın bir parçası olduğu sınıflar için .


1
Shoelace'ın uygulama detayının kamu arayüzünü kirletmemesi gerektiğine tamamen katılıyorum, ancak benim açımdan kaçınmanın zor olabileceği. Burada önerdiğiniz şey her zaman mümkün olmaz: AutoResetEvent () yöntemi, iş parçacıkları arasında iletişim kurmaktır, bu nedenle kapsamı genellikle tek bir yöntemin ötesine uzanır.
GrahamS

@GrahamS: Bu yüzden onu bu şekilde tasarlamayı denedim. Tek kullanımlık malzemeyi diğer yöntemlere de geçirebilirsiniz. Sadece sınıfa yapılan harici çağrılar, tek kullanımlık ürünün yaşam döngüsünü kontrol ettiğinde bozulur. Cevabı buna göre güncelleyeceğim.
Jordão

2
Üzgünüm, kullanılıp atılabileceğinizi biliyorum, ama hala işe yaradığını görmüyorum. Benim örnekte AutoResetEventbir üye değişkeni olmak zorunda böylece, aynı sınıfta içinde faaliyet farklı iş parçacıkları arasında iletişim kurmak için kullanılır. Kapsamını bir yöntemle sınırlayamazsınız. (örneğin, bir iş parçacığının sadece engelleme yoluyla bazı işler için beklediğini düşünün waitHandle.WaitOne(). Ana iş parçacığı daha sonra shoelace.Tie()sadece a waitHandle.Set()ve hemen dönen yöntemi çağırır ).
GrahamS

3

Temel olarak Kompozisyon veya Toplama'yı Tek Kullanımlık sınıflarla karıştırdığınızda olan şey budur. Belirtildiği gibi, ilk çıkış yolu waitHandle'ı ayakkabı bağı dışına çıkarmaktır.

Bunu söyledikten sonra, yönetilmeyen kaynaklarınız olmadığında Tek Kullanımlık kalıbı önemli ölçüde azaltabilirsiniz. (Hala bunun için resmi bir referans arıyorum.)

Ancak yıkıcıyı ve GC'yi atlayabilirsiniz.SuppressFinalize (this); ve belki de sanal boşluğu biraz temizleyin.


Teşekkürler Henk. WaitHandle'ın yaratımını Shoelace'dan çıkarırsam, o zaman birisi hala bir yere atmak zorunda kalır, bu yüzden birisi Shoelace'ın onunla bittiğini nasıl bilebilir (ve Shoelace bunu başka bir sınıfa geçirmediğini)?
GrahamS

Sadece Yaratılışın değil, aynı zamanda bekleme kolunun 'sorumluluğunu' taşımalısınız. jaredPar'ın cevabı ilginç bir fikre sahiptir.
Henk Holterman

Bu blog, IDisposable'ın uygulanmasını kapsar ve özellikle yönetilen ve yönetilmeyen kaynakları tek bir sınıfta karıştırmaktan kaçınarak görevin nasıl basitleştirileceğini ele alır blog.stephencleary.com/2009/08/…
PaulS

3

İlginçtir Driver, yukarıdaki gibi tanımlanırsa:

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

Daha sonra Shoeyapıldığında IDisposable, FxCop (v1.36) olması Drivergerektiği gibi şikayet etmiyor IDisposable.

Ancak bu şekilde tanımlanırsa:

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

o zaman şikayet edecek.

Bunun bir çözümden ziyade FxCop'un sadece bir sınırlaması olduğundan şüpheleniyorum, çünkü ilk versiyonda Shoeörnekler hala yaratılıyor Driverve hala bir şekilde bertaraf edilmesi gerekiyor.


2
Show IDisposable uygularsa, bunu bir alan başlatıcıda oluşturmak tehlikelidir. C # 'da güvenli bir şekilde yapmanın tek yolu ThreadStatic değişkenleri (düpedüz hideous) olduğundan FXCop'un bu tür başlatımlara izin vermemesi ilginçtir. Vb.net'te, IDSposable nesneleri ThreadStatic değişkenleri olmadan güvenli bir şekilde başlatmak mümkündür. Kod hala çirkin, ama o kadar da iğrenç değil. Keşke MS, bu tür saha başlatıcılarını güvenli bir şekilde kullanmak için güzel bir yol sağlar.
supercat

1
@supercat: teşekkürler. Neden tehlikeli olduğunu açıklayabilir misiniz ? Yapıcılardan birinde bir istisna atılırsa, Shoeo shoeszaman zor bir durumda bırakılacağını tahmin ediyorum.
GrahamS

3
Kişi belirli bir tip Tek Kullanımlık'ın güvenli bir şekilde terk edilebileceğini bilmedikçe, oluşturulan her nesnenin Atılmasını sağlamalıdır. Bir nesnenin alan başlatıcısında bir IDisposable oluşturulursa ancak nesne tamamen oluşturulmadan önce bir istisna atılırsa, nesne ve tüm alanları terk edilir. Nesnenin inşası, yapının başarısız olduğunu tespit etmek için "dene" bloğunu kullanan bir fabrika yöntemine sarılmış olsa bile, fabrikanın atılması gereken nesnelere referans alması zordur.
supercat

3
Ben C # ile başa çıkmak için bilmek tek yolu geçerli yapıcı atarsa ​​bertaraf edilmesi gereken nesnelerin listesini tutmak için bir ThreadStatic değişken kullanmak olacaktır. Daha sonra her alan başlatıcısının oluşturuldukça her nesneyi kaydettirmesini sağlayın, başarıyla tamamlanırsa fabrikanın listeyi temizlemesini sağlayın ve temizlenmediyse tüm öğeleri listeden atmak için bir "son" maddesi kullanın. Bu uygulanabilir bir yaklaşım olabilir, ancak ThreadStatic değişkenleri çirkin. Vb.net'te, herhangi bir ThreadStatic değişkenine ihtiyaç duymadan benzer bir teknik kullanılabilir.
supercat

3

Tasarımınızı çok sıkı bir şekilde tutarsanız, IDisposable'ın yayılmasını önlemenin teknik bir yolu olduğunu düşünmüyorum. O zaman tasarımın doğru olup olmadığını merak etmeliyiz.

Örneğinizde, sanırım ayakkabının ayakkabı bağına sahip olması mantıklıdır ve belki de sürücünün ayakkabılarına sahip olması gerekir. Ancak, otobüs sürücüye sahip olmamalıdır. Tipik olarak, otobüs sürücüleri hurdalığa giden otobüsleri izlemez :) Sürücüler ve ayakkabılar söz konusu olduğunda, sürücüler nadiren kendi ayakkabılarını yaparlar, yani gerçekten onlara "sahip olmazlar".

Alternatif bir tasarım şunlar olabilir:

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
{
   ...
}

Yeni tasarım maalesef daha karmaşıktır, çünkü ayakkabı ve sürücülerin somut örneklerini somutlaştırmak ve atmak için ekstra sınıflar gerektirir, ancak bu komplikasyon çözülmekte olan sorunun doğasında vardır. İyi olan şey, otobüslerin artık ayakkabı bağcığının imha edilmesi için artık tek kullanımlık olması gerekmemesidir.


2
Bu orijinal sorunu gerçekten çözmez. FxCop'u sessiz tutabilir, ancak hala bir şey Ayakkabı Bağı'nı atmalıdır.
AndrewS

Bence tüm yaptığın problemi başka bir yere taşımak. ShoePairWithDisposableLaces artık Shoelace () 'nin sahibidir, bu yüzden de IDisposable yapılabilir hale getirilmelidir - peki ayakkabıları kim attı? Bunu idare etmek için bazı IoC konteynerine mi bırakıyorsunuz?
GrahamS

@AndrewS: Gerçekten de, bir şeyler hala sürücüleri, ayakkabıları ve dantelleri atmalıdır, ancak imha otobüslerle başlatılmamalıdır. @GrahamS: Haklısın, sorunu başka bir yere taşıyorum çünkü başka bir yere ait olduğuna inanıyorum. Tipik olarak, ayakkabıların atılmasından da sorumlu olacak bir sınıf örnekleme ayakkabısı olacaktır. ShoePairWithDisposableLaces de tek kullanımlık yapmak için kodu düzenledim.
Joh

@Joh: Teşekkürler. Söylediğiniz gibi, başka bir yere taşımakla ilgili sorun, çok daha karmaşıklık yaratmanızdır. ShoePairWithDisposableLaces başka bir sınıf tarafından oluşturulmuşsa, o zaman Sürücü onun Ayakkabıları ile işini bitirdiğinde o sınıfın bilgilendirilmesi gerekmez, böylece onları uygun şekilde Dispose () yapabilir mi?
GrahamS

1
@Graham: evet, bir tür bildirim mekanizmasına ihtiyaç duyulacaktır. Örneğin, referans sayılarına dayanan bir bertaraf politikası kullanılabilir. Biraz daha karmaşıklık var, ama muhtemelen bildiğiniz gibi, "Ücretsiz ayakkabı diye bir şey yok" :)
Joh

1

Konversiyon Kontrol kullanmaya ne dersiniz?

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))}));
        }
    }
}

Şimdi bunu arayanlar sorunu haline getirdiniz ve Ayakkabı Bağı, ihtiyaç duyulduğunda bekleme kolunun kullanılabilir olmasına bağlı olamaz.
Andy

@andy "Shoelace ihtiyaç duyulduğunda bekleme kolunun mevcut olmasına bağlı olamaz" ile ne demek istiyorsun? Yapıcıya geçti.
Darragh

Ayakkabı bağına vermeden önce başka bir şey otomatik yeniden başlatma durumuyla uğraşmış olabilir ve kötü durumda ARE ile başlayabilir; Shoelace işini yaparken başka bir şey ARE'nin durumuyla boğuşabilir ve Shoelace'ın yanlış şeyi yapmasına neden olabilir. Sadece özel üyelere kilitlenmenizin nedeni de budur. "Genel olarak, .. kod kontrolünüz dışındaki örneklere kilitlenmekten kaçının" docs.microsoft.com/en-us/dotnet/csharp/language-reference/…
Andy

Sadece özel üyelere kilitlenmeyi kabul ediyorum. Ama bekleme kolları farklı gibi görünüyor. Aslında, iş parçacıkları arasında iletişim kurmak için aynı örneğin kullanılması gerektiğinden, bunları nesneye aktarmak daha yararlı görünmektedir. Yine de IoC'nin OP sorununa faydalı bir çözüm sunduğunu düşünüyorum.
Darragh

OP örneğinin özel üye olarak bekleme koluna sahip olduğu göz önüne alındığında, güvenli varsayım, nesnenin kendisine özel erişim beklediğidir. Tanıtıcıyı örnek dışında görünür yapmak bunu ihlal eder. Tipik olarak bir IoC, bu gibi şeyler için iyi olabilir, ancak diş çekme söz konusu olduğunda değil.
Andy
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.