Inicjacja w konstruktorze, czy poza nim?

2
186

Prolog

Krótki wstęp

Jakiś czas temu dostałem pierwszy i jedyny jak dotąd 🙂 komentarz który, prócz pochwały, zawierał też żądanie czegoś więcej. Oto on:

„Pociągnąłbym jeszcze temat w kierunku, czy i dlaczego inicjowanie kolekcji w konstruktorze jest lepsze od inicjowania przy deklaracji.”

Z braku czasu na śledztwo dotyczące jakiegoś grubszego tematu, postanowiłem zająć się tym zagadnieniem. Myślałem, że sprawę tak trywialną zamknę w godzince, czy dwóch. Okazało się inaczej.

Z dyskusji na temat blogowania, którą prowadziłem ostatnio ze swoim Team Leaderem (notabene – do jego komentarza odnosi się ten artykuł), wynikło kilka wniosków, ale jedna uwaga bardzo dobrze pasuje do tego posta. Otóż pisanie takie dzieli się na dwie fazy – pierwsza to śledztwo i jest ona bardzo ciekawa. Dowiadujesz się nowych rzeczy, uczysz się. Druga sprowadza się do ubrania tego wszystkiego w słowa i przekazania światu. Ta część jest nudna, trudna i bardzo często można się właśnie tu zniechęcić. Tak jest i teraz – czytanie o wszystkich pułapkach związanych z inicjowaniem pól klasy było bardzo fajne, ale już pisanie tego wszystkiego… ech 🙂 Mam tylko nadzieję, że z czasem moje umiejętności pisarskie podniosą się i także z tego etapu będę czerpał zadowolenie 🙂

Najlepsze rozwiązanie – nie znajdziesz go tutaj

Pola klasy można inicjować w konstruktorach, lub bezpośrednio przy deklaracji – to oczywiste. Mniej oczywiste są konsekwencje, jakie każdy z tych sposobów niesie. Ambicją niniejszego wpisu nie jest podanie gotowego i najlepszego rozwiązania – nie znam go. Chciałbym jednak zaprezentować wszystkie, czy też najważniejsze, fakty, które trzeba brać pod uwagę, wybierając określony sposób.

 

Mówię jak jest 🙂

0-0 (zwykłe inicjowanie)

Przejdźmy zatem do meritum. Mamy następującą klasę z jednym polem:


Pole to możemy zainicjować na dwa sposoby – albo tworząc konstruktor domyślny albo w miejscu deklaracji, czyli albo:

albo:

Przyznam się bez bicia, że zazwyczaj wybierałem ten pierwszy sposób. Nie było to podyktowane żadnymi względami praktycznymi ani przemyśleniami. Po prostu wydawało mi się to bardziej „schludne”, a że nie sprawiało problemów, to przy tym zostałem. I rzeczywiście – na pierwszy rzut oka, patrząc na wygenerowany kod IL, wszystko jest praktycznie tak samo. Przypadek pierwszy:

Jak widać, bez niespodzianek – w liniach 09 oraz 0E przeprowadzana jest inicjacja listy _primeNumbers. Jeżeli sprawdzimy kod konstruktora wygenerowanego automatycznie (przypadek drugi):

to widzimy, że różnica występuje tylko w kolejności inicjacji. Nasz konstruktor woła najpierw konstruktor klasy bazowej, a konstruktor wygenerowany automatycznie najpierw inicjuje listę (dlaczego to takie ważne, pokażę za chwilę). Innymi słowy – kompilator sprawdził wszystkie inicjacje nie umieszczone w konstruktorze i umieścił je na początku kodu IL konstruktora automatycznego.

 

Tak samo będzie, jeżeli napiszemy kilka własnych konstruktorów i pomieszamy sposoby inicjacji – część odbędzie się w konstruktorach, a część przy deklaracji. Kompilator sprawdzi to wszystko i umieści inicjacje tam, gdzie trzeba. I tu mamy pierwszą wyraźną zaletę inicjacji przy deklaracji. Weźmy taką sytuację:

1-0 (bardziej złożone łączenie konstruktorów)

Wykorzystujemy tutaj pięknie opcję „constructor chaining” czyli łączenia konstruktorów. Co jednak, gdy naszemu koledze z zespołu przyjdzie do głowy dodanie trzeciego konstruktora nie przyjrzy się dokładnie i nie połączy go z konstruktorem bezparametrowym? Np.:

 

Oczywiście, wszystko wyjdzie za chwilę na testach jednostkowych (ta…), ale jeżeli nie? W przypadku, gdyby inicjacja była przy deklaracji, kompilator zaopatrzyłby w inicjator także ten nowo dodany konstruktor. Wyglądałoby to tak:

Mamy zatem 1:0 dla inicjacji przy deklaracji – nie musimy się bać, że ktoś się pomyli i zburzy symetrię naszej kawowej górki 🙂

2-0 (wołanie metod wirtualnych w konstruktorze bazowym)

Teraz pokażę coś, co Resharper od razu zaznacza, jako potencjalny błąd. Oto przykładowy kod:

Błąd prawdopodobnie nie jest widoczny na pierwszy rzut oka, jednak przy tworzeniu obiektu klasy B, najpierw zostanie wywołany konstruktor A(), gdzie zainicjowana jest lista _primeNumbers, a następnie wywołana metoda VirtualMethod, korzystająca z listy _otherList, jeszcze nie zainicjowanej – jej inicjacja odbywa się dopiero w konstruktorze B(). Oczywiście, nie jest to przypadek częsty, a poza tym niezalecany, ale bywają różne sytuacje i raz rzeczywiście takie wołanie metody wirtualnej w konstruktorze klasy bazowej upraszczało mój kod i stosowałem je – bacząc przy tym, aby nie doszło do podanej wyżej sytuacji wyjątkowej.

