03. Przekazywanie parametrów

03. Przekazywanie parametrów

Autor: Paweł Kruczkowski

Opublikowano: 10/10/2006, 12:00 AM

Liczba odsłon: 132410

Tematem dzisiejszego artykułu są parametry, bez których żaden nasz program się nie obejdzie. Zostaną one przedstawione pod kątem użyteczności oraz sposobu ich przekazywania w trakcie pisania naszego kodu.

Parametr lub argument – to dwa pojęcia, które często są używane zamiennie. Niektórzy bowiem programiści uważają, że należy odróżniać parametry w deklaracji metody od argumentów, które są przekazywane do metody, kiedy ona została wywołana (w artykule będziemy używać najczęściej pojęcia parametru).

Jak wiemy, niektóre tworzone przez nas metody nie przyjmują parametrów (po prostu ich nie wymagają). Nie oznacza to jednak, że większość ich wymaga. Metody mogą przyjmować dowolną liczbę parametrów. Lista tych parametrów znajduje się wewnątrz nawiasów po nazwie metody, a każdy parametr poprzedzony jest przez nazwę jego typu.

Parametry „dają” nam uogólnienie metody, to znaczy, że taka metoda może działać na różnych danych i może być używana w różnych sytuacjach. Aby przybliżyć istotę stosowania sparametryzowanych metod, prześledźmy oto taki przykład z życia:

Mamy metodę, która zwraca sześcian liczby 3:

            int Szescian()
      {
              return 3 * 3 * 3;
      }
 

Jak łatwo się domyśleć, użycie takiej metody jest bardzo ograniczone, pomimo faktu, że rzeczywiście zwraca prawidłową liczbę, czyli 27 (3^3 = 27). Odrobina jednak jej modyfikacji może sprawić, że nasza prościutka metoda stanie się bardziej przydatna w życiu programisty. Jak to zrobić? Bardzo prosto, użyć w niej parametrów, na przykład:

            int Szescian(int i)
      {
              return i * i * i;
      }

Teraz, nasza metoda Szescian() może zwracać sześcian dowolnej liczby całkowitej z jaką zostanie ona wywołana. Fachowo powinno się nazywać taką metodę - metodą ogólną, gdyż nie tylko potrafi obliczyć sześcian z liczby 3.

A więc możemy pokusić się już w tym miejscu na stwierdzenie, że wewnątrz ciała metody parametry działają jak zmienne lokalne, czyli tak, jakby zostały zadeklarowane w ciele metody i zainicjalizowane za pomocą przekazywanych wartości. Aby powyższe stwierdzenie zrozumieć napiszmy następujący program:

class Program
{
     public void MojaMetoda(int liczba1, int liczba2)
     {
         System.Console.WriteLine("Przekazane parametry to: {0} i {1}", liczba1, liczba2);
     }
}
class Glowna
{
     public static void Main()
     {
     int szczesliwaLiczba = 7;
     int pechowaLiczba = 13;
     Program pro = new Program();
     pro.MojaMetoda(szczesliwaLiczba, pechowaLiczba);
     }
}

Metoda MojaMetoda() przyjmuje dwa parametry: oba są liczbami całkowitymi. Parametry: liczba1 oraz liczba2 funkcjonują jak zmienne lokalne metody MojaMetoda(). W statycznej metodzie Main() (o statycznych metodach była już mowa na łamach CentrumXP) zadeklarowaliśmy 2 zmienne lokalne, które to przekazaliśmy jako parametry do metody MojaMetoda() (udało nam się przekazać liczbę 7 do parametru liczba1, natomiast liczbę 13 do parametru liczba2). Po skompilowaniu i odpaleniu powyższego programiku otrzymamy następujące wyniki:

