Zacznijmy od definicji. Metoda wirtualna umożliwia nadpisanie implementacji przez klasę pochodną. Przykładami takich metod dla typu object
są: ToString, Equals oraz GetHashCode. Metody te zostały zaprojektowane w taki sposób, aby umożliwić ich nadpisanie według własnych potrzeb. Dzięki temu, możliwe jest określenie wartości tekstowej reprezentującej obiekt zwracanej przez metodę ToString, czy dostosowanie logiki porównywania przez Equals. Do deklaracji metody wirtualnej służy słowo kluczowe virtual
. Kod poniżej prezentuje zastosowanie takiej metody.
using System;
namespace VirtualAbstractSealed
{
public class Program
{
class Square
{
public int A { get; set; }
public virtual int CalcArea()
{
return A * A;
}
}
class Rectangle : Square
{
public int B { get; set; }
public override int CalcArea()
{
return A * B;
}
}
static int GetArea(Square square)
{
return square.CalcArea();
}
static void Main(string[] args)
{
var r = new Rectangle() { A = 10, B = 20 };
Console.WriteLine($"Area: {GetArea(r)}");
Console.ReadLine();
}
}
}
Wywołanie metody wirtualnej nie wyróżnia się niczym specjalnym. Różnica polega na tym, że metoda wirtualna może posiadać bardziej szczegółową implementację. W tym przykładzie, statyczna metoda GetArea pobiera argument typu Square
, a następnie wywołuje na nim metodę CalcArea. Zgodnie z oczekiwaniem, wykonana zostanie metoda CalcArea typu Rectangle
, gdyż instancja tego typu została przekazana do metody GetArea.
Zgodnie z powyższym, metoda wybrana do realizowania określana jest na podstawie instancji przekazanego typu, z racji czego niemożliwe jest oznaczenie metody statycznej jako wirtualnej.
W celu nadpisania metody wirtualnej należy skorzystać ze słowa kluczowego override
. Oczywiście nie ma takiego obowiązku. W sytuacji, gdy klasa pochodna nie będzie posiadała własnej implementacji metody wirtualnej, wywołana zostanie domyślna implementacja z klasy bazowej. Przeprowadź test, usuwając metodę CalcArea dla typu Rectangle
.
Dobra, a co w sytuacji, gdy nie usuniemy metody CalcArea klasy Rectangle, a jedynie słowo kluczowe override
? Kompilator zgłosi ostrzeżenie, takie jak poniżej. Ostrzeżenie to nie przeszkadza w kompilacji. Sprawdźmy więc, która z metod CalcArea zostanie wywołana przez metodę GetArea.
Wykonana zostanie metoda CalcArea klasy bazowej Square
. W tym przypadku nie następuje przysłonięcie metody, a jej ukrycie przez typ pochodny. Dostęp do metody klasy bazowej możemy uzyskać wyłącznie poprzez wywołanie metody za pośrednictwem typu bazowego, tak jak to robi metoda GetArea.
Kolizje nazw powodują generowanie ostrzeżeń przez kompilator. Powodem jest brak jednoznaczności w określeniu oczekiwanego działania. Być może po prostu zapomnieliśmy o użyciu słowa kluczowego override
. Innym powodem może być aktualizacja wykorzystywanej biblioteki, w której pojawiła się nowa wirtualna metoda. W takiej sytuacji najlepiej będzie zmienić nazwę metody, tak aby nie kolidowała z nowo dodaną metodą. Jednak, jeżeli z jakichś powodów nie możemy skorzystać z tego rozwiązania, C# umożliwia użycie słowa kluczowego new
, aby jawnie określić, że decydujemy się na ukrycie metody z klasy bazowej. W takiej sytuacji kod będzie działał identycznie, jednak kompilator nie wygeneruje ostrzeżenia. Według mojej oceny, brak reakcji i pozostawienie ostrzeżenia może doprowadzić w późniejszym czasie do niepotrzebnego zamieszania.
Istnieje jednak inna specyficzna sytuacja, w której celowo używamy new
. Tą sytuacją jest chęć zmiany sygnatury metody. Przykładem takiego działania jest metoda Add interfejsu ISet<T>
, która, w przeciwieństwie do bazowej metody interfejsu ICollection<T>
, zwraca typ bool
zamiast void
. Metoda Add interfejsu ISet<T>
informuje w ten sposób, czy dany element znajdował się wcześniej w zbiorze.
Przekazane informacje odnośnie metod wirtualnych dotyczą również właściwości, które tak naprawdę są metodami. Dzięki temu możliwe jest tworzenie wirtualnych właściwości oraz ich przysłanianie i ukrywanie. To samo tyczy się zdarzeń, które również są ukrytymi metodami. Notatka opisująca zdarzenia znajduje się tutaj.
Może zdarzyć się sytuacja, w której będziemy chcieli przysłonić metodę wirtualną, a następnie uniemożliwić jej ponowne przysłonięcie w kolejnych klasach pochodnych. W tym celu należy użyć słowa kluczowego sealed
, które powoduje określenie metody jako ostatecznej. Metody ostateczne są przeciwieństwem metod wirtualnych. Oprócz zamknięcia metody możliwe jest określenie klas jako ostatecznych. Przykład poniżej pokazuje, w jaki sposób zamknąć klasę Rectangle wraz z metodą CalcArea.
sealed class Rectangle : Square
{
public int B { get; set; }
public sealed override int CalcArea()
{
return A * B;
}
}
Język C# umożliwia zdefiniowanie metody wirtualnej nieposiadającej domyślnej implementacji. Taką metodę nazywamy abstrakcyjną. Jej definicja ogranicza się do podania sygnatury. Klasa posiadająca metodę abstrakcyjną, określana jest jako niekompletna. Takie klasy również stają się abstrakcyjne i co najważniejsze, nie istnieje możliwość stworzenia ich instancji. Tworząc klasę abstrakcyjną, należy użyć słowa kluczowego abstract
, w przeciwnym razie kompilator zgłosi błąd. Dziedzicząc po klasie abstrakcyjnej, nie mamy konieczności implementacji wszystkich jej abstrakcyjnych składowych; pochodna klasy stanie się również abstrakcyjna.
Metody abstrakcyjne, podobnie jak interfejsy, ograniczają się do podania sygnatury. Jednak w przypadku metod abstrakcyjnych możliwe jest ustalenie modyfikatorów dostępności. Nie możliwe jest określenie prywatnej metody abstrakcyjnej, gdyż nie będzie ona dostępna dla klas pochodnych.
Możliwe jest określenie klasy abstrakcyjnej bez tworzenia metod abstrakcyjnych. W ten sposób możemy zablokować możliwość tworzenia instancji takiej klasy.
Warto również zapamiętać, że klasy abstrakcyjne mogą korzystać z interfejsów, ograniczając się do podania sygnatury, bez potrzeby pełnej implementacji. Pokazuje to przykład poniżej.
interface IArea
{
int CalcArea();
}
abstract class Shape : IArea
{
public abstract int CalcArea();
}
class Square : Shape
{
public int A { get; set; }
public override int CalcArea()
{
return A * A;
}
}
class Rectangle : Square
{
public int B { get; set; }
public override int CalcArea()
{
return A * B;
}
}
Przejdźmy do dostępności składowych klasy bazowej. Wszystko, co nie zostało określone jako prywatne, dostępne jest w klasie pochodnej. W przykładzie poniżej, pole IsCalculated zostało określone jako prywatne, przez co nie jest dostępne zarówno w klasie Rectangle, jak i na zewnątrz. Natomiast, w przypadku właściwości LastCalcArea, możliwy jest dostęp z klasy Rectangle. W większości przypadków, jeżeli będziemy chcieli odnieść się do składowej klasy bazowej, możemy to zrobić w taki sam sposób, jak do składowej własnej klasy. Możemy również posłużyć się referencją this
. Czasem jednak będziemy potrzebowali jawnie określić, że chcemy odwołać się do składowej klasy bazowej. Takim przypadkiem może być potrzeba wywołania metody, która została przysłonięta. W tym celu musimy skorzystać ze słowa kluczowego base
. Zwróć uwagę, że usunięcie base
spowodowałoby rekurencyjne wywołanie metody PrintSize. Metoda PrintSize klasy Rectangle korzysta z base
przed wykonaniem własnego bloku. Nic jednak nie stoi na przeszkodzie, aby wykonanie metody klasy bazowej zostało wykonane w innym miejscu. Możliwe jest również wielokrotne wywołanie metody bazowej.
using System;
namespace VirtualAbstractSealed
{
public class Program
{
class Square
{
public int A { get; set; }
private static bool IsCalculated = false;
protected int LastCalcArea { get; set; }
public virtual int CalcArea()
{
IsCalculated = true;
LastCalcArea = A * A;
return LastCalcArea;
}
public virtual void PrintSize()
{
Console.WriteLine($"A: {A}");
}
}
sealed class Rectangle : Square
{
public int B { get; set; }
public sealed override int CalcArea()
{
this.LastCalcArea = A * B;
return this.LastCalcArea;
}
public override void PrintSize()
{
base.PrintSize();
Console.WriteLine($"B: {B}");
}
}
static void Main(string[] args)
{
var r = new Rectangle() { A = 10, B = 20 };
r.PrintSize();
Console.ReadLine();
}
}
}
Notatka opisuje mechanizmy umożliwiające sterowanie wywołaniem metod przez klasy pochodne. Jest to kolejny wpis dotyczący tematyki obiektowości. W kolejnym wpisie znalazły się informacje dotyczące sposobów wywoływania konstruktorów. Link tutaj.
Troska Robert