Błąd występujący tutaj można naprawić albo nie wołając metody VirtualMethod w konstruktorze A(), albo inicjując obie listy przy deklaracji. Wtedy, jak zauważyliśmy w poprzednim podpunkcie, kompilator wygeneruje kod tak, aby najpierw inicjowana była lista, a następnie wołany był konstruktor klasy bazowej. 2-1 (inicjacja właściwości automatycznych) Dużo mówić tu nie trzeba – właściwości automatycznych nie można inicjować w miejscu ich deklaracji. Tutaj konstruktor nie dość, że ma przewagę, to jest jedyną możliwością. Kod:

Dla porządku dodam jednak, że jest to prawdą dla wersji języka niższej niż 6.0. W najnowszej możemy napisać:

Nie wszyscy jednak mają to szczęście i mogą używać wersji najnowszej, stąd przypisuję tu punkt konstruktorom.

2-2 (łapanie wyjątków podczas inicjacji)

Tu także sprawa jest prosta – jeżeli po prostu inicjujesz domyślną wartością, problemu nie ma. Jednak gdy chcesz zainicjować w sposób bardziej wymyślny, tylko konstruktor umożliwia obsługę ewentualnych błędów. Weźmy na przykład kod:

Metoda GetRowsFromDatabase jest oczywiście mocno uproszczona, ale możemy sobie wyobrazić, że łączy się ona z bazą, przetwarza rekordy itp. Co jeżeli coś pójdzie nie tak i zostanie rzucony wyjątek? W obecnej postaci, jego obsługa jest niemożliwa. Co innego tutaj:

3-2 (inicjowanie pól statycznych)

Ostatni już argument za inicjacją w miejscu deklaracji. Pola statyczne także można inicjować w konstruktorze i poza nim. W obu przypadkach kod konstruktora wygenerowany przez kompilator będzie bardzo podobny, ale jeżeli inicjujemy zmienną w miejscu deklaracji, klasa będzie oznaczona jako:


.class private auto ansi beforefieldinit ClassName

a jeżeli w konstruktorze statycznym, to:

.class private auto ansi ClassName

Małe słówko „beforefieldinit” powoduje, że konstruktor statyczny będzie wołany tylko, jeżeli wołana będzie statyczna zmienna. Jeżeli nie będzie do niej odwołań, konstruktor nie zostanie wywołany. Jeżeli inicjujemy ją w konstruktorze, to będzie on wywołany w przypadku, jeżeli używane będzie jakiekolwiek pole klasy (niekoniecznie statyczne). W zależności od okoliczności (czyli tego, co jest potrzebne do zainicjowania zmiennej), może to mieć znaczny wpływ na wydajność.

Podsumowanie

Nie jestem w stanie powiedzieć, co jest lepsze. Część przedstawionych problemów jest akademicka, lub łatwa do uniknięcia. Najlepszym przykładem jest mój kod – mimo, że używam konstruktorów do inicjowania, to nie mam z tym żadnych kłopotów. Wydaje mi się, że najlepszą radą jaką mogę dać jest: jakikolwiek sposób wybierzesz, bądź w tym konsekwentny. Tylko (i aż) tyle wystarczy, by polepszyć strukturę pisanego kodu.

Nota dodatkowa: tym razem znów się poddałem przy edycji wpisu. Blogger głupieje gdy wklejam kod z Visual Studio, a nadal nie zdołałem zainstalować żadnej wtyczki do obsługi kodu, stąd musiałem się ratować wstawianiem zrzutów z ekranu. Mam nadzieję, że w przypadku tak prostych przykładów, nie stanowi to dla Ciebie, czytelniku, większego problemu.
  • Hej Pawle,
    Dzięki za wzięcie tematu na tapetę. Fajnie przeanalizowałeś sprawę.
    Ja, jak wiesz, jestem zwolennikiem inicjowania przy deklarowaniu ze względu na to, że takie podejście uwalnia mnie od problemu, czy każdy konstruktor zainicjuje czy nie moją kolekcję (tu muszę przypomnieć, że mi głównie chodzi o kolekcje).
    Ale mam pewną radę czy robić tak, czy inaczej. Nie wkładałbym do jednego worka inicjowania konstruktorem – nazwę prostym i poprzez metody, które robią coś bardziej zaawansowanego.
    I tu moja rada – jeżeli inicjujemy prostym konstruktorem (tak jak kolekcję, tylko po to, żeby była i żeby można było do niej coś dodać) to radzę przy deklaracji, ale jeżeli to coś bardziej wyrafinowanego to zdecydowanie umieściłbym to w konstruktorze, bo to coś niebanalnego i chcielibyśmy o tym jawnie wiedzieć czytając kod konstruktora, że kolekcję inicjujemy np. danymi z bazy.
    Mi wygląda na to, że uniwersalnego rozwiązania nie ma. Są różne sposoby, ważne aby znać ich cechy i umieć świadomie a nie przypadkowo wybierać i móc powiedzieć dlaczego się zrobiło tak a nie inaczej. A ten artykuł właśnie takiej wiedzy dostarcza. Pozdrawiam

  • Dzięki za miłe słowa Andrzeju 🙂 Jakbyś miał jakieś jeszcze tematy do zbadania, to zapraszam 🙂