Kilka ciekawostek z przeładowywania metod, część I

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.

Leave a Reply

Your email address will not be published.