2012-05-09 16 views
29

Począwszy od follwing sytuacji:Zadeklarować IDisposable dla klasy lub interfejsu?

public interface ISample 
{ 
} 

public class SampleA : ISample 
{ 
    // has some (unmanaged) resources that needs to be disposed 
} 

public class SampleB : ISample 
{ 
    // has no resources that needs to be disposed 
} 

Klasa SampleA powinny wdrożyć IDisposable interfejsu do zwalniania zasobów. Można to rozwiązać na dwa sposoby:

1. Dodać wymaganą interfejs klasy SampleA:

public class SampleA : ISample, IDisposable 
{ 
    // has some (unmanaged) resources that needs to be disposed 
} 

2. dodać go do ISample interfejsu i siła wynikająca zajęcia do jego realizacji:

public interface ISample : IDisposable 
{ 
} 

Jeśli wstawisz go do interfejsu wymusić realizację dowolnego celu realizacji IDisposable, nawet jeśli nie mają nic do dysponowania. Z drugiej strony bardzo wyraźnie widać, że konkretna implementacja interfejsu wymaga bloku utylizacji/używania i nie trzeba go używać jako odczytywalnego do czyszczenia. Może być trochę plusów/minusów w obie strony ... dlaczego sugerowałbyś, aby preferować jedną preferowaną metodę?

+4

Jakie jest prawdopodobieństwo, że kod napisany na interfejsie (przypuszczalnie zostanie przekazany już skonstruowaną instancją) jest odpowiedzialny za * zakończenie * (użytecznego) okresu istnienia tej instancji? –

+1

@Damien_The_Unbeliever: Ok, zakładając, że ISample pochodzi z wyniku metody fabryki lub poprzez Dependency Injection. – Beachwalker

+2

To jest rzecz - jeśli * pochodzi * z fabryki, prawdopodobnie twój kod * jest * odpowiedzialny za usunięcie - więc umieściłbym go w interfejsie. Ale jeśli jest on wstrzykiwany, to zakładam, że wtryskiwacz jest odpowiedzialny za całe życie, więc * nie pasowałoby do interfejsu - nie sądzę, że istnieje odpowiednik uniwersalny dla tego pytania. –

Odpowiedz

6

Jeśli stosuje się wzór using(){} do wszystkich interfejsów najlepiej mieć ISample pochodzą z IDisposable ponieważ zasada przy projektowaniu interfejsów ma sprzyjać „łatwość użytkowania” nad „łatwość wdrożenia ".

+0

jak użyłbyś instrukcji using w foreach? –

+1

"... a zobaczysz interfejs ..." To jest klucz. Wiele dobrych rozwiązań OOP będzie używać tylko interfejsu, więc myślę, że sensowny jest interfejs do implementacji IDisposable. W ten sposób zużywający się kod może traktować wszystkie podklasy w ten sam sposób. –

6

Osobiście, jeśli wszystkie ISample powinny być jednorazowe, umieściłbym je na interfejsie, jeśli tylko niektóre z nich umieściłem tylko na zajęciach, gdzie powinno być.

Wygląda na to, że masz drugi przypadek.

+0

Ale jak zapewnić wywołanie Dispose(), jeśli użytkownik twojej lib nie wie o tym i może być wymagana implementacja wymagana do usunięcia? – Beachwalker

+0

@Stegi - łatwe. Po prostu przetestuj 'is IDisposable' i wywołaj' Dispose' jeśli tak. – Jamiec

+0

Tak, wiem, ale to by śmieciło kod ... ponieważ KAŻDA inna implementacja (np. IList, I ...) może być IDisposable. Wydaje mi się, że IDisposable powinna być ogólną implementacją klasy bazowej obiektu (nawet jeśli jest pusta). – Beachwalker

0

Osobiście wybrałbym 1, chyba że podasz konkretny przykład dla dwóch osób. Dobrym przykładem dwóch jest IList.

