piątek, 30 czerwca 2017

Dziedziczenie

Język C# udostępnia model pojedynczego dziedziczenia, co oznacza, że dana klasa odziedziczy składowe, zgodnie z definicją klasy bazowej. W przypadku interfejsów możliwe jest dziedziczenie wielokrotne. Nazwę klasy bazowej określamy po znaku : (dwukropku) za nazwą klasy. Jeżeli klasa dodatkowo ma implementować interfejsy, to także one podawane są po znaku : (dwukropku), rozdzielone znakiem , (przecinkiem). W sytuacji, gdy klasa jednocześnie dziedziczy z klasy bazowej oraz interfejsów, w pierwszej kolejności wskazujemy klasę bazową, a dopiero po niej określamy interfejsy. Przykład poniżej prezentuje składnię umożliwiającą określenie klasy bazowej wraz z interfejsami.

public class Vehicle
{
    public string SerialNumber { get; set; }
}

public interface IStart
{
    void Start();
}

public interface IStop
{
    void Stop();
}

public class TwoWheeler : Vehicle, IStart, IStop
{
    public void Start()
    {
    }

    public void Stop()
    {
    }
}

public class Motorbike : TwoWheeler
{
}

W naszym przykładzie klasa Motorbike dziedziczy bezpośrednio po TwoWheeler, a jednocześnie pośrednio po Vehicle. Oczywiście, nie jest to dziedziczenie wielokrotne. Klasa pochodna dziedziczy wszystkie składowe, z uwzględnieniem modyfikatorów określających dostępność. Tworząc instancję klasy Motorbike, mamy możliwość wykonania metod: Start oraz Stop. Instancja klasy Motorbike jest instancją zarówno TwoWheeler jak i Vehicle, dzięki czemu możliwe jest wykonanie niejawnej konwersji z Motorbike na Vehicle.

var m = new Motorbike();
m.Start();

Vehicle v = m;
v.SerialNumber = "SB1BG76L70E007081";

Automatyczna niejawna konwersja w drugą stronę nie jest dostępna. Możliwe jest wykonanie jawnej konwersji (na kilka sposobów). Przede wszystkim możemy wykonać rzutowanie, jednak nie ma gwarancji, że konwersja zakończy się sukcesem. W przypadku, gdy obiekt v nie jest instancją typu Motorbike, zgłoszony zostanie wyjątek InvalidCastException.

var motorbike = (Motorbike)v;

W sytuacji, gdy będziemy chcieli mieć pewność, że obiekt v jest instancją typu Motorbike, możemy skorzystać z operatora as. Operator as wykonuje próbę konwersji, bez zgłaszania wyjątku w razie niepowodzenia. Jeżeli rzutowanie nie powiedzie się, operator zwraca wartość null.

var motorbike = v as Motorbike;

Ostatnim sposobem jest zastosowanie operatora is, który ogranicza się do sprawdzenia typu oraz zwrócenia wartości true, w przypadku gdy obiekt jest zgodny z przekazanym typem.

var vIsMotorbike = v is Motorbike;

Analogicznie do klas również interfejsy mogą dziedziczyć, z tą różnicą, że możliwe jest dziedziczenie wielokrotne. Pokazuje to przykład poniżej.

public interface IStart
{
    void Start();
}

public interface IStop
{
    void Stop();
}

public interface IRun : IStart, IStop
{
    bool IsRunning { get; }
}

Omówiliśmy dziedziczenie klas oraz interfejsów. Pozostało zaprezentować dziedziczenie w przypadku typów ogólnych. Kod poniżej udostępnia klasę bazową Base będącą typem ogólnym. Tworząc typ pochodny od Base, należy określić argument typu lub ponownie użyć typu ogólnego. W przypadku dziedziczenia po klasie posiadającej kilka parametrów typu, możliwe jest mieszanie obu sposobów.

public class Base<T>
{
    public T Value { get; set; }
}

public class ListInt : Base<int>
{

}

public class GenericBase<T> : Base<T>
{

}

public class DictionaryBase<TKey, TValue> : Base<TKey>
{
    public TKey Key { get; set; }
}

public class DictionaryInt<TValue> : DictionaryBase<int, TValue>
{

}

