Czym są metody wirtualne i dlaczego nie powinniśmy wywoływać ich w konstruktorze?

Dziś wpis poruszający tematykę metod wirtualnych, który jest skierowany raczej dla początkujących programistów choć nie ukrywam, że ja sam stosunkowo późno pojąłem to zagadnienie. Dlaczego więc o tym piszę? Tak jak przy okazji jednego z moich pierwszych postów (link macie tu) do podjęcia tematu sprowokował mnie Resharper, który to ostatnio wyświetlił mi taki oto komunikat:

 

Virtual member call in constructor

 

Całe szczęście, że R# bacznie czuwał nad moimi poczynaniami, ponieważ przez pomyłkę mogłem zafundować sobie żmudne debugowanie kodu plus kilka dodatkowych, siwych włosów na głowie. Dlaczego?

 

Metody wirtualne

Żeby wytłumaczyć sobie problematykę posta musimy najpierw dowiedzieć się czym są metody wirtualne i gdzie możemy się z nimi spotkać. Mówiąc najprościej metody te pozwalają programiście na podmianę (tzw. przesłonięcie) ich logiki w klasach potomnych. Brzmi skomplikowanie? Bez obaw, po tym przykładzie wszystko będzie jasne:

 


class Animal
{
    public virtual void PrintType() =>
        Console.WriteLine("Animal");
}

class Dog : Animal
{
    public override void PrintType() =>
        Console.WriteLine("Dog");
}

public class Program
{
    public static void Main(string[] args)
    {
        new Dog().PrintType();
        Console.ReadKey();
    }
}

 

Jak widać na powyższym przykładzie, nasz program posiada dwie klasy (nie liczymy Program). Klasa Animal jest naszym typem bazowym, która posiada jedną metodę wirtualną o nazwie PrintType. Zadaniem metody jest wypisanie w konsoli słowa „Animal”. Klasa reprezentująca psa dziedziczy po klasie Animal jednocześnie przesłaniając logikę metody PrintType poprzez użycie słowa override. Jak wspomniałem wcześniej, przesłonięcie jest równoznaczne ze zmianą zachowania metody wirtualnej. Sprawdźmy zatem co zostanie wypisane w konsoli:

 

example1

 

Wszystko się zgadza. Tu być może części z Was mogły nasunąć się dwa pytania:

  • Czy możemy przesłaniać metody tak długo jak nam się to podoba?
  • Czy chcąc rozszerzyć zachowanie metody wirtualnej jesteśmy zmuszeni duplikować kod w metodzie przesłaniającej?

 

Po kolei, odpowiedź na pierwsze pytanie brzmi: tak! Ilustruje to poniższy przykład:

 


class Animal
{
    public virtual void PrintType() =>
        Console.WriteLine("Animal");
}

class Dog : Animal
{
    public override void PrintType() =>
        Console.WriteLine("Dog");
}

class Labrador : Dog
{
    public override void PrintType() =>
        Console.WriteLine("Labrador");
}

public class Program
{
    public static void Main(string[] args)
    {
        new Labrador().PrintType();
        Console.ReadKey();
    }
}


 

A tak prezentuje się wynik działania programu:

 

example2

Wniosek jest jeden. W takim przypadku wywołana zostaje zawsze ostatnia metoda przesłaniająca i tylko ona! Mówiąc kolokwialnie wywołamy metodę klasy, która w hierarchii znajduje się najniżej. Zapamiętajcie to, ponieważ rodzi to pewne implikacje, do których zaraz przejdziemy. Warto także wspomnieć, że jesteśmy w stanie zablokować możliwość dalszych modyfikacji poprzez oznaczenie metody jako sealed. Przykładowo:

 


class Animal
{
    public virtual void PrintType() =>
        Console.WriteLine("Animal");
}

class Dog : Animal
{
    public sealed override void PrintType() =>
        Console.WriteLine("Dog");
}

class Labrador : Dog
{
    public override void PrintType() => //BŁĄD. Nie możemy dalej modyfikować zachowania metody
        Console.WriteLine("Labrador");
}

 

Przejdźmy do pytania drugiego. Jak się pewnie domyślacie twórcy języka przewidzieli chęć jedynie rozszerzenia działania metody wirtualnej, a nie kompletnej jej zmiany. Sposób realizacji tego zadania przedstawia poniższy kod:

 




class Animal
{
    public virtual void PrintType() =>
        Console.WriteLine("Animal");
}

class Dog : Animal
{
    public override void PrintType()
    {
        Console.WriteLine("Dog");
        base.PrintType();
    }
        
}