An IList oznacza, że ​​musisz zaimplementować indeksator swojej kolekcji. Jednak IList oznacza również, że jesteś IEnumerable i powinieneś mieć GetEnumerator() dla swojej klasy.

W twoim przypadku masz wahaliśmy że klasy, które implementują ISample musiałby realizować IDisposable, jeśli nie każda klasa, która implementuje interfejs musi wdrożyć IDisposable to nie ich zmusić.

Koncentrując się na IDispoable w szczególności, IDispoable w szczególności zmusza programistów używających twojej klasy do napisania jakiegoś rozsądnie brzydkiego kodu. Na przykład,

foreach(item in IEnumerable<ISample> items) 
{ 
    try 
    { 
     // Do stuff with item 
    } 
    finally 
    { 
     IDisposable amIDisposable = item as IDisposable; 
     if(amIDisposable != null) 
      amIDisposable.Dispose(); 
    } 
} 

Nie tylko jest kod straszne, nie będzie znacząca kara wydajność w zapewnieniu jest wreszcie blokować do dysponowania rzeczą dla każdej iteracji tej liście, nawet jeśli Dispose() tylko powraca w realizacja.

Wkleiłem kod, aby odpowiedzieć na jeden z komentarzy tutaj, łatwiejszy do odczytania.

+0

Ale co, jeśli istnieją fabryki (lub proste użycie kontenera IoC), które dostarczają obiekty implementacji z wymaganiem ich usunięcia lub bez? Będziesz musiał zadzwonić do Dispose(), rzucając i nazywając to explicitely. W ten sposób twój kod potrzebuje wiedzy na temat (możliwej) implementacji ... jakiegoś rodzaju sprzężenia? W przeciwnym razie możesz po prostu użyć bloku, który jest bardzo łatwy. – Beachwalker

+0

@Stegi przy użyciu bloku może nie wyglądać brzydko, ale nadal będzie miał kary za wydajność. Również nie widzę, jak używałbyś bloku używając foreach na przykład.Kod Microsoftu jest opatrzony przykładami, w których to robi, IDisposable amIDisposable = object as IDisposable; if (amIDisposable! = null) amIDisposable.Dispose(); Ponieważ nie rzuca wyjątku, jeśli nie może go przesłać do IDisposable, kara za wykonanie nie jest prawie żadna. –

+0

Nawet jeśli tylko 1% klas, które zaimplementują lub odziedziczą po jakimś określonym typie, zrobi wszystko w 'IDisposable.Dispose', jeśli kiedykolwiek będzie konieczne wywołanie' IDisposable.Dispose' na zmiennej lub polu zadeklarowanym jako ten typ (w przeciwieństwie do typu implementacyjnego lub pochodnego), co jest dobrym znakiem, że sam typ powinien dziedziczyć lub implementować 'IDisposable'. Testowanie w czasie wykonywania, czy instancja obiektu implementuje 'IDisposable' i wywołuje' IDisposable.Dispose' na nim, jeśli tak (jak to było konieczne z nietypowym 'IEnumerable') jest głównym zapachem kodu. – supercat

2

IDispoable to bardzo często spotykany interfejs, nic więc dziwnego, że dziedziczy po nim interfejs. W ten sposób unikniesz sprawdzania typu w kodzie za jedyne koszty, aby w niektórych implementacjach ISample implementacja nie była wykonywana. Tak więc twój drugi wybór może być lepszy z tego punktu widzenia.

+0

To po prostu nieprawda. Jeśli twoja klasa ma pole, które implementuje IDisposable, to klasa musi być IDisposable, jak również wywołać Dispose of the field. Jeśli dodasz IDisposable, gdy nie jest to potrzebne, kończysz z tym, że połowa twoich klas może być IDisposable. –

3

