Parametry, przeciążanie i przekazywanie danych

Czy metoda może zmienić zmienną z Main? Odpowiedź brzmi „normalnie nie, ale da się to wymusić” — i właśnie od tego zależy, dlaczego int.TryParse wygląda tak, jak wygląda.

C# ref / out przeciążanie 50 min
CEL LEKCJI

Czego się dziś nauczysz

  • Wyjaśnisz, dlaczego metoda pracuje na kopii przekazanej wartości
  • Użyjesz ref, out i in we właściwych sytuacjach
  • Nadasz parametrom wartości domyślne i wywołasz metodę argumentem nazwanym
  • Napiszesz przeciążenia metody i wyjaśnisz, czym mogą się różnić
  • Powiesz, gdzie sięga zmienna zadeklarowana w metodzie

Przygotowanie: lekcje 01–19. Przewidywany czas: 45–90 minut z zadaniami. Przykłady wymagają .NET 8 lub nowszego, z włączonymi ImplicitUsings i Nullable.

TEORIA

Metoda dostaje kopię, nie zmienną

Parametr metody to osobna zmienna, która przy wywołaniu dostaje kopię przekazanej wartości. Zmiana parametru w środku metody nie dotyka zmiennej z miejsca wywołania.

Program.cs
static void Wyzeruj(int liczba)
{
    liczba = 0;                  // zmienia TYLKO lokalna kopie
    Console.WriteLine($"w metodzie: {liczba}");
}

static void Main()
{
    int punkty = 18;
    Wyzeruj(punkty);
    Console.WriteLine($"w Main:     {punkty}");
}
wynik w konsoli
w metodzie: 0
w Main:     18

To samo dotyczy napisów, znaków, liczb rzeczywistych i wartości logicznych — czyli wszystkiego, co poznałeś do tej pory.

Jak więc oddać wynik na zewnątrz

Normalną drogą jest return: metoda liczy i zwraca wartość, a wywołujący przypisuje ją do zmiennej. To najczytelniejszy sposób i pierwszy, po który sięgasz. Dwa słowa opisane niżej — ref i out — są wyjątkami na sytuacje, w których jeden zwracany wynik nie wystarcza.

TEORIA

ref, out i in — kiedy metoda sięga do zmiennej

ref — praca na tej samej zmiennej

Słowo ref mówi: „nie kopiuj, daj dostęp do oryginału”. Piszemy je dwa razy — w deklaracji metody i przy wywołaniu.

Program.cs
static void Zwieksz(ref int liczba)
{
    liczba = liczba + 1;
}

static void Main()
{
    int licznik = 5;
    Zwieksz(ref licznik);
    Console.WriteLine(licznik);   // 6
}

Zmienna przekazywana przez ref musi mieć wartość przed wywołaniem — metoda ma prawo ją odczytać.

out — metoda ma coś zapisać

out działa podobnie, ale w jedną stronę: metoda musi przypisać wartość do takiego parametru, zanim się zakończy. Zmienna przed wywołaniem może być pusta.

Program.cs
static void PodzielZReszta(int a, int b, out int calosc, out int reszta)
{
    calosc = a / b;
    reszta = a % b;
}

static void Main()
{
    PodzielZReszta(17, 5, out int c, out int r);
    Console.WriteLine($"{c} reszty {r}");   // 3 reszty 2
}

Znasz to z lekcji 11: int.TryParse(tekst, out int liczba) zwraca bool — czy się udało — a samą liczbę oddaje przez out. Teraz wiesz, dlaczego tak wygląda.

in — tylko do odczytu

Program.cs
static double Pole(in double bok)
{
    // bok = 5;   // BLAD: parametru in nie wolno zmieniac
    return bok * bok;
}
SłowoPrzed wywołaniemW metodzieTypowe użycie
(brak)musi mieć wartośćzmiany lokalne, niewidoczne na zewnątrzdomyślny wybór
refmusi mieć wartośćodczyt i zapis oryginałuzamiana dwóch zmiennych miejscami
outmoże być pustatrzeba przypisać wartośćdrugi wynik obok bool, jak w TryParse
inmusi mieć wartośćtylko odczytduże struktury, gdy kopiowanie kosztuje

Nie nadużywaj ref i out

Metoda z trzema parametrami out jest trudniejsza do czytania niż metoda zwracająca jedną sensowną wartość. Gdy potrzebujesz kilku wyników naraz, to zwykle znak, że powinny trafić do jednego obiektu — a klasy poznasz w lekcji 37.

TEORIA

Parametry opcjonalne i argumenty nazwane

Program.cs
static void Zamow(string produkt, int sztuki = 1, bool pilne = false)
{
    Console.WriteLine($"{produkt} x{sztuki}, pilne: {pilne}");
}