class Labrador : Dog
{
    public override void PrintType()
    {
        Console.WriteLine("Labrador");
        base.PrintType();
    }
        
}

public class Program
{
    public static void Main(string[] args)
    {
        new Labrador().PrintType();
        Console.ReadKey();
    }
}


 

Jak widać wystarczy jedynie wywołać metodę „rodzica” używając słowa base. Efekt jest następujący:

 

example3

 

Ponownie zwróćcie uwagę na kolejność wypisania wyrazów. Wywoływanie odbywało się „od dołu” hierarchii. No dobrze, z takim warsztatem jesteśmy w stanie w pełni zrozumieć problematykę dzisiejszego wpisu.

 

Wywołanie metody wirtualnej w konstruktorze

Dlaczego w takim razie Resharper przestrzegał mnie przed wywołaniem metody wirtualnej w konstruktorze? Zmodyfikujmy nieco nasz przykład:

 



class Animal
{
    public Animal()
    {
        PrintType();
    }

    public virtual void PrintType() =>
        Console.WriteLine("Animal");
}

class Dog : Animal
{
    public Dog()
    {
        
    }

    public override void PrintType() =>
        Console.WriteLine("Dog");
}

class Labrador : Dog
{
    StringBuilder StringBuilder { get; }

    public Labrador()
    {
        StringBuilder = new StringBuilder();
    }

    public override void PrintType()
    {
        StringBuilder
            .Append("La")
            .Append("bra")
            .Append("dor");

        Console.WriteLine(StringBuilder.ToString());
    }
        
}

public class Program
{
    public static void Main(string[] args)
    {
        new Labrador().PrintType();
        Console.ReadKey();
    }
}


 

W konstruktorze klasy Animal dokonałem wywołania metody wirtualnej. Aby zrozumieć jak zachowa się nasz program musimy wiedzieć w jakiej kolejności zostaną wywołane kolejne konstruktory:

  1. Dog
  2. Animal
  3. Labrador

 

Co się zatem stanie? Po wejściu w konstruktor klasy Animal wywołamy metodę wirtualną PrintType. Ktoś mógłby powiedzieć, że to nic strasznego, ponieważ wypiszemy w konsoli  wyraz „Animal”. Błąd! Pamiętacie co prosiłem zapamiętać? Owszem wywołamy metodę PrintType, ale w klasie która jest najniżej w hierarchii czyli klasy Labrador! I tu pojawia się haczyk. Wywołując ów metodę nie wywołamy uprzednio konstruktora klasy Labrador, tym samym nie zainicjujemy obiektu StringBuilder! Nasz program zakończy się zatem wyjątkiem NullReferenceException. Jak więc widać R# ostrzegł mnie bardzo dobrze, a uwierzcie mi, że doszukiwanie się błędu w takim kodzie to piekło.

 

Wiecie już czym są metody wirtualne i jak działają. Nie pozostaje mi nic innego jak pożegnać się z Wami i zachęcić do śledzenia mnie na twitterze oraz facebooku gdzie pojawiają się najświeższe wpisy i ciekawostki z branży. Ponadto wszystkich Was, którzy jeszcze nie głosowali w ankiecie konkursu Daj się poznać, zachęcam do przejrzenia blogów finalistów (wśród których mam zaszczyt być) i oddania głosu na trzy najlepsze. Link macie tu.

 

Bonusowy LOL kontent na do widzenia 🙂

Od momentu opublikowania pierwszego wpisu minęły ok. 4 miesiące. Przez ten czas strasznie irytowały mnie dwie rzeczy:

  • brak możliwości wyjustowania tekstu
  • brak możliwości zmiany rozmiaru czcionki w poście

 

Jak widzicie ten post spełnia moje obie zachcianki. Co więc zrobiłem? Okazało się, że toolbox nad obszarem do pisania w Wordpresie posiada magiczny przełącznik:

 

example4

 

Szczerze mówiąc nigdy nie kusiło mnie jego jego kliknięcie, ponieważ jak dla mnie sugeruje, że się po prostu schowa. Okazało się jednak, że nagle pojawiła się opcja justowania tekstu i zmiany rozmiaru czcionki… Mam tylko pytanie. DLACZEGO JUSTOWANIE NIE ZNALAZŁO SIĘ OBOK INNYCH OPCJI FORMATOWANIA TEKSTU, A NP. PODKREŚLENIE KOŁO BOLDOWANIA I POCHYLANIA?!!! Boże, albo ja jestem taki głupi, albo faktycznie ktoś nie słyszał o UX.

 

I tym optymistycznym akcentem się z Wami żegnam 😀

Cześć !

You may also like...