interfejs IFoo prawdopodobnie powinien wdrożyć IDisposable jeżeli jest prawdopodobne, że przynajmniej niektóre niektóre implementacje wdroży IDisposable, a przynajmniej niektórych przypadkach ostatnia wzmianka życiu do instancji będą przechowywane w zmiennej lub pola typu IFoo. Powinno to prawie na pewno zaimplementować IDisposable, jeśli jakiekolwiek implementacje mogą zaimplementować IDisposable, a instancje zostaną utworzone za pośrednictwem interfejsu fabrycznego (tak jak w przypadku instancji IEnumerator<T>, które w wielu przypadkach są tworzone za pomocą interfejsu fabrycznego IEnumerable<T>).

Porównywanie IEnumerable<T> i IEnumerator<T> jest pouczające. Niektóre typy implementujące IEnumerable<T> również implementują IDisposable, ale kod, który tworzy wystąpienia takich typów, będzie wiedział, czym one są, wiedzą, że wymagają utylizacji i używają ich jako swoich konkretnych typów. Takie instancje mogą zostać przekazane do innych procedur jako typ IEnumerable<T>, a te inne procedury nie miałyby żadnej wskazówki, że obiekty ostatecznie będą wymagały utylizacji, ale te inne procedury w większości przypadków nie byłyby ostatnimi, które utrzymywałyby odniesienia do obiektów.. Z kolei instancje IEnumerator<T> są często tworzone, używane i ostatecznie odrzucane przez kod, który nie wie nic o podstawowych typach tych instancji poza faktem, że zostały zwrócone przez IEnumerable<T>. Niektóre implementacje powrotnych implementacji z IEnumerator<T> będą przeciekać zasoby, jeśli ich metoda IDisposable.Dispose nie zostanie wywołana, zanim zostaną one porzucone, a większość kodu, który akceptuje parametry typu IEnumerable<T>, nie będzie w stanie ustalić, czy takie typy mogą być do niego przekazywane. Chociaż byłoby możliwe, aby IEnumerable<T> zawrzeć właściwość EnumeratorTypeNeedsDisposal, aby wskazać, czy zwrócona IEnumerator<T> musiałaby zostać usunięta, lub po prostu wymagać, aby procedury, które wywołują GetEnumerator() sprawdzają typ zwróconego obiektu, aby zobaczyć, czy implementuje on IDisposable, jest to szybsze i łatwiejsze aby bezwarunkowo wywołać metodę Dispose, która może nie zrobić nic, niż określić, czy jest ona konieczna i zadzwonić do niej tylko wtedy, gdy tak jest.

+0

'IEnumerator ' jest dobrym przykładem interfejsu, w którym zarządzanie czasem życia jest zdecydowanie częścią jego API. Zrobiono to prawdopodobnie po to, aby rzeczy były prostsze. Jednak można by pomyśleć, że jeden z nich chciałby nazwać "GetEnumerator()" w jednej metodzie iw pewnym momencie przekazać moduł wyliczający do innej metody. Ta druga metoda nie miałaby idealnego dostępu do "Dispose()". Jeśli framework miał "interfejs IDispecableEnumerator : IEnumerator , IDisposable {}' i 'interface IEnumerator : IEnumerator {T Current {get; }} ', to może sprawić, że niektóre rzeczy będą" czystsze "(ale także bardziej złożone: - /). – binki

+1

@binki: Co sprawiłoby, że rzeczy byłyby czystsze, gdyby "IEnumerator" wdrożył 'IDisposable'. Kod, który otrzymuje "IEnumerable", o którym nic nie wie, musi zakładać, że może być konieczne wywołanie 'Dispose' na" IEnumerator ", który zwraca. Jeśli przekaże on zwrócony moduł wyliczający do jakiejś innej metody zużycia w późniejszym czasie, ta ostatnia metoda może być odpowiedzialna za wywołanie na niej polecenia "Dispose". Ponieważ wywoływanie "nie robić niczego" na czymś, co jest znane z implementacji 'IDisposable' jest szybsze niż sprawdzanie, czy obiekt implementuje' IDisposable' ... – supercat

+1

