wtorek, 12 grudnia 2017

Tworzenie obiektów

Z wcześniejszej notatki dowiedzieliśmy się, w jaki sposób klasa pochodna dziedziczy składowe klasy bazowej. Opis działania konstruktorów w kontekście dziedziczenia został celowo pominięty, ponieważ przykłady ograniczały się do wykorzystania domyślnych bezargumentowych konstruktorów, wygenerowanych przez kompilator. W tym wpisie skupimy się wyłącznie na konstruktorach.

W przypadku konstruktorów nie występują zasady umożliwiające ich dziedziczenie. Powód jest raczej oczywisty: konstruktor klasy Square nie ma informacji o składowych klasy Rectangle. Chcąc dostarczyć w pełni funkcjonalny obiekt, musimy go odpowiednio zainicjalizować. Bezpośrednie skorzystanie z konstruktora klasy Square mogłoby doprowadzić do błędnej inicjalizacji typu pochodnego Rectangle. Korzystając z klasy, należy korzystać z konstruktorów udostępnionych przez tę klasę. Spójrzmy na przykład poniżej, który nawiązuje do kodu z wcześniejszej notatki. Analizując tekst zwrócony przez aplikację, łatwiej będzie zrozumieć zasady określające wywołania konstruktorów, które zostały opisane poniżej przykładu.

using System;

namespace BuildingObjects
{
    public class Program
    {
        class Square
        {
            public Square()
            {
                Console.WriteLine("Constructor Square()");
            }

            public Square(int a) : this()
            {
                Console.WriteLine($"Constructor Square({a})");
            }

            public int A { get; set; }

            public virtual int CalcArea()
            {
                return A * A;
            }
        }

        class Rectangle : Square
        {
            public Rectangle()
            {
                Console.WriteLine("Constructor Rectangle()");
            }

            public Rectangle(int a, int b) : base(a)
            {
                Console.WriteLine($"Constructor Rectangle({a}, {b})");
            }

            public int B { get; set; }

            public override int CalcArea()
            {
                return A * B;
            }
        }

        static void Main(string[] args)
        {
            var r0 = new Rectangle() { A = 10, B = 20 };

            Console.WriteLine();
            
            var r1 = new Rectangle(5, 15);

            Console.ReadLine();
        }
    }
}

Zarówno klasa Rectangle jak i Square posiadają zdefiniowane konstruktory, wewnątrz których wyświetlany jest tekst pozwalający na prześledzenie ścieżki. Klasa Rectangle posiada dwa konstruktory, co umożliwia utworzenie obiektu typu Rectangle zarówno poprzez new Rectangle() jak i new Rectangle(5, 15). Podczas tworzenia obiektu typu Rectangle, konstruktor klasy bazowej Square wywoływany jest jako pierwszy. Dopiero po nim następuje wywołanie konstruktora klasy Rectangle.

Konstruktor Rectangle(int a, int b) jawnie określa, który z konstruktorów klasy bazowej ma zostać wywołany poprzez słowo kluczowe base. Sam base(a) ma charakter sygnatury metody, gdzie wewnątrz nawiasów umieszczamy parametry określające konkretny konstruktor klasy bazowej. Usunięcie przekazanego parametra a, zostawiając base(), spowoduje wywołanie konstruktora domyślnego klasy bazowej. W przypadku konstruktora domyślnego (bezargumentowego) typu Rectangle, następuje niejawne wywołanie domyślnego konstruktora klasy bazowej. Jeżeli z klasy Square usuniemy domyślny konstruktor, kompilator zgłosi błąd, taki jak poniżej.

W tym przykładzie pojawia się jeszcze jedno słowo kluczowe this. Co prawda poznaliśmy je wcześniej w kontekście referencji do obiektu, jednak w tym kontekście, podobnie jak wcześniej opisane base(), pozwala określić, który z konstruktorów ma zostać wywołany. Jednak tym razem, nie będzie to konstruktor klasy bazowej, a inny konstruktor wewnątrz klasy.

Sprawa się nieco komplikuje, gdy dołożymy inicjalizację składowych klas. Obrazuje to kod poniżej. Dodana została statyczna metoda SetDefault, zwracająca 0. Ważne jest jednak, że metoda zapisuje informację, która z właściwości została zainicjalizowana.

using System;

namespace BuildingObjects
{
    public class Program
    {
        protected static int SetDefault(string prop)
        {
            Console.WriteLine($"Initialization of {prop}");
            return 0;
        }

        class Square
        {
            public Square()
            {
                Console.WriteLine("Constructor Square()");
            }

            public Square(int a) : this()
            {
                Console.WriteLine($"Constructor Square({a})");
            }

            public int A { get; set; } = SetDefault(nameof(A));

            public virtual int CalcArea()
            {
                return A * A;
            }
        }

        class Rectangle : Square
        {
            public Rectangle()
            {
                Console.WriteLine("Constructor Rectangle()");
            }

            public Rectangle(int a, int b) : base(a)
            {
                Console.WriteLine($"Constructor Rectangle({a}, {b})");
            }

            public int B { get; set; } = SetDefault(nameof(B));

            public override int CalcArea()
            {
                return A * B;
            }
        }

        static void Main(string[] args)
        {
            var r0 = new Rectangle() { A = 10, B = 20 };

            Console.WriteLine();

            var r1 = new Rectangle(5, 15);

            Console.ReadLine();
        }
    }
}

Analizując zwrócony wynik, widzimy, że w pierwszej kolejności inicjalizowane są właściwości. Być może pamiętasz, ale to zachowanie zostało już opisane w notatce Obiektowość w pigułce. Jeżeli nie miałeś okazji jej przeczytać, zachęcam Cię do tego.

Warto wiedzieć, że za sprawą kompilatora cały kod inicjalizujący obiekt umieszczony jest wewnątrz konstruktora. Kod ten podzielony jest na kilka części: pierwsza uruchamia inicjalizację składowych obiektu, nie dotyczy to składowych klasy bazowej. Następnie uruchomiony zostaje konstruktor klasy bazowej, który wywoła inicjalizację swoich składowych. Dopiero teraz uruchomiony zostaje kod wewnątrz konstruktorów.

Zauważ, że choć rozpoczynasz od wywołania konstruktora, to w pierwszej kolejności inicjalizowane są składowe w kierunku od klasy pochodnej do bazowej. Następnie w odwrotnej kolejności wykonywany jest kod zawarty wewnątrz konstruktorów. Takie działanie ma na celu prawidłową inicjalizację obiektu. Zachęcam was do przeprowadzenia testu: wewnątrz bezargumentowego konstruktora klasy Square wywołajcie metodę CalcArea. Uruchomiona zostanie przesłonięta wersja metody klasy Rectangle. Na nasze szczęście, kompilator zadbał o wartości użytych właściwości, dzięki czemu wszystko zadziała prawidłowo.

Po przeczytaniu ostatnich notatek powinieneś mieć już całkiem sporą wiedzę. Między innymi powinieneś wiedzieć, w jaki sposób metody wirtualne pozwalają na nadpisanie implementacji w klasach pochodnych. Miałeś również okazję zapoznać się z dostępem do składowych oraz procesem inicjalizacji obiektu przez konstruktory.

Troska Robert