static void Main()
{
    Zamow("mysz");                          // mysz x1, pilne: False
    Zamow("klawiatura", 3);                 // klawiatura x3, pilne: False
    Zamow("monitor", 2, true);              // monitor x2, pilne: True
    Zamow("kabel", pilne: true);            // kabel x1, pilne: True
}
  • Parametry z wartością domyślną muszą stać na końcu listy — inaczej kompilator nie wiedziałby, który argument pominięto.
  • Wartość domyślna musi być stałą znaną przy kompilacji: liczba, napis, true, null.
  • Argument nazwany (pilne: true) pozwala pominąć środkowy parametr i przy okazji czyni wywołanie czytelnym. Porównaj Zamow("monitor", 2, true) z Zamow("monitor", sztuki: 2, pilne: true).
TEORIA

Ta sama nazwa, różne listy parametrów

Przeciążenie to kilka metod o tej samej nazwie, różniących się liczbą albo typami parametrów. Kompilator wybiera właściwą po argumentach wywołania.

Program.cs
static int Pole(int bok)                    => bok * bok;
static int Pole(int szerokosc, int dlugosc) => szerokosc * dlugosc;
static double Pole(double promien)          => 3.14159 * promien * promien;

Console.WriteLine(Pole(3));        // 9    - kwadrat
Console.WriteLine(Pole(3, 4));     // 12   - prostokat
Console.WriteLine(Pole(2.0));      // 12,56636 - kolo

Zapis => to skrócona postać metody składającej się z jednej instrukcji return. Znaczy dokładnie to samo co { return bok * bok; }.

Sam typ wyniku nie wystarczy

Dwie metody int Licz(int x) i double Licz(int x) to błąd CS0111. Kompilator wybiera przeciążenie po argumentach, a nie po tym, do czego przypiszesz wynik — przy takiej parze nie miałby jak zdecydować.

Przeciążanie znasz z praktyki: Console.WriteLine przyjmuje liczbę, napis, znak i wartość logiczną, bo istnieje kilkanaście jego przeciążeń.

Para metodPoprawne przeciążenie?
Pole(int) i Pole(int, int)tak — różna liczba parametrów
Pole(int) i Pole(double)tak — różne typy
int Pole(int) i double Pole(int)nie — CS0111, różni je tylko typ wyniku
Pole(int a) i Pole(int b)nie — nazwy parametrów nie mają znaczenia
Pole(int) i Pole(int, int b = 1)kompiluje się, ale Pole(5) pasuje do obu — unikaj
TEORIA

Gdzie sięga zmienna

Zmienna zadeklarowana w metodzie istnieje tylko w niej i znika po jej zakończeniu. Dwie metody mogą mieć zmienne o tej samej nazwie — to dwie różne zmienne.

Program.cs
static void Pierwsza()
{
    int licznik = 1;
    Console.WriteLine(licznik);
}

static void Druga()
{
    int licznik = 100;      // inna zmienna, ta sama nazwa - to w porzadku
    Console.WriteLine(licznik);
    // Console.WriteLine(punkty);  // BLAD: 'punkty' nie istnieje w tej metodzie
}

static void Main()
{
    int punkty = 5;
    Pierwsza();
    Druga();
}

Dlaczego to dobrze

Gdyby metody widziały nawzajem swoje zmienne, zmiana nazwy w jednej mogłaby zepsuć drugą. Zamknięcie zmiennych w metodzie sprawia, że każdą można czytać i poprawiać osobno — po to właśnie dzielimy program na metody.

PRZYKŁAD

Przykład: kalkulator zamówień

Program.cs
class Program
{
    static double Netto(double cena, int sztuki = 1)
    {
        return cena * sztuki;
    }

    static double Netto(double cena, int sztuki, double rabatProcent)
    {
        return Netto(cena, sztuki) * (1 - rabatProcent / 100);
    }

    static void ZBrutto(double netto, out double vat, out double brutto)
    {
        vat = netto * 0.23;
        brutto = netto + vat;
    }

    static void Zamien(ref double a, ref double b)
    {
        double pomoc = a;
        a = b;
        b = pomoc;
    }

    static void Main()
    {
        double bez = Netto(120.0, 3);
        double zRabatem = Netto(120.0, 3, 10);

        Console.WriteLine($"bez rabatu : {bez:F2}");
        Console.WriteLine($"z rabatem  : {zRabatem:F2}");

        ZBrutto(zRabatem, out double vat, out double brutto);
        Console.WriteLine($"VAT        : {vat:F2}");
        Console.WriteLine($"brutto     : {brutto:F2}");

        double tania = 10, droga = 990;
        Zamien(ref tania, ref droga);
        Console.WriteLine($"po zamianie: {tania:F2} i {droga:F2}");
    }
}
wynik w konsoli
bez rabatu : 360,00
z rabatem  : 324,00
VAT        : 74,52
brutto     : 398,52
po zamianie: 990,00 i 10,00

Co dzieje się po kolei

  • Dwa przeciążenia Netto różnią się liczbą parametrów. Drugie wywołuje pierwsze zamiast powtarzać mnożenie — poprawka wzoru wystarczy w jednym miejscu.
  • Netto(120.0, 3) korzysta z jawnie podanej liczby sztuk; Netto(120.0) użyłoby wartości domyślnej 1.
  • ZBrutto oddaje dwa wyniki — dlatego out, a nie return. Zapis out double vat deklaruje zmienną wprost w wywołaniu.
  • Zamien to podręcznikowe zastosowanie ref: bez niego zamiana zaszłaby tylko w lokalnych kopiach i nic nie byłoby widać.
