HIK Elektronikus Felsőoktatási Tankönyv- és Szakkönyvtár
A Kempelen Farkas Felsőoktatási Digitális Tankönyvtár/vagy más megjelenítő által közvetített digitális tartalmat a felhasználó a szerzői jogról szóló 1999. évi LXXVI. tv. 33. paragrafus (4) bekezdésében meghatározott oktatási, illetve tudományos kutatási célra használhatja fel. A felhasználó a digitális tartalmat képernyőn megjelenítheti, letöltheti, arról elektronikus adathordozóra vagy papíralapon másolatot készíthet, adatrögzítő rendszerében tárolhatja. A Kempelen Farkas Felsőoktatási Digitális Tankönyvtár/vagy más megjelenítő weblapján található digitális tartalmak üzletszerû felhasználása tilos, valamint kizárt a digitális tartalom módosítása és átdolgozása, illetve az ilyen módon keletkezett származékos anyag további felhasználása.

15. Eljárások és függvények felsőfokon

Hernyák Zoltán

Kérdés: tudunk-e a paraméterváltozókon keresztül értéket visszaadni?

Erre sajnos a C#-ban nem egyszerű válaszolni. A helyzet igen bonyolult mindaddig, amíg a referencia típusú változókkal alaposan meg nem ismerkedünk.

Addig is használjuk az alábbi szabályokat:

  • amennyiben a paraméterváltozók típusa a nyelv alaptípusai (int, double, char, string, …) közül valamelyik, úgy nem tudunk értéket visszaadni a hívás helyére,

  • ha a paraméter típusa tömb, akkor a tömbelemeken keresztül azonban igen.

Pl.:

static void Kiiras(int a,int b)

{

Console.WriteLine("A {0}+{1}={2}",a,b,a+b);

a = a+b;

}

Ha a fenti eljárást az alábbi módon hívjuk meg …

int x=12;

Kiiras(x,20);

Console.WriteLine("A hivas utan az x erteke x={0}",x);

… ekkor kiíráskor az ’x’ változó értéke még mindig 12 lesz. Mivel a paraméterátadás során a híváskori érték (12) átadódik az eljárás ’a’ változójába mint kezdőérték (a=12), de utána a hívás helyén szereplő ’x’ változó, és a fogadó oldalon szereplő ’a’ változó között minden további kapcsolat megszűnik. Ezért hiába teszünk az ’a’ változóba az eljáráson belül más értéket (a+b), az nem fogja befolyásolni az ’x’ értékét, marad benne a 12.

A fenti technikát érték szerinti paraméterátadásnak nevezzük. Ekkor az aktuális paraméterlistában feltüntetett értéket a fogadó oldal átveszi – a nélkül, hogy a kapcsolatot fenntartaná. Az érték egyszerűen átmásolódik a fogadó oldal változóiba.

Mint az ábrán is látszik, az ’x’ aktuális értéke (12) átmásolódik a memória külön területén helyet foglaló ’a’ változóba. E miatt az a=a+b értékadás eredménye is ezen a külön területen kerül tárolásra, és nem zavarja, nem változtatja meg az ’x’ értékét.

Amennyiben azonban a paraméterátadás-átvétel során tömböket adunk át, úgy a helyzet megváltozik:

static void Feltoltes(int[] tomb)

{

tomb[0] = 10;

}

static int[] kisTomb=new int[10];

Feltoltes( kisTomb );

Console.WriteLine(”A 0. tombelem={0}”, kisTomb[0] );

A hívás helyén szereplő kis tömb még feltöltetlen, amikor átadjuk a ’Feltoltes’ eljárásnak. Az eljárás fogadja a tömböt a ’tomb’ változóban, majd megváltoztatja a ’tomb’ elemeinek értékét. Ezen változás azonban beleíródik a ’kisTomb’ által reprezentált tömb-be is, ezért az eljárás lefutása után a kisTomb[0] értéke már 10 lesz.

Az ilyen jellegű paraméterátadást referencia-szerinti átadásnak nevezzük!

A referencia típus nem jelentkezik külön a változó deklarációja során mint külön kulcsszó, vagy egyéb jelzés, ezért nehéz felismerni. Egyelőre fogadjuk el szabályként, hogy azok a változók lesznek referencia típusúak, amelyek esetén a ’new’ kulcsszót kell használni az érték megadásakor (példányosítás).