Jeśli tworząc nową klasę nie wskażemy typu ogólnego, kompilator zrobi to za nas, używając typu object. To właśnie dzięki temu możliwe jest użycie typu object jako pojemnika dowolnie innego typu (z wyjątkiem wskaźników, które nie dziedziczą po object). Typ object udostępnia kilka składowych: ToString, Equals, GetHashCode oraz GetType, które występują we wszystkich referencjach dowolnego typu.

Zacznijmy od metody ToString, której domyślna implementacja zwraca nazwę typu. Jednak część z typów implementuje ją w bardziej użyteczny sposób, zwracając tekstowe reprezentacje swoich wartości. Dla przykładu struktura Point zwraca wartości X oraz Y.

Metoda Equals pozwala na porównanie z dowolnym innym obiektem. Domyślna implementacja sprawdza tożsamość, czyli wartość true zostanie zwrócona wyłącznie gdy porównywany obiekt jest tym samym obiektem. Nierzadko typy posiadają własną implementację, porównując na podstawie wartości, ponownie posłużę się przykładem struktury Point, która działa właśnie w ten sposób. W sytuacji, gdy potrzebujemy porównać tożsamość np. obiektów Point, możemy użyć metody ReferenceEquals.

var p1 = new Point(10, 30);
var p2 = new Point(10, 30);

var resultEquals = p1.Equals(p2);
var resultReferenceEquals = ReferenceEquals(p1, p2);

W przypadku metody GetHashCode, zwracana jest liczba całkowita reprezentująca skrócone wartości obiektu. Z metody korzystają mechanizmy bazujące na kodach mieszających. Para obiektów, dla których metoda Equals zwraca true, musi zwrócić identyczne kody mieszające. Należy jednak pamiętać, że zwrócony kod dla tego samego obiektu może być różny w zależności od wersji .NET Framework, architektury systemu, czy domeny aplikacji.

Metoda GetType zwraca obiekt typu Type, umożliwiający pozyskanie informacji na temat obiektu. Typ jest związany z mechanizmem odzwierciedleń, który zostanie opisany w niedalekiej przyszłości.

Metody ToString, Equals oraz GetHashCode są oznaczone jako wirtualne, dzięki czemu możliwe jest ich nadpisanie w klasach pochodnych. Więcej o metodach wirtualnych możesz przeczytać tutaj.

Po przedstawieniu zasad dotyczących dziedziczenia oraz składowych wynikających z automatycznego dziedziczenia po typie object, przejdźmy do modyfikatorów. Pierwsze informacje opisujące modyfikatory pojawiły się w notatce Obiektowość w pigułce. Dla przypomnienia, składowe oznaczone modyfikatorem public dostępne są dla wszystkich, private zezwala wyłącznie na dostęp wewnątrz typu. Zakres modyfikatora internal odnosi się do komponentu (podzespołu) wewnątrz którego został zdefiniowany. Dziedziczenie wprowadza dwa kolejne poziomy. Składowe oznaczone protected dostępne są wewnątrz zdefiniowanego typu oraz we wszystkich typach pochodnych. Drugim wprowadzonym poziomem jest protected internal (dla kompilatora kolejność słów kluczowych jest bez znaczenia), który zezwala na dostępność wewnątrz typu oraz we wszystkich typach pochodnych, a dodatkowo dla całego kodu wewnątrz podzespołu, w którym zdefiniowany został typ.

Korzystając z modyfikatorów podczas dziedziczenia należy zwrócić uwagę, aby typ pochodny nie posiadał mniej rygorystycznego modyfikatora niż typ bazowy. Analizując przykład poniżej, klasa bazowa A stanowi fragment klasy B, a zatem każdy, kto będzie korzystał z B, musi posiadać dostęp do klasy A. Modyfikator public łamie tę zasadę, przez co język C# nie pozwala na takie działanie. Klasa B może skorzystać wyłącznie z modyfikatorów protected lub private (zawężenie dostępu jest możliwe). Poniżej kodu, zamieszczam generowany przez niego błąd.

protected class A
{
    protected void Print()
    { }
}

public class B : A
{

}

Notatka powinna przybliżyć tematykę dziedziczenia, która jest niezwykle istotna w programowaniu obiektowym. Kontynuacją jest wpis dotyczący metod wirtual

Troska Robert