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 Hider
są przykrywane. 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 #
- Pola i właściwości klasy pochodnej
- Pola i właściwości klasy bazowej
- Konstruktor klasy bazowej
- Konstruktor klasy pochodnej