Egy függvénynek bárhány paramétere, (bemenete) lehet, de csakis egy kimenete, azaz egy visszatérési értéke. Ezáltal jól illeszkedik a matematikai függvény fogalomra a programozásban megismert változat.
A gyakorlatban ez azonban nem mindig kényelmes, praktikus, vagy optimális. Most nézzük meg milyen lehetőségeink vannak akkor, ha egy metódusból több adatot szeretnénk “kivezetni”!
Visszatérés összetett típussal
Megtehetjük, hogy, definiálunk egy összetett típust: Osztályt vagy struktúrát. Abban felvesszük az összes kimeneti adatot publikus mezőként. Ha a függvényünk ezen típus egy példányával tér vissza, akkor továbbra is csak egy visszatérésünk lesz, de az több információt is tartalmaz.
Összetett típusokat bővebben itt tárgyaltuk: Összetett típusok és példányaik
// A függvény visszaadja azt a legkisebb téglalapot,
// amiben minden bemenetben kapott pont szerepel.
Rectangle ContainingRectangle (Vector2[] points)
{
Vector2 min = new Vector2();
Vector2 max = new Vector2();
min.x = points[0].x;
min.y = points[0].y;
max.x = points[0].x;
max.y = points[0].y;
for (int i = 1; i < points.Length; i++)
{
if (points[i].x < min.x)
min.x = points[i].x;
if (points[i].y < min.y)
min.y = points[i].y;
if (points[i].x > max.x)
max.x = points[i].x;
if (points[i].y > max.y)
max.y = points[i].y;
}
Rectangle rectangle = new Rectangle();
rectangle.center.x = (min.x + max.x) / 2;
rectangle.center.y = (min.y + max.y) / 2;
rectangle.size.x = max.x - min.x;
rectangle.size.y = max.y - min.y;
return rectangle; // Itt gyakorlatilag 4 flaot értéket adunk vissza.
}
struct Vector2 // Struktúra egy sík beli pontra
{
public float x;
public float y;
}
struct Rectangle // Struktúra egy sík beli téglatestre
{
public Vector2 center;
public Vector2 size;
}
Kimenő out
paraméterek használata
Ha egy out
kulcsszót teszünk egy paraméter elé azzal jelezzük, hogy az egy kimenő adat lesz.
float square5 = Square(5, out string square5Text);
Console.WriteLine(square5Text);
float Square (float num, out string text)
{
text = "A szám négyzete: " + (num * num);
return num * num;
}
Mint azt láthatjuk, az out
kulcsszót ki kell írnunk mind a metódus definíciójánál, mind a használatakor egyaránt.
Egy metódus tetszőleges számú kimenő out
paramétert tartalmazhat.
Az out
paramétereknek minden return
előtt kötelező paramétert adni minden lefutási ágon.
⚠️ Figyelem ⚠️
Az out
paraméterek használata nagy általánosságban kerülendő. Helyette sokszor szerencsésebb egy új összetett típust bevezetni.
Még általánosabban: Túl sok paraméterű metódus írása kerülendő. Ha túlzottan hagyatkozunk a kimenő paraméterekre az összetett típusok helyett, nagyon hamar eljutunk ahhoz a pontig, hogy a paraméterlistáink hosszúak és átláthatatlanok lesznek.
A Try
Tervezési minta
A legjellemzőbb “helyes” használata az out
paraméternek az úgy nevezett TryGet...
típusú függvényekkel lehetséges.
Ezen függvények mindig a Try
(megpróbál) szóval kezdődnek, és egy bool típussal térnek vissza.
Ezek a függvények mindig “megpróbálnak” kiszámolnia valamilyen eredményt és visszatérnek azzal, hogy sikerült-e. Ha sikeres a számítás, akkor egy out
paraméterben adják vissza az eredményt.
Példák lehet korábban már tárgyalt Unity metódusok is:
bool b1 = Physics.Raycast(ray, out RaycastHit hit); // Try kulcsszó itt hiányzik
bool b2 = gameObj.TryGetComponent(out Rigidbody rb);
// Mindkét metódus bool típussal tér vissza, ami azt adja meg sikeres volt-e a keresés.
Fentieket Lásd: Raycast, Unity Objektumok referenciái
Példa egy Try…
típusú függvény implementálására és használatára:
// return: Tudunk-e osztani egyáltalán.
// out paraméter: eredmény.
bool TryDivide(float a, float b, out float result) // Megpróbálunk osztani
{
if (b == 0) // Nullával nem tudunk osztani
{
result = 0;
return false;
}
else // Egyébként...
{
result = a / b;
return true;
}
}
// Használat:
if(TryDivide(5, 4, out float result)) // Ha tudjuk osztani a-t b-bel,
{
Console.WriteLine(result); // akkor kiírjuk az eredményt.
}
Referencia típusú paraméterek
Ha egy referenciatípusú adatot használunk paraméternek, és ezen az objektumon változtatunk metóduson belül, annak hatása érzékelhető lesz a hívás után is.
Az alábbi példában tömböket használok fel ennek érzékeltetésére. Bővebben: Tömbök
int[] array = new int[10]; // A tömb egy referenciatípusú
Fill(array);
Console.WriteLine(array[3]); // 3
void Fill(int[] array)
{
for (int i = 0; i < array.Length; i++)
array[i] = i;
}
Ennek az oka az adat átadásának módjában keresendő: Referencia- és értéktípusok
A ref
kulcsszó
Adat típusú paraméterek esetén is nyilatkozhatunk arról, hogy referenciaként történjen az átadás. Ekkor a metóduson belül, annak referenciaként megjelölt paraméterein történt módosítások “kihatnak” azokra a változók tartalmára is, amiket a híváskor adtunk meg.
Ha azt szeretnénk, hogy egy érték típusú paraméter referenciaként viselkedjen, akkor a ref
kulcsszót kell használni a metódus definiálásakor a megfelelő paraméter előtt. Emellett az out
-hoz hasonlóan ezt a kulcsszót ki kell írni híváskor is.
int n = 5;
DoubleOf(ref n); // Metódus hívása
Console.WriteLine(n); // 10 (Ez a ref kulcsszó használata nélkül 5 lenne)
// ...
void Double(ref int num) // Metódus definiálása
{
num *= 2;
}
Kiegészítő anyag: Enumerátorok
Írhatunk olyan függvényeket, amik nem csak egyszer térnek vissza, de akár egymás után többször is és ezen visszatéréseket egymás után egyből fel tudjuk használni.
(Itt most nem a korai metódusmegszakításról beszélek, mikor több return
is van egy metóduson belül, de minden hívásnál maximum egy fut le.)
Ehhez az System.Collections.Generic
névtér részét képező IEnumerable
generikus osztályt használhatjuk a következőképpen:
// Az alábbi metódus tetszőleges számú Vector2Int objektummal tér vissza:
static IEnumerable<Vector2Int> GetPositionsInCircle(Vector2Int center, int range)
{
for (int x = -range; x <= range; x++)
for (int y = -range; y <= range; y++)
{
Vector2Int point = new(x, y);
if(point.magnitude <= range)
yield return center + point; // Figyeljük meg a visszetérést: yield return
}
}
Ezután egy foreach
ciklusban tudunk végig iterálni az enumerátor által visszaadott elemeken:
foreach(Vector2Int position in GetPositionsInCircle(Vector2Int.zero, 5))
{
Instantiate(prefab, (Vector2) position)
}
A fenti megoldás hasonló ahhoz, mintha egy listával térnénk vissza a már jól ismert módon:
static List<Vector2Int> GetPositionsInCircle(Vector2Int center, int range)
{
List<Vector2Int> result = new ();
for (int x = -range; x <= range; x++)
for (int y = -range; y <= range; y++)
{
Vector2Int point = new(x, y);
if(point.magnitude <= range)
result.Add(center + point); // Először összeállítunk egy listát
}
return result; // Azután térünk vissza
}
A lenti listás megoldást is pont úgy lehetne foreach
ciklussal használni, mint az előző, enumerátorral visszatérőt.
A két megoldás vére hajtás szempontjából nem teljesen ekvivalens.
Az enumerátor, amint kiszámolt egy elemet, visszaadja azt és a foreach
ciklus egyből fel is használja a visszaadott eleme, csak ezután történik a következő megkeresése. Ezzel szemben a listás megoldás először összeállítja a teljes sorozatot majd egészében tér vissza velük és csak ezután tudja egy külső foreach
ciklus felhasználni azt.
A Unity Coroutine-jai is enumerátorokat használnak: Késleltetett műveletvégrehajtás
Az enumerátorok előnyei a listákkal szemben abban rejlenek, hogy nem igénylik az azonnali elemek kiszámítását vagy tárolását. Az IEnumerable
-t visszaadva, a végrehajtás értékei csak akkor kerülnek kiszámításra, amikor azokra iterálunk. Ez különösen előnyös lehet nagy vagy akár végtelen adathalmazok esetén, mivel lehetővé teszi az elemek későbbi feldolgozását, csökkentve ezzel a memóriahasználatot és javítva a teljesítményt.
Ezzel szemben a listának is sok előnye van az enumerátorhoz képest, hiszen az utóbbival semmi egyebet nem lehet tenni, csak megfelelő sorrendben, elejétől a végéig végig járni, míg a listával számtalan egyéb műveletet is tudunk végezni. Ezért sok esetben még akkor is szükségünk van egy listára, ha egy enumerátor adja vissza az elemeket. Ezt a következő módon tehetjük meg:
Lsit<Vector2Int> list = new();
// ...
IEnumerable<Vector2Int> enumerable = GetPositionsInCircle(Vector2Int.zero, 5);
list.AddRange(enumerable);