Zacznij od tych prostych zajęć ...
Powiedzmy, że mam prosty zestaw klas, taki jak ten:
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
ma Driver
, Driver
ma dwa Shoe
s, każdy Shoe
ma Shoelace
. Bardzo głupie.
Dodaj obiekt IDisposable do Shoelace
Później zdecydowałem, że niektóre operacje na serwerze Shoelace
mogą być wielowątkowe, więc dodaję EventWaitHandle
znak, aby wątki się komunikowały. Więc Shoelace
teraz wygląda to tak:
class Shoelace
{
private AutoResetEvent waitHandle = new AutoResetEvent(false);
bool tied = false;
// ... other stuff ..
}
Zaimplementuj IDisposable na Shoelace
Ale teraz FxCop Microsoftu będzie narzekał: „Zaimplementuj IDisposable na 'Shoelace', ponieważ tworzy elementy członkowskie następujących typów IDisposable: 'EventWaitHandle'."
Dobra, wdrażam IDisposable
dalej Shoelace
i moja zgrabna mała klasa staje się strasznym bałaganem:
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;
}
}
Lub (jak podkreślają komentatorzy), ponieważ Shoelace
sam nie ma niezarządzanych zasobów, mogę użyć prostszej implementacji usuwania bez potrzeby Dispose(bool)
i 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);
}
}
Oglądaj z przerażeniem, jak rozprzestrzenia się IDisposable
Tak, to naprawione. Ale teraz FxCop będzie narzekać, że Shoe
tworzy plik Shoelace
, więc też Shoe
musi być IDisposable
.
I Driver
tworzy Shoe
tak Driver
musi być IDisposable
. I Bus
tworzy Driver
tak Bus
musi być IDisposable
i tak dalej.
Nagle moja mała zmiana w aplikacji Shoelace
powoduje dużo pracy, a mój szef zastanawia się, dlaczego muszę się wymeldować, Bus
aby dokonać zmiany Shoelace
.
Pytanie
Jak zapobiec temu rozprzestrzenianiu się IDisposable
, ale jednocześnie zapewnić, że niezarządzane obiekty są prawidłowo usuwane?
źródło
Odpowiedzi:
Tak naprawdę nie można „zapobiec” rozprzestrzenianiu się IDisposable. Niektóre klasy muszą zostać usunięte,
AutoResetEvent
a najbardziej efektywnym sposobem jest zrobienie tego wDispose()
metodzie, aby uniknąć narzutu finalizatorów. Ale ta metoda musi być jakoś wywołana, więc dokładnie tak, jak w twoim przykładzie klasy, które zawierają lub zawierają IDisposable, muszą je usunąć, więc muszą być również jednorazowe itp. Jedynym sposobem uniknięcia tego jest:using
wzorze)W niektórych przypadkach IDisposable można zignorować, ponieważ obsługuje opcjonalną wielkość liter. Na przykład WaitHandle implementuje IDisposable do obsługi nazwanego Mutex. Jeśli nazwa nie jest używana, metoda Dispose nic nie robi. MemoryStream to kolejny przykład, który nie używa żadnych zasobów systemowych, a jego implementacja Dispose również nic nie robi. Dokładne przemyślenie, czy niezarządzany zasób jest używany, czy nie, może być pouczające. Podobnie można sprawdzić dostępne źródła bibliotek .net lub użyć dekompilatora.
źródło
Jeśli chodzi o poprawność, nie można zapobiec rozprzestrzenianiu się IDisposable poprzez relację między obiektami, jeśli obiekt nadrzędny tworzy i zasadniczo jest właścicielem obiektu podrzędnego, który teraz musi być jednorazowy. FxCop jest poprawny w tej sytuacji, a rodzic musi być IDisposable.
Możesz uniknąć dodawania IDisposable do klasy liścia w hierarchii obiektów. Nie zawsze jest to łatwe zadanie, ale jest to ciekawe ćwiczenie. Z logicznego punktu widzenia nie ma powodu, dla którego sznurowadło musi być jednorazowe. Zamiast dodawać tutaj WaitHandle, można również dodać skojarzenie między ShoeLace i WaitHandle w miejscu, w którym jest używany. Najprostszym sposobem jest użycie instancji Dictionary.
Jeśli możesz przenieść WaitHandle do luźnego powiązania za pośrednictwem mapy w miejscu, w którym WaitHandle jest faktycznie używany, możesz przerwać ten łańcuch.
źródło
Aby zapobiec
IDisposable
rozprzestrzenianiu się, powinieneś spróbować hermetyzować użycie jednorazowego obiektu wewnątrz jednej metody. Spróbuj zaprojektowaćShoelace
inaczej:Zakres uchwytu oczekiwania jest ograniczony do
Tie
metody, a klasa nie musi mieć pola jednorazowego, a więc sama nie musi być jednorazowa.Ponieważ uchwyt oczekiwania jest szczegółem implementacji wewnątrz elementu
Shoelace
, nie powinien on w żaden sposób zmieniać swojego publicznego interfejsu, na przykład poprzez dodanie nowego interfejsu w swojej deklaracji. Co się wtedy stanie, gdy nie będziesz już potrzebować pola jednorazowego, czy usunieszIDisposable
deklarację? Jeśli pomyślisz oShoelace
abstrakcji , szybko zdasz sobie sprawę, że nie powinna być zanieczyszczona zależnościami infrastruktury, takimi jakIDisposable
.IDisposable
powinny być zarezerwowane dla klas, których abstrakcja obejmuje zasób wymagający deterministycznego czyszczenia; tj. dla klas, w których rozporządzalność jest częścią abstrakcji .źródło
AutoResetEvent
jest używany do komunikacji między różnymi wątkami, które działają w tej samej klasie, więc musi być zmienną składową. Nie możesz ograniczyć jego zakresu do metody. (np. wyobraź sobie, że jeden wątek po prostu czeka na pracę, blokując sięwaitHandle.WaitOne()
. Następnie główny wątek wywołujeshoelace.Tie()
metodę, która po prostu wykonuje awaitHandle.Set()
i natychmiast zwraca).Zasadniczo dzieje się tak, gdy mieszasz Kompozycję lub Agregację z klasami jednorazowymi. Jak wspomniano, pierwszym wyjściem byłoby zrefaktoryzowanie waitHandle z sznurówki.
Powiedziawszy to, możesz znacznie zmniejszyć wzorzec Disposable, gdy nie masz niezarządzanych zasobów. (Nadal szukam oficjalnego odniesienia do tego.)
Ale możesz pominąć destruktor i GC.SuppressFinalize (this); i może trochę wyczyścić wirtualną pustkę Dispose (bool dispose).
źródło
Co ciekawe, jeśli
Driver
zdefiniowano jak powyżej:Wtedy, kiedy
Shoe
jest zrobionyIDisposable
, FxCop (v1.36) nie narzeka, że takDriver
powinno byćIDisposable
.Jednak jeśli jest zdefiniowane w ten sposób:
wtedy będzie narzekać.
Podejrzewam, że jest to tylko ograniczenie FxCop, a nie rozwiązanie, ponieważ w pierwszej wersji
Shoe
instancje są nadal tworzone przezDriver
i nadal trzeba je jakoś pozbyć.źródło
Shoe
konstruktorów, toshoes
zostałby w trudnym stanie?Nie sądzę, aby istniał techniczny sposób zapobiegania rozprzestrzenianiu się IDisposable, jeśli projekt będzie tak ściśle powiązany. Należy się więc zastanowić, czy projekt jest właściwy.
W twoim przykładzie myślę, że sensowne jest, aby but miał sznurowadło, a może kierowca powinien posiadać swoje buty. Jednak autobus nie powinien być właścicielem kierowcy. Zazwyczaj kierowcy autobusów nie podążają za autobusami na złomowisko :) W przypadku kierowców i butów kierowcy rzadko robią własne buty, czyli tak naprawdę ich nie „posiadają”.
Alternatywnym projektem może być:
Nowy projekt jest niestety bardziej skomplikowany, ponieważ wymaga dodatkowych klas w celu utworzenia instancji i usunięcia konkretnych instancji butów i sterowników, ale ta komplikacja jest nieodłącznie związana z rozwiązywaniem problemu. Dobrą rzeczą jest to, że autobusy nie muszą już być jednorazowego użytku tylko w celu pozbycia się sznurowadeł.
źródło
A co powiesz na użycie Inversion of Control?
źródło