Czym jest polimorfizm?
Polimorfizm (wielopostaciowość) to jedno z podstawowych założeń dotyczących programowania obiektowego. Wielopostaciowość, to znaczy zapisywanie jednej funkcji (metody) pod różnymi postaciami. To znaczy, dzięki niemu możemy obsługiwać różne typy w różny sposób, bez znajomości tych typów. Polimorfizm można podzielić na 2 grupy, polimorfizm statyczny, a także polimorfizm dynamiczny.
Polimorfizm statyczny
W C# to przeciążanie funkcji oraz operatorów. Chodzi o to, że w 1 klasie może istnieć dużo metod o takiej samej nazwie, lecz różniących się tylko parametrami. Polimorfizm statyczny zachodzi podczas kompilacji programu, to która metoda zostanie wybrana, zależy od ilości, a także typu przekazanych do metody argumentów. Warto wspomnieć, że nie można przeciążyć metody, która różni się tylko zwracanym typem.
Polimorfizm dynamiczny
w C# jest powiązany z funkcjami wirtualnymi i abstrakcyjnymi, jest to tak zwane przesłanianie (nadpisywanie) funkcji. Idea polimorfizmu (dynamicznego) kręci się wokół dziedziczenia klas. Aby przesłonić metodę w klasie bazowej, należy użyć słowa kluczowego virtual, a w klasie podrzędnej override. Zazwyczaj, jeżeli ktoś mówi o polimorfizmie, to ma na myśli właśnie polimorfizm dynamiczny. Przykłady w tym artykule, również będą dotyczyły polimorfizmu dynamicznego.
Zadanie w C#
W ostatnim artykule opisałem Ci, na czym polega dziedziczenie. Polimorfizm (dynamiczny) jest ściśle powiązany właśnie z dziedziczeniem. Spójrz na poniższy przykład, na którym została przedstawiona hierarchia dziedziczenia kilku klas.
public class Shape
{
}
public class Square : Shape
{
}
public class Triangle : Shape
{
}
public class Program
{
static void Main(string[] args)
{
var shapes = new List<Shape>()
{
new Square(),
new Triangle()
};
}
}
Mamy tutaj klasę Shape, po której dziedziczą klasy Square oraz Triangle. Załóżmy, że mamy za zadanie napisać program, który będzie rysował różne kształty figur, które będą w podanej liście shapes.
Złe rozwiązanie (bez polimorfizmu)
Na początek zacznijmy od złego rozwiązania, to znaczy jak mógłby wyglądać taki program bez zastosowania polimorfizmu.
public class Shape
{
}
public class Square : Shape
{
public void DrawSquare()
{
Console.WriteLine("Draw Square");
}
}
public class Triangle : Shape
{
public void DrawTriangle()
{
Console.WriteLine("Draw Triangle");
}
}
public class Program
{
static void Main(string[] args)
{
var shapes = new List<Shape>()
{
new Square(),
new Triangle()
};
foreach (var shape in shapes)
{
if (shape is Square)
(shape as Square).DrawSquare();
else if (shape is Triangle)
(shape as Triangle).DrawTriangle();
}
}
}
//OUTPUT
//Draw Square
//Draw Triangle
Co prawda wynik jest prawidłowy, robi to, co potrzebujemy, ale takie rozwiązanie jest bardzo problematyczne, wprowadza duży bałagan w kodzie. Jeżeli w przyszłości będzie trzeba dodać nową figurę, którą również będziemy chcieli rysować, to metoda Main będzie wtedy musiała zostać zmodyfikowana i potrzebne będą kolejne warunki sprawdzające jakiego typu jest dany obiekt.
Prawidłowe rozwiązanie z zastosowaniem polimorfizmu
Jak w takim razie powinno się prawidłowo zaprojektować te klasy? Należy skorzystać z polimorfizmu.
public class Shape
{
public virtual void Draw()
{
}
}
public class Square : Shape
{
public override void Draw()
{
Console.WriteLine("Draw Square");
}
}
public class Triangle : Shape
{
public override void Draw()
{
Console.WriteLine("Draw Triangle");
}
}
public class Program
{
static void Main(string[] args)
{
var shapes = new List<Shape>()
{
new Square(),
new Triangle()
};
foreach (var shape in shapes)
{
shape.Draw();
}
}
}
//OUTPUT
//Draw Square
//Draw Triangle
W powyższym przykładzie zastosowaliśmy polimorfizm. Jak widzisz, tym razem w metodzie Main nie musimy już sprawdzać typu obiektu. Jeżeli dojdzie jakaś nowa klasa do naszej aplikacji, to również metoda Main nie będzie musiała zostać zmieniona. A teraz spróbuje Ci wyjaśnić, to co zmieniliśmy. Po pierwsze dodaliśmy do klasy Shape nową metodę Draw, jak widzisz, jest to metoda wirtualna, to znaczy oznaczona słowem kluczowym virtual. Gdy oznaczamy metodę jako virtual, to umożliwiamy nadpisanie tej metody klasie podrzędnej (dziedziczącej). Aby w klasie podrzędnej nadpisać taką metodę, musimy użyć słowa kluczowego override, dzięki któremu właśnie nadpisujemy metodę o tej samej definicji własną implementacją. Dzięki temu klasy Square oraz Triangle mają własną nadpisaną implementację metody Draw. Dlatego w metodzie Main, iterując po liście Shapes, za każdym razem, bez sprawdzanie typu, wywoływana jest oczekiwana implementacja. Polimorfizm jest jeszcze bardzo powiązany z klasami i metodami abstrakcyjnymi, dlatego w kolejnym artykule poruszę właśnie ten temat.
PODSUMOWANIE:
Polimorfizm jest to bardzo ważny filar programowania obiektowego, bez którego znajomości, ciężko Ci będzie znaleźć pracę jako młodszy programista C#/.NET. Pokazałem Ci, jak dzieli się polimorfizm oraz jakie ma zastosowanie. Najważniejsze, żebyś zapamiętał słowa kluczowe virtual (do oznaczenia metod, które mogą zostać przesłonięte w klasie pochodnej) oraz override (do oznaczenia metod przesłoniętych). Jeżeli masz jakieś pytania co do tego artykułu, to napisz do mnie maila, lub zostaw komentarz. W kolejnym tygodniu poruszymy temat abstrakcji w programowaniu obiektowym, rozwinę również wtedy temat polimorfizmu.
Poprzedni artykuł - Co To Jest Dziedziczenie w Programowaniu Obiektowym?.
Następny artykuł - Co To Jest Abstrakcja w Programowaniu Obiektowym?.