Przeładowania metod to podstawy języka. Niestety, nieumiejętnie stosowane, mogą przystworzyć problemów nawet zaawansowanym programistom. Z tego względu, uważam, że należy po prostu unikać tych przeładować, które są zbyt trudne w zrozumieniu – powodują niepotrzebne zamieszanie.
Zacznijmy od klasycznego przykładu, który jest zrozumiały dla każdego:
private static void Main(string[] args) { Display("Hello World"); Display(5); } private static void Display(string text) { Console.WriteLine("string"); } private static void Display(int number) { Console.WriteLine("int"); }
Nie ma tutaj żadnej zagadki – pierwsze wywołanie wykona funkcje z parametrem string a drugie z int. Nieco mniej zrozumiałą konstrukcją może być:
private static void Main(string[] args) { Display(5); } private static void Display(double arg) { Console.WriteLine("double"); } private static void Display(Int16 arg) { Console.WriteLine("int16"); } private static void Display(int arg) { Console.WriteLine("int"); } private static void Display(float arg) { Console.WriteLine("float"); }
Która funkcja zostanie wywołana? Teoretycznie każda z nich spełnia wymagania ponieważ 5 może być skonwertowane zarówno do int16 jak i int, float, double czy long. C# zawsze będzie starał się dobrać najlepsze dopasowanie. Mamy tutaj do czynienia z całkowitą liczbą, stąd int16,int,long są lepszymi kandydatami niż zmiennoprzecinkowe (skomplikowane) zmienne typu double czy float.
I tutaj mała pułapka… Może wydawać się, że int16 jest lepszy niż int ponieważ zajmuje mniej pamięci, a żeby pomieścić cyfrę 5, 16 bitów w zupełności wystarcza. Deklarując jednak każdą zmienne, możemy określić jej typ za pomocą specyfikatorów, a mianowicie (źródło, stackoverflow):
var d = 1.0d; // double var f = 1.0f; // float var m = 1.0m; // decimal var i = 1; // int var ui = 1U; // uint var ul = 1UL; // ulong var l = 1L; // long
Stąd wynika, że jak przekażemy liczbę całkowitą, bez specyfikatora, zostanie użyty typ Int32. Powinno być już jasne, która metoda zostanie wykonana, gdy wywołamy:
Display(5.00);
Będzie to oczywiście wersja z Double – aby wywołać float musielibyśmy napisać 5.00f.
A co jeśli mamy następujący kod?
internal class Program { private static void Main(string[] args) { Display(5.00); } private static void Display(float arg) { Console.WriteLine("float"); } }
Wywoła to błąd kompilacji – 5.00 to double a nie float – nie ma tutaj niejawnej konwersji. Natomiast, niejawna konwersja ma miejsce z typu mniej dokładnego do bardziej, tzn.:
internal class Program { private static void Main(string[] args) { Display(5.00f); } private static void Display(double arg) { Console.WriteLine("double"); } }
Widać, że jest trochę tutaj niespodzianek. To jednak dopiero początek. C# naprawdę potrafi zaskoczyć, jeśli chodzi o wywoływanie metod. Powyższe pułapki są mimo wszystko logiczne i wiadomo, czego można spodziewać się.
Rozważmy następujący przykład:
internal class Program { private static void Main(string[] args) { Child child = new Child(); child.Display((int) 5); } } internal class Parent { public void Display(int arg) { Console.WriteLine("int"); } } internal class Child : Parent { public void Display(double arg) { Console.WriteLine("double"); } }
Child dziedziczy po Parent. Nie mamy tutaj do czynienia z method hiding, ponieważ sygnatury metod są różne od siebie. Jest to wciąż klasyczny przykład przeładowywania metod. Normalnie, Display z Integer zostałby wywołany, ponieważ jest to lepsze dopasowanie. C# jednak w przypadku dziedziczenia, zawsze weźmie najpierw pod uwagę metodę znajdującą się bliżej w hierarchii klas. Operujemy na Child, dlatego najpierw będzie C# próbował znaleźć dopasowanie w Child, a dopiero potem w Parent. Z tego względu Display(double) wygrywa i zostanie wykona.
Jest to poważna pułapka i osobiście unikam takiego kodu. Wystarczy, że zmienimy wskaźniki na:
private static void Main(string[] args) { Parent child = new Child(); child.Display((int) 5); }
I w takim przypadku, wersja z Int zostanie wywołania a nie z double, jak powyżej.
Kolejną pułapką są wirtualne metody i ich przeciążenia:
internal class Program { private static void Main(string[] args) { Child child = new Child(); child.Display((int) 5); } } internal class Parent { public virtual void Display(int arg) { Console.WriteLine("int"); } } internal class Child : Parent { public override void Display(int arg) { Console.WriteLine("override int"); } public void Display(double arg) { Console.WriteLine("double"); } }
Korzystamy ze wskaźnika Child, zatem najpierw próbujemy znaleźć dopasowanie w Child. Mamy przeciążoną metodę Display(int) i logiczna odpowiedź brzmiałaby, że “override int” zostanie wywołany. Twórcy C# jednak, zdecydowali się ignorować przeciążenia i w pierwszej kolejności zostaną rozpatrzone wszystkie inne metody w Child – w tym przypadku będzie to Display(double)!
W przyszłym poście, kolejne dziwne przeładowania wykorzystujące dziedziczenie – to właśnie one powodują największe problemy i powinny być unikane.