Dziedziczenie

Dziedziczenie #

Dziedziczenie działa podobnie jak w C++. Mamy jeden tryb dziedziczenia, który odpowiadałby publicznemu dziedziczeniu z C++. W C# nie mamy wielodziedziczenia: możemy dziedziczyć tylko po jednej klasie na raz.

public class Person
{
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public int Age { get; set; }
}

public class Student : Person
{
    public string StudentID { get; set; }
}

Tak jakbyśmy się spodziewali Student, to także Person posiada wszystko to co posiada klasa bazowa.

Student alice = new Student() { FirstName = "Alice", 
                                LastName = "Brown", 
                                Age = 25, 
                                StudentID = "X-84355" };
Console.WriteLine($"{alice.FirstName} {alice.LastName}, {alice.Age}, {alice.StudentID}");

Polimorfizm #

Referencje są polimorficzne, można je traktować tak jakby były typu bazowego. Możemy przekazać obiekt klasy Student do metody, która akceptuje obiekt klasy bazowej - w końcu to jest to samo. Dzięki temu możemy pisać rozszerzalny kod, który operuje na typie ogólnym, nie zastanawiając się o konkretne implementacje.

Register(alice);

public void Register(Person person)
{
// ...
}

Możemy także niejawnie rzutować typ podklasy, do typu bazowego. W drugą stronę wymaga to jawnego rzutowania i może zakończyć się wyjątkiem InvalidCastException.

Person person = alice; // Implicit cast
Student student = (Student)person; // Requires explicit cast

Operator as #

Operator as działa podobnie do dynamic_cast z C++. Możemy go użyć, żeby wykonać rzutowanie w dół, które zwraca null jeżeli się ono nie powiedzie.

Teacher teacher = alice as Teacher;

if (teacher != null)
{
    Console.WriteLine("SAP ID: {teacher.SapID}");
}

public class Teacher : Person
{
    public int SapID { get; set; }
}

Są jednak lepsze sposoby na sprawdzenie typu.

Operator is #

Generalnie operator is sprawdza, czy zmienna pasuje do wzorca i zwraca wynik w postaci boola. Jednym ze wzorców który nas interesuje jest wzorzec typu.

if (alice is Teacher)
{
    Teacher teacher = (Teacher)alice;
    Console.WriteLine("SAP ID: {teacher.SapID}");
}

Dodatkowo we wzorcu typu możemy wprowadzić zmienną tego typu.

if (alice is Teacher teacher)
{
    Console.WriteLine("SAP ID: {teacher.SapID}");
}

Metody wirtualne #

Funkcje wirtualne działają na tej samej zasadzie co w C++. Pod spodem wykorzystują mechanizm tablic funkcji wirtualnych.

W C# wirtualne mogą być nie tylko metody, ale też właściwości, indeksery i zdarzenia.

public class Vehicle
{
    public float Position { get; protected set; } = 0;
    public virtual float Speed { get; protected set; } = 1.0f;
    public string Name { get; }
    
    public Vehicle(string name) => Name = name;
    public virtual float Run(float dt)
    {
        Console.WriteLine($"Vehicle.Run({dt})");
        return (Position = Position + dt * Speed);
    }
}

public class Car : Vehicle
{
    public override float Speed { get; protected set; } = 0.0f;
    public virtual float Acceleration { get; }
    
    public Car(string name, float acceleration) : base(name) => Acceleration = acceleration;
    public override float Run(float dt)
    {
        Console.WriteLine($"Car.Run({dt})");
        Position += dt * Speed;
        Speed += dt * Acceleration;
        return Position;
    }
}

public class Bike : Vehicle
{    
    public Bike(string name) : base(name) {}
    public override float Run(float dt)
    {
        Console.WriteLine($"Bike.Run({dt})"); // We can skip implementation if not for the output.
        return base.Run(dt);
    }
}

base #

Jeżeli chodzi o słowo kluczowe base, to ma ono tutaj dwa znaczenia. Możemy go użyć do:

  • Wywołania do metod nadpisanych
  • Wywołania konstruktora klasy bazowej

Działa analogicznie do słówka this, ale dla klasy bazowej.

List<Vehicle> vehicles = [new Bike("Romet"), new Car("Honda Civic", 1.5f), new Car("Toyota Yaris", 1.0f)];

const float dt = 1.0f;
for (float time = 0.0f; time < 4.0f; time += dt)
{
    Console.WriteLine($"====== time: {time,5:F1}s ======");
    foreach (var vehicle in vehicles)
    {
        vehicle.Run(dt);
    }
    foreach (var vehicle in vehicles)
    {
        Console.WriteLine($"Vehicle {vehicle.Name}, Position {vehicle.Position}");
    }
}

Kod źródłowy