ELEMENTY WBUDOWANE

Zestawienie elementów

ElementZnaczenieOgraniczenia
refdostęp do zmiennej wywołującegoSłowo piszemy w deklaracji i przy wywołaniu; zmienna musi mieć wcześniej wartość.
outwynik oddawany przez parametrMetoda musi przypisać wartość przed każdym return.
inprzekazanie bez prawa zmianyWewnątrz metody parametru nie wolno modyfikować.
int x = 1 w liście parametrówwartość domyślnaTylko na końcu listy; musi być stałą znaną przy kompilacji.
nazwa: wartośćargument nazwanyPozwala pominąć parametry opcjonalne i uczytelnić wywołanie.
przeciążeniekilka metod o tej samej nazwieMuszą różnić się liczbą albo typami parametrów; sam typ wyniku nie wystarczy (CS0111).
=> wyrażenieskrócony zapis metodyDla ciała złożonego z jednej instrukcji. Równoważne { return …; }.
int.TryParse(t, out x)boolPrzykład z biblioteki: wynik logiczny plus wartość przez out.
CZĘSTE BŁĘDY

Zanim utkniesz

ZapisProblem
Zwieksz(licznik) przy ref intCS1620 — słowo ref trzeba powtórzyć przy wywołaniu.
ref na zmiennej bez wartościCS0165 — zmienna musi być przypisana przed wywołaniem.
out bez przypisania w metodzieCS0177 — parametr out musi dostać wartość na każdej ścieżce.
Parametr domyślny nie na końcuCS1737 — po parametrze opcjonalnym nie może stać wymagany.
Przeciążenie różniące się tylko wynikiemCS0111 — kompilator nie ma po czym wybrać.
Oczekiwanie, że metoda zmieni zwykły parametrBez ref metoda pracuje na kopii. Zwróć wynik przez return.

Co dalej z metodami

Metoda może wywołać nie tylko inną metodę, ale również samą siebie. Ten sposób rozwiązywania zadań poznasz w lekcji 21.

ZADANIA

Zadania

ZAD 1Kopia czy oryginał★☆☆

Napisz metodę void Wyzeruj(int liczba) i wywołaj ją dla zmiennej o wartości 42. Wypisz zmienną przed i po wywołaniu, a potem dopisz ref w obu miejscach i porównaj wyniki.

ZAD 2Dwa pola★☆☆

Napisz przeciążenia Pole(int bok) oraz Pole(int szerokosc, int dlugosc). Sprawdź: Pole(3) daje 9, a Pole(3, 4) — 12. Dodaj trzecie przeciążenie dla koła i wyjaśnij, dlaczego nie koliduje z pozostałymi.

ZAD 3Zamiana miejscami★★☆

Napisz Zamien(ref string a, ref string b) i sprawdź ją na dwóch imionach. Następnie usuń oba słowa ref, uruchom ponownie i zapisz, co się zmieniło.

ZAD 4Dzielenie z resztą★★☆

Napisz void PodzielZReszta(int a, int b, out int calosc, out int reszta). Sprawdź dla 17 i 5 oraz dla 4 i 9. Zastanów się, co powinno się stać dla b równego zero — rozwiązanie poznasz w lekcji 35.

ZAD 5Zamówienie z opcjami★★☆

Napisz Zamow(string produkt, int sztuki = 1, bool pilne = false), wypisującą podsumowanie. Wywołaj ją czterokrotnie: z samą nazwą, z liczbą sztuk, ze wszystkimi argumentami i z pominięciem środkowego przy użyciu argumentu nazwanego.

ZAD 6Kalkulator ocen★★★

Napisz komplet metod: double Srednia(double a, double b) i przeciążenie dla trzech ocen, string Slownie(double srednia) zwracającą opis, oraz void Podsumuj(double srednia, out double doPiatki, out bool zdane). Ułóż z nich program wypisujący pełne podsumowanie dla trzech ocen. Żadna metoda nie może wypisywać wyników — od tego jest Main.

PODSUMOWANIE

Co trzeba zapamiętać

  • Parametr to kopia — zmiana parametru nie dotyka zmiennej wywołującego.
  • ref daje dostęp do oryginału; słowo piszemy w deklaracji i przy wywołaniu.
  • out służy do oddania dodatkowego wyniku i musi zostać przypisany w metodzie.
  • Parametry opcjonalne stoją na końcu listy, a argument nazwany pozwala pominąć środkowy.
  • Przeciążenia różnią się liczbą lub typami parametrów — nigdy samym typem wyniku.
  • Zmienne zadeklarowane w metodzie nie istnieją poza nią; dane przekazujemy parametrami i zwracamy przez return.

Dokumentacja: Microsoft Learn — temat tej lekcji.