... najprostszy sposób wypełnienia zobowiązań przez obiekty to wywołanie "Wyrzuć" na posiadaczy rachunków, których są właścicielami, zanim je porzucą, nie zważając na to, czy te rachmistrzowie będą się tym przejmować. – supercat

16

Po Inteface Segregation Principle z SOLID jeśli dodać IDisposable do interfejsu dajesz metod do klientów, którzy nie są zainteresowani, więc należy go dodać do A.

Poza tym interfejs jest jednorazowy, ponieważ nigdy dyspozycyjność jest czymś związanym z konkretną implementacją interfejsu, nigdy z samym interfejsem.

Każdy interfejs może być potencjalnie zaimplementowany z elementami lub bez elementów, które należy usunąć.

1

Zaczynam myśleć, że umieszczenie IDisposable na interfejsie może spowodować pewne problemy. Oznacza to, że czas życia wszystkich obiektów implementujących ten interfejs może być bezpiecznie synchronicznie zakończony. To znaczy, że pozwala ktoś napisać kod w ten sposób i wymaga wszystkie implementacje wspierać IDisposable:

using (ISample myInstance = GetISampleInstance()) 
{ 
    myInstance.DoSomething(); 
} 

tylko kod, który uzyskuje dostęp do betonu typu może znać odpowiedni sposób kontrolować żywotność obiektu.Na przykład typ może nie wymagać usuwania, może obsługiwać IDisposable lub może wymagać pewnego asynchronicznego procesu czyszczenia po zakończeniu korzystania z niego (np. something like option 2 here).

Autor interfejsu nie jest w stanie przewidzieć wszystkich możliwych potrzeb związanych z wdrażaniem klas w przyszłym życiu/zakresie zarządzania. Celem interfejsu jest umożliwienie obiektowi wyeksponowania niektórych interfejsów API, aby mógł on być przydatny dla niektórych użytkowników. Niektóre interfejsy mogą być powiązane z zarządzaniem na całe życie (np. IDisposable), ale ich łączenie z interfejsami niezwiązanymi z zarządzaniem na całe życie może spowodować, że pisanie implementacji interfejsu będzie trudne lub niemożliwe. Jeśli masz bardzo mało implementacji interfejsu i struktury kodu, aby konsument interfejsu i menedżer życia/zakresu były w tej samej metodzie, to rozróżnienie to nie jest jasne na początku. Ale jeśli zaczniesz omijać swój obiekt, będzie to wyraźniejsze.

W powyższym przykładzie kodu pokazałem trzy typy scenariuszy zarządzania na całe życie, dodając do dwóch podanych przez Ciebie.

  1. SampleA jest IDisposable synchronicznego using() {} nośniku.
  2. SampleB używa czystego zbierania śmieci (nie zużywa żadnych zasobów).
  3. SampleC wykorzystuje zasoby, które uniemożliwiają ich synchroniczną likwidację i wymaga pod koniec swojego życia okresu await (aby mógł powiadomić kod zarządzania na całe życie, że wykonał on zużywanie zasobów i wywoływał efekt bąbelków w przypadku wszelkich asynchronicznie występujących wyjątków).

Utrzymując zarządzania dożywotnią oddzielić od innych interfejsów, można uniknąć błędów programistów (np przypadkowych połączeń do Dispose()) i bardziej czysto wspierać przyszłych nieoczekiwanych wzorców zarządzania życia/zakres.

+0

Typy, które nie wymagają czyszczenia mogą zaimplementować 'IDisposable' łatwo i poprawnie za pośrednictwem' void IDisposable.Dispose() {}; '. Jeśli niektóre obiekty zwrócone z funkcji fabrycznej będą wymagały czyszczenia, łatwiejsze i łatwiejsze będzie zwrócenie typu, który implementuje 'IDisposable' i wymaga, aby klienci zbalansowali każde wywołanie do funkcji fabrycznej za pomocą wywołania zwróconego obiektu' Dispose' niż do wymagają, aby klienci określili, które obiekty wymagają czyszczenia. – supercat