A referencia-típusú változóknál dupla memóriafelhasználás van. Az elsődleges területen ilyenkor mindig egy memóriacímet tárol a rendszer. Ezen memóriacím a másodlagos memóriaterület helyének kezdőcíme lesz. Ezen másodlagos memóriaterületet a ’new’ hozza létre.

Az nyelvi alaptípusok (int, double, char, string, …) értékének képzésekor nem kell a ’new’ kulcsszót használni …

int a = 10;

… de a tömböknél igen:

int[] tomb = new int[10];

Az ilyen jellegű változókat általában az jellemzi, hogy viszonylag nagy memória-igényük van. Egy ’int’ típusú érték tárolása csak 4 byte, míg a fenti tömb tárolása 40 byte.

Az érték szerinti paraméterátadás során az érték az átadás pillanatában két példányban létezik a memóriában – lemásolódik:

int x=12;

Kiiras(x);

static void Kiiras(int a)

{

...

}

A referencia típusú változók esetén azonban nem másolódik le az érték még egyszer a memóriában (ami a tömb esetén a tömbelemek lemásolását jelentené), hanem csak a szóban forgó tárterület címe (memóriacíme) adódik át. Ekkor a fogadó oldal megkapja a memóriaterület címét, és ha beleír ebbe a memóriaterületbe, akkor azt a küldő oldal is észlelni fogja majd:

static void Feltoltes(int[] tomb)

{

tomb[0] = 10;

}

static int[] kisTomb=new int[10];

Feltoltes( kisTomb );

A fenti ábrán látszik, hogy a ’kisTomb’ változónk aktuális értéke átmásolódik a ’tomb’ aktuális értékébe. Csakhogy a ’kisTomb’ értéke egy memóriacím, ez az, ami valójában átmásolódik a ’tomb’ váltózóba, és nem ténylegesen a tömb elemei.

Mivel a ’tomb’ változó is ismerni fogja a tömbelemek tényleges helyét a memóriában, ezért amennyiben megváltoztatja azokat, úgy a változtatásokat a ’kisTomb’-ön keresztül is el tudjuk majd érni, hiszen mindkét esetben ugyanazokra a tömbelemekre hivatkozunk a memóriában.

Ez az oka annak, hogy a tömbelemek értékének módosítása a fogadó oldalon maradandó nyomot hagy a memóriában – amikor az eljárás már befejezte a futását, az értékek akkor is hozzáférhetőek.

A referencia-szerinti paraméterátadás is érték szerinti paraméterátadás, a változó értéke ilyenkor egy memóriacím, mely mint érték másolódik át a paraméterváltozókba. Az már más kérdés, hogyha a fogadó oldal megkapja a terület kezdőcímét, akkor azt meg is változtathatja. Mivel az eredeti változó is ugyanezen területre hivatkozik, ezért a változásokat a küldő oldalon később észlelni, (pl: kiolvasni) lehet.

Amennyiben a fogadó oldalon referencia típusú paramétert fogadunk, úgy a küldő oldalon csak olyan kifejezés szerepelhet, melynek végeredménye egy referencia:

Feltoltes( new int[20] );

Az előzőekben bemutatott kifejezés létrehoz egy 20 elemű int tömböt. A new foglalja le számára a memóriát, majd visszaadja a terület kezdőcímét. Ezt a kezdőcímet általában egy megfelelő típusú változóban tároljuk el, de amennyiben erre nincs szükség, úgy a memóriacímet tárolás nélkül átadhatjuk egy eljárásnak paraméterként.

static int[] Feltoltes(int[] tomb)

{

for(int i=0;i<tomb.Length;)

{

Console.Write("A tomb {0}. eleme=",i);

string strErtek=Console.ReadLine();

int x = Int32.Parse( strErtek );

if (x<0) continue;

tomb[i] = x;

i++;

}

return tomb;

}

Az előzőekben bemutatott függvény paraméterként megkapja egy int típusú tömb kezdőcímét. A tömb elemeit billentyűzetről tölti fel oly módon, hogy csak pozitív számot fogad el. A feltöltött tömb címét (referenciáját) visszaadja.

Mivel a függvény ugyanazt a memóriacímet adja vissza, mint amit megkapott, ennek nem sok értelme látszik. Figyeljük meg azonban a hívás helyét:

int[] ertekek = Feltoltes( new int[40] );

A hívás helyén a frissen elkészített tömb memóriacímét nem tároljuk el, hanem rögtön átadjuk a függvények. A tömb a hívás helyén készül el, üresen (csupa 0-val inicializálva), a függvény feltölti ezt a tömböt elemekkel, majd visszaadja az immár feltöltött tömb címét.

