CodeGym /Kursy /C# SELF /Przeciążanie metod (Method Overloading)

Przeciążanie metod (Method Overloading)

C# SELF
Poziom 21 , Lekcja 1
Dostępny

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) { ... }
Przeciążanie tylko po typie zwracanym jest niemożliwe!

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
WriteLine()
Wypisuje pusty wiersz
WriteLine(string)
Wypisuje tekst
WriteLine(int)
Wypisuje liczbę całkowitą
WriteLine(double)
Wypisuje liczbę zmiennoprzecinkową
WriteLine(string, object)
Formatuje tekst z jednym argumentem
WriteLine(string, params object[])
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?
greeter.Greet()
void Greet()
greeter.Greet(75)
void Greet(int)

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}");
}
Metoda ze zmienną liczbą parametrów ( params)

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;
}
Przeciążanie po modyfikatorze ref

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) |
    +-----------------------+
Schemat przeciążania metod w klasie

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.

Komentarze
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION