1. Wprowadzenie
W prawdziwym życiu wiele akcji to trochę jak szwajcarski scyzoryk: ta sama komenda może działać z różnymi narzędziami. Wyobraź sobie bankomat: jeśli wsadzisz kartę — bankomat pyta o PIN; jeśli wpiszesz numer telefonu — czeka na kod z SMS-a. Akcja jedna — "sprawdź użytkownika", ale sposoby różne.
W programowaniu często mamy podobną sytuację: trzeba wykonać niby jedną operację, ale dane mogą być różnych typów albo mieć różną liczbę parametrów. Na przykład nasza metoda ma wyświetlić powitanie zarówno dla człowieka, jak i dla zwierzaka, albo zsumować dwa, trzy, a nawet dziesięć liczb całkowitych.
Jasne, można by nazwać metody jakoś inaczej: SumTwo, SumThree, SumArray. Ale programiści są leniwi (nie bez powodu mówią, że lenistwo to motor postępu). Poza tym kod robi się wtedy mniej czytelny.
Przeciążanie metody
Przeciążanie metod — to sposób, żeby "zmusić" jedną i tę samą metodę do pracy z różnymi zestawami parametrów, bez zmiany jej nazwy. To jedna z form polimorfizmu, ale nie związana z dziedziczeniem.
Przeciążanie metody — to możliwość tworzenia w jednej klasie (albo strukturze) kilku metod o tej samej nazwie, ale z różnymi listami parametrów (typ, ilość i/lub kolejność).
Sygnatura metody
Sygnatura metody w C# — to jej nazwa plus typ(y) i kolejność parametrów. Typ zwracany przez metodę nie wchodzi w skład sygnatury! To często prowadzi do nieoczekiwanych błędów (o tym za chwilę).
2. Przeciążanie w akcji: proste przykłady
Stwórzmy klasę Greeter, która będzie witać użytkowników na różne sposoby: tylko po imieniu, po imieniu i wieku, albo w ogóle bez parametrów.
public class Greeter
{
// Powitanie bez parametrów
public void Greet()
{
Console.WriteLine("Cześć, świecie!");
}
// Powitanie z imieniem
public void Greet(string name)
{
Console.WriteLine($"Cześć, {name}!");
}
// Powitanie z imieniem i wiekiem
public void Greet(string name, int age)
{
Console.WriteLine($"Cześć, {name}! Masz już {age} lat? Nieźle!");
}
}
Teraz możesz wywołać dowolną z tych metod, a kompilator C# sam wybierze odpowiednią wersję — patrząc na ilość i typy przekazanych parametrów.
var greeter = new Greeter();
greeter.Greet(); // Cześć, świecie!
greeter.Greet("Ania"); // Cześć, Ania!
greeter.Greet("Piotr", 23); // Cześć, Piotr! Masz już 23 lat? Nieźle!
3. Różnica według typu i ilości parametrów
Przeciążanie działa, jeśli metody różnią się:
- ilością parametrów,
- typem przynajmniej jednego parametru,
- kolejnością typów parametrów (ale tu trzeba uważać).
Spróbujmy dodać jeszcze jedno przeciążenie, które przyjmuje tylko wiek:
public void Greet(int age)
{
Console.WriteLine($"Tyle lat — to jest sztos! ({age} lat)");
}
Teraz wywołania:
greeter.Greet(10); // Tyle lat — to jest sztos! (10 lat)
Ważne: jeśli metody różnią się tylko typem zwracanym, nie da się ich przeciążyć. Na przykład taki kod wywali błąd:
// Błąd kompilacji!
public int Foo(string s) { ... }
public double Foo(string s) { ... }
Kompilator się obrazi: "Metoda Foo(string) już jest zdefiniowana, wymyśl coś trudniejszego!"
4. Przeciążanie i standardowa biblioteka C#
Przeciążanie to nie tylko nasz wymyślony Greet. Otwórz dokumentację .NET dla Console.WriteLine:
| Sygnatura | Zastosowanie |
|---|---|
|
Wypisuje pusty wiersz |
|
Wypisuje tekst |
|
Wypisuje liczbę całkowitą |
|
Wypisuje liczbę zmiennoprzecinkową |
|
Formatuje tekst z jednym argumentem |
|
Formatuje z wieloma argumentami |
To wszystko są przeciążenia tej samej metody — WriteLine. Teraz już wiesz, czemu zawsze możesz zrobić:
Console.WriteLine("Po prostu tekst");
Console.WriteLine(123);
Console.WriteLine(2.5);
Console.WriteLine("Suma: {0}", 42);
I kompilator zawsze ogarnia, o co Ci chodzi!
5. Jak kompilator wybiera, które przeciążenie wywołać?
Tu jest twardo: patrzy na typy i ilość faktycznie przekazanych argumentów. Mała tabelka dla jasności:
| Wywołanie | Jaka wersja zadziała? |
|---|---|
|
|
|
|
Co jeśli będzie niejednoznaczność?
Czasem sytuacja wymyka się spod kontroli. Przykład niejednoznacznego przeciążenia — kompilator nie będzie wiedział, którą wersję wybrać
public void Print(int a, double b) { ... }
public void Print(double a, int b) { ... }
printer.Print(5, 10);
// Błąd: niejednoznaczność — którą Print wywołać? (obie niby pasują)
Kompilator wywali błąd niejednoznaczności. W takich przypadkach lepiej unikać przeciążeń z tą samą ilością parametrów i podobnymi typami, bo można zmylić kompilator.
6. params — zmienna liczba parametrów
Załóżmy, że chcesz zrobić metodę, która przyjmuje nieokreśloną liczbę liczb. Tu przyda się słówko kluczowe params.
public void SumAll(params int[] numbers)
{
int sum = 0;
foreach (int n in numbers)
sum += n;
Console.WriteLine($"Suma: {sum}");
}
Teraz możesz wywołać:
SumAll(1, 2, 3); // Suma: 6
SumAll(10, 20); // Suma: 30
SumAll(); // Suma: 0
Metody z params można łączyć z przeciążaniem, ale najważniejsze, żeby nie robić takich przeciążeń, które utrudnią kompilatorowi jednoznaczne rozpoznanie, o którą wersję Ci chodziło.
7. Przeciążanie i modyfikatory parametrów (ref, out, in)
C# rozróżnia metody po modyfikatorach parametrów (czyli sygnatura void Foo(int a) różni się od void Foo(ref int a), i obie mogą być w jednej klasie):
public void SetValue(int a)
{
a = 42;
}
public void SetValue(ref int a)
{
a = 100;
}
Wywołanie bez ref trafi do pierwszej wersji, z ref — do drugiej:
int n = 5;
SetValue(n); // n zostaje 5 (przekazywana jest kopia)
SetValue(ref n); // n staje się 100
8. Schemat: czym jest przeciążanie
+----------+
| MyClass |
+----------+
|
| (fragment metod)
+-----------------------+
| void Foo() |
| void Foo(int a) |
| void Foo(string s) |
| void Foo(int a, int b) |
+-----------------------+
A jeśli pokazać to w kodzie:
// Wywołujemy przeciążone wersje metody Foo():
var mc = new MyClass();
mc.Foo(); // void Foo()
mc.Foo(5); // void Foo(int)
mc.Foo("Hello"); // void Foo(string)
mc.Foo(2, 3); // void Foo(int, int)
9. Przykład: przeciążamy metody w naszej aplikacji
Rozwijamy dalej naszą edukacyjną apkę, dodając przeciążenie metody w hierarchii zwierzaków.
public class Animal
{
public string Name { get; set; }
// Metoda do wydawania dźwięku
public virtual void MakeSound()
{
Console.WriteLine("Jakiś niezrozumiały dźwięk...");
}
// Przeciążona metoda: dźwięk z podaną głośnością
public void MakeSound(int volume)
{
Console.WriteLine($"Zwierzak wydaje dźwięk o głośności {volume} dB.");
}
}
public class Dog : Animal
{
public override void MakeSound()
{
Console.WriteLine("Hau!");
}
// Przeciążona metoda: szczekanie z głośnością
public void MakeSound(int volume)
{
Console.WriteLine($"Hau! (głośność: {volume} dB)");
}
}
Spróbuj takich wywołań:
Dog rex = new Dog();
rex.MakeSound(); // Hau!
rex.MakeSound(75); // Hau! (głośność: 75 dB)
Zwróć uwagę: w klasie potomnej (Dog) przeciążyliśmy metodę MakeSound(int volume), i teraz są dwie wersje: z parametrem i bez.
10. Typowe błędy przy przeciążaniu metod
Błąd nr 1: próba przeciążenia tylko po typie zwracanym.
To niemożliwe — typ zwracany nie wchodzi w skład sygnatury metody. Przeciążenie musi się różnić ilością lub typami parametrów wejściowych, a nie void czy int.
Błąd nr 2: niejednoznaczne przeciążenia, które mylą kompilator.
Przeciążenia z tą samą liczbą parametrów i podobnymi typami (np. int i double) mogą zmylić kompilator. Przykład: Print(int a, double b) i Print(double a, int b) — wywołanie Print(1, 1) wywoła błąd niejednoznaczności.
Błąd nr 3: konflikt params z innymi przeciążeniami.
Metoda z params może przechwycić wywołanie, które miało trafić do innego przeciążenia. Jeśli typy się pokrywają, kompilator może wybrać nie tę metodę, którą chciałeś.
Błąd nr 4: zapomnienie, że ref i out wchodzą w sygnaturę.
Metody Do(ref int x) i Do(out int x) są traktowane jako różne przeciążenia. Jeśli o tym zapomnisz, łatwo się pomylić i wywołać złą wersję metody.
GO TO FULL VERSION