A fenti megoldás azért szép, mert egy sorban oldja meg a tömb létrehozását, feltöltését. Amennyiben a Feltoltes() csak egy eljárás lenne, úgy az alábbi módon kellene meghívni:

int[] ertekek = new int[40];

Feltoltes( ertekek );

Amennyiben egy alaptípusú paraméter-változón keresztül szeretnénk értéket visszaadni, úgy azt jelölni kell:

static void ParosElemek(int[] tomb, ref int db, ref int osszeg)

{

db=0;

osszeg=0;

for(int i=0;i<tomb.Length;i++)

if (tomb[i] % 2 == 0)

{

db++;

osszeg = osszeg + tomb[i];

}

}

A fenti eljárás két értéket is előállít – a páros tömbelemek számát, és összegét. Mivel ez már kettő darab érték, ezért nem írhatjuk meg függvényként. Egy függvény csak egy értéket adhat vissza.

Az ilyen jellegű paraméterátadást cím szerinti paraméterátadásnak nevezzük. Ekkor a hívás helyén is jelölni kell, hogy az átadott változóban kimenő adatokat várunk:

int x=0,y=0;

ParosElemek(tomb, ref x, ref y);

Console.WriteLine("A paros elemek db={0}, osszeguk={1}",x,y);

A ’ref’ kulcsszóval kell jelölni, hogy az adott paraméterben értéket is vissza szeretnénk adni. Ugyanakkor a ’ref’ kulcsszó ennél többet jelent – a ’ref’ paraméterben értéket is adhatunk át az eljárás számára. Ez a paraméter klasszikus átmenő paraméter.

Az átmenő paraméterek egyidőben bemenő és kimenő paraméterek. Az eljárásban ezen paraméter értékét felhasználhatjuk, és meg is változtathatjuk.

int x,y;

ParosElemek(tomb, ref x, ref y);

Console.WriteLine("A paros elemek db={0}, osszeguk={1}",x,y);

Amennyiben a paraméterként átadott változók nem rendelkeznének értékkel a hívás pillanatában (definiálatlan változó), úgy a C# fordító szintaktikai hibát jelez.

Ez a fenti függvény esetén egyébként értelmetlen, hiszen a függvény db és osszeg paraméterváltozóinak kezdőértéke érdektelen. A probléma forrása az, hogy e paraméterek csak kimenő értékeket hordoznak, bemenő értékük érdektelen. Az ilyen paramétereket nem a ’ref’, hanem az ’out’ kulcsszóval kell megjelölni!

static void ParosElemek(int[] tomb, out int db, out int osszeg)

{

db=0;

osszeg=0;

for(int i=0;i<tomb.Length;i++)

if (tomb[i] % 2 == 0)

{

db++;

osszeg = osszeg + tomb[i];

}

}

A hívás helyén is:

int dbszam,summa;

ParosElemek(tomb, out dbszam, out summa);

A ’ref’ és az ’out’ módosítók között az egyetlen különbség, hogy a hívás helyén (aktuális paraméterlistában) szereplő változóknak ’out’ esetén nem kötelező kezdőértéküknek lenniük. Valamint egy ’out’-os paraméterváltozót a fordító kezdőérték nélkülinek tekint, és a függvényben kötelező értéket adni ezeknek a paraméterváltozóknak a függvény visszatérési pontja (return) előtt.

A ’ref’ esetén a függvényhívás előtt a változóknak kötelező értéket felvenni, és a függvényben nem kötelező azt megváltoztatni.

Aki esetleg keresné, ’in’ módosító nincs a C#-ban, ugyanis a paraméterek alapértelmezésben bemenő adatok, vagyis az ’in’ módosítót ki sem kell írni.

static void Csere(ref int a, ref int b)

{

int c;

c=a;

a=b;

b=c;

}

A fenti eljárás a paramétereként megkapott két változó tartalmát cseréli fel. Mivel ilyenkor érdekes a paraméterek aktuális értéke is, ezért a ’ref’ kulcsszót kell használni.

A fenti eljárás kitűnően felhasználható egy rendező eljárásban:

static void Rendezes(int[] tomb)

{

for(int i=0;i<tomb.Length-1;i++)

for(int j=i+1;j<tomb.Length;j++)

if (tomb[i]>tomb[j]) Csere(ref tomb[i],ref tomb[j]);

}

Programozás tankönyv

XV. Fejezet