Zanim przejdziemy dalej, tzn. do szczegółowego omówienia sposobów przekazywania parametrów, musimy wprowadzić dwa nowe pojęcia, które są niezbędne, aby te sposoby prawidłowo wytłumaczyć. Mam na myśli zdefiniowanie typów skalarnych i referencyjnych. Jeśli ktoś z nas miał do czynienia z językiem C++ bądź Java, to wie, że typy jakie te języki oferują dzielą się na wbudowane (typy te udostępnia sam język obiektowy) i na zdefiniowane przez użytkownika. Podobnie ma się język C# 2.0. W języku tym prawie wszystkie typy wbudowane są typami skalarnymi (oprócz typu Object oraz String). Natomiast prawie wszystkie typy zdefiniowane przez programistę to typy referencyjne (za wyjątkiem struktur, o których to będzie mowa na łamach CentrumXP oraz typów wyliczeniowych, które już doskonale znamy i umiemy stosować). Podstawową różnicą między typem skalarnym a referencyjnym jest sposób ich przechowywania w pamięci. W przypadku typów skalarnych, wartość przechowywana jest na stosie [1], natomiast w przypadku typów referencyjnych, na stosie jest przechowywany jedynie adres obiektu, a sam obiekt przechowywany jest na stercie [2].

Po zapoznaniu się z dwoma nowymi pojęciami, możemy powrócić do naszych parametrów.

Domyślnie typy skalarne (wbudowane) przekazywane są do metod przez wartość. Innymi słowy, gdy obiekt właśnie typu skalarnego jest przekazywany do metody, to w tym momencie jest tworzona jego tymczasowa kopia. Kopia ta zostaje usunięta, gdy metoda zakończy swoje działanie (ten sposób jest wykorzystany w ostatnim naszym przykładzie).

Jest też drugi sposób przekazywania typów skalarnych do metod. Mowa tutaj o przekazywaniu obiektu typu skalarnego przez referencję. W języku C# istnieją dwa modyfikatory: ref oraz out, dzięki którym ten sposób możemy zrealizować.

Zajmijmy się najpierw przekazywaniem parametrów z wykorzystaniem modyfikatora ref. Napiszmy następujący program:

public class Czas
{
     private int _rok;
     private int _miesiac;
     private int _dzien;
 
     //konstruktor klasy Czas
     public Czas(System.DateTime dt)
     {
         _rok = dt.Year;
         _miesiac = dt.Month;
         _dzien = dt.Day;
     }
       
     public void PobierzCzas(int r, int m, int d)
     {
         r = _rok;
         m = _miesiac;
         d = _dzien;
     }
 }
 
 public class Glowna
 {
     static void Main()
     {
     int rok = 2004;
     int miesiac = 11;
     int dzien = 10;
     //obiekt klasy DateTime
     System.DateTime dt = new DateTime(1982, 8, 23);
     //obiekt klasy Czas 
     Czas czas = new Czas(dt);
 
     czas.PobierzCzas(rok, miesiac, dzien);
 
     System.Console.WriteLine("Mamy następującą datę: {0}-{1}-{2}", rok, miesiac, dzien);
 
 

}
}
 
 
 

Otrzymamy następujący wynik:

Pewnie niektórzy z nas zastanawiają się dlaczego właśnie taki otrzymaliśmy wynik po uruchomieniu powyższego programiku. Zanim wytłumaczymy sobie powód, chciałbym napisać parę słów komentarza do pewnych fragmentów powyższego kodu. W klasie Czas zdefiniowaliśmy sobie konstruktor Czas(), który przyjmuje obiekt typu DateTime (klasa ta przechowuje pełen zestaw informacji na temat dat, czasu etc). Obiekt ten jest udostępniany przez bibliotekę System i zawiera wiele publicznych wartości, m.in. właśnie: Year, Month oraz Day, które odpowiedzialne są za wyciągnięcie odpowiednio roku, miesiąca, czy danego dnia z podanej daty. Wreszcie obiekt ten odpowiada prywatnym zmiennym składowym obiektu Czas (odpowiednio zmienne: _rok, _miesiac oraz _dzien), innymi słowy do zmiennych tych przypisywane są odpowiednie wartości jakie przechowuje w danej chwili obiekt dt.