Całość działa analogicznie jakby to działało w C++. Jeśli vehicle jest typu Car, to wywoła się Car.Run, jeżeli typu Bike, to wywoła się Bike.Run. Jeżeli typ nie nadpisałby tej metody to wywołałoby się Vehicle.Run. Jedyna kosmetyczna różnica jest taka, że w C# słówko override jest wymagane, jeżeli chcemy nadpisać wirtualną metodę - jeżeli tego nie zrobimy tylko ją przykryjemy i wygenerujemy ostrzeżenie kompilatora.

Klasy abstrakcyjne #

Klasy abstrakcyjne to klasy ze słówkiem kluczowym abstract. Nie możemy inicjalizować obiektów tej klasy, możemy w takiej klasie definiować abstrakcyjne składowe - czyli takie, które mają sygnaturę, ale nie mają implementacji. Mogą to być abstrakcyjne metody, właściwości, indeksery i zdarzenia. Jest to analogia do klas z C++, które mają zadeklarowane funkcje czysto wirtualne.

Zamiast dostarczać implementację metody Vehicle.Run, możemy zrobić ją abstrakcyjną. Wtedy, każda nieabstrakcyjna podklasa musi dostarczyć własną implementację.

public abstract class Vehicle
{
    public float Position { get; protected set; } = 0;
    public virtual float Speed { get; protected set; } = 1.0f;
    public string Name { get; }
    
    public Vehicle(string name) => Name = name;
    public abstract float Run(float dt);
}

Przykrywanie składowych #

Podklasa może definiować te same składowe, co klasa bazowa.

public class Base
{
    public int Member = 0;
    public string Method() => "Base.Method";
    public virtual string VirtualMethod() => "Base.VirtualMethod";
}

public class Hider : Base
{
    public int Member = 1;
    public string Method() => "Hider.Method";
    public string VirtualMethod() => "Hider.VirtualMethod";
}

public class Overrider : Base
{
    public override string VirtualMethod() => "Overrider.VirtualMethod";
}

W tym przykładzie pola i metody w Hiderprzykrywane. Zazwyczaj nie jest to celowe działanie - kompilator w takim przypadku generuje ostrzeżenie (warning).

Konsekwencje przykrywania możemy zobaczyć na przykładzie:

Hider hider = new Hider();
Base baseHider = hider;

Overrider overrider = new Overrider();
Base baseOverrider = overrider;

Console.WriteLine(hider.Method()); // Hider.Method
Console.WriteLine(hider.VirtualMethod()); // Hider.VirtualMethod
Console.WriteLine(baseHider.Method()); // Base.Method
Console.WriteLine(baseHider.VirtualMethod()); // Base.VirtualMethod

Console.WriteLine(overrider.Method()); // Base.Method
Console.WriteLine(overrider.VirtualMethod()); // Overrider.VirtualMethod
Console.WriteLine(baseOverrider.Method()); // Base.Method
Console.WriteLine(baseOverrider.VirtualMethod()); // Overrider.VirtualMethod

Kod źródłowy

Przykrywane metody nie są nadpisywane, zazwyczaj jeżeli metoda była oznaczona jako virtual, to intencją jest jej nadpisanie. Jeżeli faktycznie zamiarem było przykrycie, to można ostrzeżenia kompilatora uciszyć słówkiem kluczowym new. Jest to jedyne działanie tego słowa kluczowego w tym kontekście.

public class Hider : Base
{
    public new int Member = 1;
    public new string Method() => "Hider.Method";
    public new string VirtualMethod() => "Hider.VirtualMethod";
}

sealed #

Słówko sealed zaaplikowane do metody powoduje, że nie można jej nadpisywać w podklasie. Nie znaczy to jednak że takiej metody nie można przykryć. Zaaplikowane dla klasy powoduje, że takiej klasy nie można dziedziczyć.

Konstruktory #

Konstruktory nie są dziedziczone. Klasa pochodna musi zdefiniować swój własny zestaw konstruktorów.

Jeżeli pochodna musi także zadbać o inicjalizację klasy bazowej. W konstruktorze klasy pochodnej możemy użyć base, żeby wywołać któryś z konstruktorów klasy bazowej.

public class Base
{
    public int X;
    public Base(int x) => X = x;
}

public class Derived : Base
{
    public int Y;
    public Derived(int x, int y) : base(x) => Y = y;
}

Jeżeli klasa bazowa dostarcza konstruktor bezparametrowy, to możemy pominąć jawne wywołanie konstruktora klasy bazowej, ale niejawnie będzie wywoływany wtedy konstruktor bezparametrowy klasy bazowej.

public class Base
{
    public int X;
    public Base() => X = 1;
}

public class Derived : Base
{
    public int Y;
    public Derived() => Y = 1; // Can skip implicit base call
}

Kolejność inicjalizacji #

  1. Pola i właściwości klasy pochodnej
  2. Pola i właściwości klasy bazowej
  3. Konstruktor klasy bazowej
  4. Konstruktor klasy pochodnej