O modyfikatorze sealed już kiedyś pisałem – dzięki niemu możemy zabronić dziedziczenia po danej klasie:
sealed class SealedClass { } class ChildClass:SealedClass // BLAD { }
Moim zdaniem jest on niedoceniany i zbyt rzadko używany. Może dlatego, że nie wnosi on nic nowego do funkcjonalności czy łatwości w pisaniu kodu a “jedynie” stanowi ważny element w projekcie oraz w tym, jak inni użytkownicy z takiej biblioteki będą korzystać.
Pierwszą zaletą SEALED jest wydajność. Oczywiście w większości przypadków taka optymalizacja nie ma jakiegokolwiek znaczenia ale dobrze wiedzieć, że oprócz dobrego design osiągniemy lepszą wydajność. W programowaniu obiektowym, wywoływanie metod wirtualnych jest trochę wolniejsze ponieważ najpierw trzeba sprawdzić, która dokładnie metoda ma zostać wykonana (polimorfizm). Do tego służy tzw. Virtual method table – po szczegóły odsyłam do Wikipedii. Przykład:
internal class Program { class BaseClass { internal virtual void HelloWorld() { } } sealed class SampleClass : BaseClass { } private static void Main(string[] args) { SampleClass sampleClass = new SampleClass(); sampleClass.HelloWorld(); } }
Pomimo, że zostanie wyemitowana instrukcja IL callvirt, CLR wywoła metodę w sposób niewirtualny a zatem pozbawiony przeszukiwania VMT. Gdyby nie modyfikator sealed, SampleClass mógłby wskazywać na klasy pochodne, które mają już inną implementacje metody HelloWorld – stąd wywołanie byłoby trochę wolniejsze.
Powyższa optymalizacja należy jednak do ciekawostek. W praktyce dobrze to wiedzieć i pisać kod wydajniejszy ale zdecydowanie nie przynosi to zauważalnego efektu. Ważniejszą kwestią jest stabilność klasy. Jeśli nie zaprojektowało się klasy z myślą o możliwości jej rozszerzenia wtedy powinno się użyć sealed. W końcu klasa nienapisana z myślą o metodach wirtualnych itp. może przynieść efekty nieoczekiwane. Warto zabezpieczyć się przed tym zagrożeniem i “zapieczętować ją”. Pochodne klasy mogą w sposób nieświadomy modyfikować stan klasy bazowej czy nie wywoływać bazowych metod kiedy jest to potrzebne. Nie każdy pisany kod musi być zawsze maksymalnie rozszerzalny i skalowalny. Czasami potrzebna jest pojedyncza klasa i nie warto tracić czasu na przygotowanie jej pod przyszłe, ewentualne rozszerzenia. Pisząc rozszerzalną klasę należy każde pole, metodę przemyśleć tak, aby klasa pochodna nie mogła popsuć bazowej implementacji. Zasada jest taka, że inny programista, nie znając bazowej implementacji nie powinien przypadkowo zakłócić stabilności klas – poprawny projekt zawsze wymusza to.
Jeśli w pewnym momencie okaże się, że jednak potrzebne są pewne rozszerzenia wtedy nie ma problemu z usunięciem sealed. W drugą stronę jest to oczywiście niemożliwe. Klasa raz niezapieczętowana nie może później już być zapieczętowana ponieważ mogłoby to spowodować błędy w kompatybilności. Jeśli klasa najpierw była unsealed, wtedy w międzyczasie ktoś mógł już napisać klasę pochodną. W takim przypadku dodanie sealed po prostu spowodowałoby, że jakiś kod nie skompiluje się.
Dobrym zwyczajem jest rozpoczęcie implementacji od maksymalnych ograniczeń w dostępności do danych. Metody zatem powinno pisać się jako niewirtualne, prywatne a klasy jako sealed oraz internal (jeśli nie są one zagnieżdżone).
Warto wspomnieć, że modyfikator sealed można również dołączać do metod przeciążanych:
class BaseClass { internal virtual void HelloWorld() { } } class SampleClass : BaseClass { sealed internal override void HelloWorld() { base.HelloWorld(); } }
W takim przypadku SampleClass:HelloWorld jest ostatnią przeciążoną implementacją. Dalsze klasy dziedziczące po SampleClass nie będą mogły już dostarczać własnej implementacji HelloWorld. Podobnie sealed może zostać połączony z właściwościami – nie powinno to zaskakiwać bo są one tak naprawdę zwykłymi metodami.