W wyniku działania naszego programu otrzymaliśmy datę: 2004-11-10, pomimo że pewnie niektórzy z nas sądzą, że powinniśmy uzyskać datę 1982-08-23 (a to dlatego, że zainicjowaliśmy obiekt dt typu DateTime i w jego konstruktorze przypisaliśmy jemu tę właśnie datę. Następnie obiekt ten podaliśmy do konstruktora Czas() przy inicjacji obiektu czas i wykonaliśmy na nim metodę PobierzCzas() – stąd nasze przypuszczenia).

Dlaczego uzyskaliśmy taki, a nie inny wynik? Wszystkiemu winne są parametry. W programie do metody PobierzCzas() w klasie Glowna, przekazywane są 3 parametry, które ta metoda modyfikuje. Jednak tych modyfikacji nie widzimy wypisując datę na ekranie, gdyż metoda ta przyjmuje parametry typu całkowitego, które są skalarami (przypominam, typ wbudowany w języku C# 2.0). A jak już wiemy, domyślnie typy skalarne są przekazywane do metod poprzez wartość, więc w metodzie PobierzCzas() nowe wartości przypisywane są kopiom, a nie oryginalnym zmiennym (po wykonaniu się tej metody, kopie te są usuwane).

Aby jednak uzyskać pożądany efekt, wykorzystajmy modyfikator: ref w następujący sposób:

public class Czas
{
     private int _rok;
     private int _miesiac;
     private int _dzien;
 
     //konstruktor klasy Czas
     public Czas(System.DateTime dt)
     {
         _rok = dt.Year;
         _miesiac = dt.Month;
         _dzien = dt.Day;
     }
     //parametry tej metody poprzedzone slowem: ref
     public void PobierzCzas(ref int r, ref int m, ref int d)
     {
         r = _rok;
         m = _miesiac;
         d = _dzien;
     }
 }
 
 public class Glowna
 {
     static void Main()
     {
         int rok = 2004;
         int miesiac = 11;
         int dzien = 10;
         //obiekt klasy DateTime
         System.DateTime dt = new DateTime(1982, 8, 23);
         //obiekt klasy Czas, 
         Czas czas = new Czas(dt);
         //przekazywanie argumentow przez referencje, a nie przez wartosc
         czas.PobierzCzas(ref rok, ref miesiac, ref dzien);
         System.Console.WriteLine("Mamy następującą datę: {0}-{1}-{2}", rok, miesiac, dzien);
      
     }
 }

Po skompilowaniu będziemy mieli następujący wynik:

Jak widzimy, otrzymalismy prawidłową datę. Wystarczyło tylko dopisać w dwóch miejscach magiczne słówko ref i mamy dobrze działający program. Modyfikator ten powoduje, że nasza metoda PobierzCzas() używa właśnie referencji wskazującej na oryginalne zmienne (a nie na ich kopie), których wartości zostały zmodyfikowane w metodzie PobierzCzas(), gdyż w metodzie głównej Main() zainicjowaliśmy obiekt dt:

    System.DateTime dt = new DateTime(1982, 8, 23);

W ten sposób zaistniałe zmiany w metodzie PobierzCzas() są już widoczne w metodzie Main() i odpowiednio wyświetlone na ekranie.

Jak już wyżej zostało napisane, obok modyfikatora ref istnieje jeszcze modyfikator out, o którym parę słów.

Pewnie część z nas – dogłębnie analizując powyższy przykład z modyfikatorem ref – zastanawia się po co nadawać wartości zmiennym (rok, miesiac i dzien), które są przekazywane do metody PobierzCzas(), która ta z kolei i tak sobie je odpowiednio zmodyfikuje. Jednak nie jest to nasz błąd. Musimy pamiętać w tym miejscu o jednym istotnym fakcie. A mianowicie, język C# wymaga zdefiniowanego przypisania. Innymi słowy, każda zmienna musi mieć nadaną wartość, zanim ją w naszym programie użyjemy (już o tym doskonale wiemy, piszę w ramach przypomnienia).

Idąc dalej, gdybyśmy więc nie przypisali żadnych wartości zmiennym: rok, miesiac oraz dzien i tak je przekazali do metody PobierzCzas() (w której i tak wartości te się zmienią) to podczas kompilacji otrzymamy mniej więcej taki komunikat:

Jednak – jak się już pewnie domyślamy, jest sposób, aby rzeczywiście nieinicjalizować zmienne (poprzez przypisanie im odpowiednich wartości), a następnie je przekazać do metody (która i tak sobie je odpowiednio zmodyfikuje) i… nie uzyskać na ekranie błędu. Odpowiedź jest prosta: użyć w naszym programie modyfikatora out.

Jego użycie polega więc na tym, że kompilator nie wymaga inicjalizacji parametru przed przekazaniem go przez referencję. Zmodyfikujmy nasz program w następujacy sposób:

public class Czas
{
    private int _rok;
    private int _miesiac;
    private int _dzien;
 
    //konstruktor klasy Czas
    public Czas(System.DateTime dt)
    {
        _rok = dt.Year;
        _miesiac = dt.Month;
        _dzien = dt.Day;
    }
    //parametry tej metody poprzedzone slowem: ref
    //public void PobierzCzas(int r, int m, int d)
    public void PobierzCzas(out int r, out int m, out int d)
    {
        r = _rok;
        m = _miesiac;
        d = _dzien;
    }
}
public class Glowna
{
    static void Main()
    {
        //deklarujemy zmienne, ale nie przypisujemy im zadnych wartości
        int rok, miesiac, dzien;
        //obiekt klasy DateTime
        System.DateTime dt = new DateTime(1982, 8, 23);
        //obiekt klasy Czas, 
        Czas czas = new Czas(dt);
        //przekazywanie argumentow przez referencje z wykorzystaniem modyfikatora out
        czas.PobierzCzas(out rok, out miesiac, out dzien);
            System.Console.WriteLine("Mamy następującą datę: {0}-{1}-{2}", rok, miesiac, dzien);
      
  }
}

Jak widzimy, sposób deklarowania paramterów z modyfikatorem out jest identyczny jak w przypadku modyfikatora ref. W powyższym przykładzie zadeklarowaliśmy 3 zmienne: rok, miesiac oraz dzien (nie przypisując im żadnych wartości) i tylko takie przekazaliśmy przez referencję w metodzie PobierzDane(). A więc parametry w ten sposób przekazane nie niosą ze sobą żadnych informacji (bo nie mają przypisanej żadnej wartości) a jedynie pobierają dane (dane z metody, która je odpowiednio modyfikuje).

Po skompilowaniu i uruchomieniu powyższego przykładu otrzymamy następujące wyniki:

W dzisiejszym artykule, został przedstawiony mechanizm przekazywania parametrów. Mechanizm ten odgrywa bardzo ważną rolę w naszym programie, gdyż bez niego nie dałoby się napisać prawidłowo działającego kodu. Reasumując, typy skalarne (czyli typy wbudowane w języku C# np. intiger) są domyślnie przekazywane przez wartość. Gdy użyjemy jednak modyfikatora ref, to przekazywanie odbywa się już przez referencję. Dzięki temu, w metodzie wywołującej możliwe jest odczytywanie wartości, które zostały zmodyfikowane. Jest też modyfikator out, którego zadaniem jest jedynie pobieranie wartości z metody (pozwala przekazać przez referencję nizainicjalizowaną zmienną).

W kolejnym artykule przybliżymy sobie pojęcia przeciążania metod oraz konstruktorów.