Oto moja perspektywa: „var” się nie da nadużywać
Trafiłem ostatnio na tekst o nadużywaniu var. Przede wszystkim zafrapowało mnie użycie pojęcia „nadużywanie„, bo sugerowało, że podczas tworzenia kodu należałoby (oprócz wielu reguł) brać jeszcze pod uwagę czy danej konstrukcji nie używa się zbyt często (czyli nadużywa). Tylko jakie w takim razie powinno być kryterium umożliwiające stwierdzenie, czy coś jest nadużyciem, czy nie? Czy jeśli używam LINQ, to powinienem równoważyć jego wystąpienia używaniem pętel (for, foreach)? Czy jeśli część kodu to zwykłe procedury lub funkcje (czyli nie będące usługami konkretnego obiektu), to część z nich mogę wstawić do klas statycznych, niemniej pozostałe powinienem udostępniać tylko z poziomu instancji (choć i tak nie będą z nią nic robić)?
Prawdę powiedziawszy użycie pojęcia „nadużycie” jawi mi się właśnie jako … nadużycie.
Przyjrzyjmy się cóż takiego niewłaściwego jest w var, że nie powinno być zbyt często używane? Otóż autor tekstu argumentuje, że ukrywa ono przed programistą typ zmiennej. Hm, to może na początek przypatrzmy się następującym przykładom:
var i = 5; // i będzie typu int, bo każda podana jawnie liczba całkowita jest w C# jest tego typu var dc = 5M // dc będzie typu decimal, na co wskazuje kwalifikator typu przy liczbie var db = 1.23 // db będzie typu double, bo każda podana jawnie liczba rzeczywista w C# jest tego typu var c = 'a' // c będzie typu char var s = "Tekst"; // s będzie typu string var a = new[] { 0, 1, 2 }; // a będzie tablicą o elementach typu int var anon = new { Name = "Terry", Age = 34 }; // anon będzie typem anonimowym
Jak widać, choć nie opatrzono zmiennych jawnym typem, to wywnioskowanie typu zmiennej nie nastręcza wielkich problemów. Takich problemów nie będzie zaś na pewno, jeśli do zainicjowania zmiennej zostanie użyty konstruktor:
var list = new List<int>(); var author = new Author("Robert", "C", "Martin");
Autor jednak nie odnosił się do tego typu trywialnych zastosowań. Chodziło raczej (pozwalam sobie przytoczyć całość przykładu) o zastosowania w rodzaju:
var human = new Human(); // Ok, wiadomo, że human będzie typu Human var settings = GetSettings(); // Ustawienia...hmmm - jakiego typu? co to za ustawienia? var second = GetSecond(); // tragedia var x = GetX(); // jakiego typu jest x?
Może na początek przyjrzyjmy się trzeciej linii kodu (aby zapobiec tragedii ;)). Mamy tutaj zmienną second. Możliwości są trzy, zmienna określa:
- wartość sekundy (zapewne od 0 do 59);
- coś co jest drugie;
- ilość sekund, tylko zapomniano o końcowym „s” (tak w nazwie zmiennej jak i metody);
Czy w jakiś sposób pomoże tutaj informacja, że second jest typu int? Nie, albowiem liczba pasuje do wszystkich trzech wariantów. A jeśli to będzie string? Również niczego to nie zmieni. Zamiast var można podstawiać różne typu (w tym własne klasy), a i tak niczego to nie zmieni. Bo problem nie leży tam, gdzie próbuje się go szukać. Przypomina to trochę stare, seksistowskie powiedzenie „on nie winny, ona winna, bo mu dawać nie powinna” .
Winne są: nazwa zmiennej i nazwa metody, która ją inicjuje. Na pewno zaś winowajcą nie jest var. Jak to zatem powinno wyglądać? Oto możliwe warianty:
var secondOfMessageIncoming = MessageIncoming.Second; // lub var secondDriver = Race.Drivers.Second; // lub var secondDriver = Race.Drivers.GetPlace(2); // lub var seconds = Duration.Seconds;
Teraz nie ma wątpliwości, o co chodzi, otrzymujemy kolejno:
- wartość sekundy nadejścia komunikatu (od 0 do 59, czyli int, bo kazdy inny typ tej wartości jest mniej wydajny)
- kierowcę (klasa lub string), który jako drugi zakończył wyścig
- jak wyżej
- ilość sekund przedziału czasowego (z przedziału 0..59, gdyby było TotalSeconds, przedział może być większy), na pewno jednak int
Zauważcie, że użycie typów zamiast var niczego tu nie daje, a wręcz utrudnia późniejszą refaktoryzację, gdybyśmy np. chcieli przechowywać dane w prostszym typie (np. byte dla wartości sekundy) lub dedykowanej klasie (secondDriver może być klasą jak i stringiem). Poza tym, użycie typu wymusza jego wcześniejsze poznanie na podstawie typu zwracanego przez metody, a w danym miejscu kodu może być to kompletnie niepotrzebne, bo utworzona zmienna jest przetwarzana przez kilka metod, a uzyskany wynik przekazywany jako wynik.
Z tych samych powodów settings (z cytowanego przykładu) nie jest enigmatyczny, bo albo odwołamy się wyłącznie do nich (przekazując jakieś metodzie, jak właśnie wyżej napisałem), albo do jakiejś konkretnej właściwości (czyli konkretnego ustawienia) i tutaj znajomość typu niczego nie zmieni, bo oznaczałoby to, że każdy czytający kod ma wyrytą w pamięci strukturę ustawień – co chyba nikomu nie wydaje się możliwe – prawda? Zaś jeśli są to konkretne ustawienia, to niech i zmienna, i metoda je zwracająca konkretnie się nazywają. Dzięki temu czytającemu kod będzie łatwiej się odnaleźć. Bo piszącemu jest to obojętne – składowe ustawień poda mu przecież Visual Studio.
Co do x i GetX – to przykład jest tendencyjny, zakładam, że tak raczej piszą tylko niedouczeni nowicjusze ;).
Oto jeszcze kilka przykładów kodu, który nie pozostawia wątpliwości, co do zawartości zmiennej. Dla celów przykładu (aby nie sugerować nazwą co w zmiennej jest) ich nazwy są bardzie enigmatyczne niż to być powinno (dlatego w komentarzu podaję poprawną nazwę).
var race = raceFactory.CreateMotorcyclesRace(); // lepsza nazwa zmiennej to: motorcyclesRace var race = raceFactory.CreateCarsRace(); // lepsza nazwa zmiennej to: carsRace var race = CarsRaceObjectMother.Create(); // lepsza nazwa zmiennej to: carsRace
Czy zatem var należy używać zawsze? Och, zawsze wystrzegajcie się słowa „zawsze” ;). Odpowiadając na pytanie: nie, to byłaby druga skrajność. Z pewnością var nie przyda się w przypadku, gdy chcemy zainicjować zmienną korzystając z polimorfizmu czyli zmienna ma być typu klasy bazowej lub reprezentować interfejs.
IRace juniorRace = BikesRaceObjectMother.Create(); IRace seniorRace = BikesRaceObjectMother.Create(); AnyRace anyRace = HorseRaceObjectMother.Create();