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.