Az eddigi fejezetekben a C# és az objektumorientált programozás alapelveivel ismerkedtünk meg. Programjaink során sűrűn fog adódni, hogy viszonylag nagy mennyiségű, logikailag összefüggő adatot kell tárolnunk a memóriában. Ez többféleképpen megvalósítható, éppen ezért az adatstruktúrák fejezet az adatok strukturált tárolásával foglalkozik.
A tömbökkel már korábban is találkoztunk C# során. A szöveg típus felfogható a karakterek tömbjeként. Egy egyszerű tömb létrehozásának a szintaxisa:
int[] tomb = new int[30];
A tömb típust a változó neve után írt szögletes zárójelek jelzik. A tömb mindig referencia típus lesz, ezért a new kulcsszóval foglalni kell neki memóriaterületet. A fenti deklaráció egy 30 egész típusú szám tárolására képes tömböt hoz létre. A tömb elemei egész számokkal indexeltek. Az indexelés nullától indul, tehát a fenti kódrészletben létrehozott tömb esetén az első elem indexe 0, az utolsóé pedig 29. Ha ebből a tömbből megpróbáljuk kivenni a -1. vagy a 30. elemet, akkor tömb alul/felül indexelési kivételt kapunk.
A tömb elemeit megadhatjuk futás közben, de program készítés során is. Az alábbi példa a tömb elemeinek megadását mutatja be mindkét módon:
using System;
namespace PeldaTomb
{
class Program
{
static void Main(string[] args)
{
var gyumolcsok = new string[]
{
"alma", "körte", "szilva"
};
var bevitelek = new string[3];
for (int i=0; i<bevitelek.Length; i++)
{
Console.WriteLine("{0}. bevitel: ", i);
bevitelek[i] = Console.ReadLine();
}
foreach (var gyumolcs in gyumolcsok)
{
Console.WriteLine(gyumolcs);
}
foreach (var bevitel in bevitelek)
{
Console.WriteLine(bevitel);
}
Console.ReadLine();
}
}
}
A program egy lehetséges kimenete:
1. bevitel: első
2. bevitel: második
3. bevitel: harmadik
alma
körte
szilva
első
második
harmadik
A forráskódban megfigyelhető, hogy ha előre adjuk meg az elemekkel a tömböt, akkor a tömb méretszámát nem kell kitenni, mivel az elemek száma egyértelműen meghatározza.
Továbbá az is feltűnhet, hogy a foreach ciklus csak kiíratásra van használva. Ennek az az oka, hogy a foreach ciklusváltozója csak olvasható. Tehát, ha tömbbe szeretnénk ciklikusan írni, akkor mindenképpen a for ciklust kell alkalmaznunk.
A tömb a C/C++ nyelvekkel ellentétben C# esetén valódi típus, ami azt jelenti, hogy metódus argumentumban is szerepeltethetünk tömböt. Ezzel már találkozhattunk is mintaprogramjaink során. A main metódus egy szöveg tömböt vesz át. Ezen szövegtömb az operációs rendszer által a programnak átadott argumentumokat tartalmazza. Ez alapján készíthetünk argumentumokkal vezérelt programokat is. Az alábbi példa ezt mutatja be:
using System;
namespace PeldaArgumentumok
{
class Program
{
static void Main(string[] args)
{
if (args.Length < 1)
{
Console.WriteLine("Nem elég argumentum!");
return;
}
try
{
switch (args[0])
{
case "hello":
Console.WriteLine("Hello");
break;
case "hellow":
Console.WriteLine("Hello, {0}!", args[1]);
break;
default:
Console.WriteLine("Ismeretlen argumentum!");
break;
}
}
catch (Exception ex)
{
Console.WriteLine("Hiba történt: {0}", ex.Message);
}
Console.ReadLine();
}
}
}
A példaprogramot parancssorból indítsuk el a következő paraméterekkel:
pelda_argumentumok.exe hello
Ekkor a képernyőn a Hello üzenetet kapjuk. Ha a programot a hellow World argumentumokkal futtatjuk, akkor a képernyőn a Hello, World! üzenet fog megjelenni. Az ilyen programok esetén igencsak kényelmetlen lenne a hibakeresés, ha minden egyes alkalommal így kellene hibát keresnünk a programunk működésében. Szerencsére a Visual Studio rendelkezik erre is megoldással.
A Solution Explorer-ben válasszuk ki a projektünket, majd jobb kattintás a projekt nevén és a megjelenő menüben válasszuk ki a Properties lehetőséget. Ez megnyitja a korábban a főfüggvénynél tárgyalt alkalmazás beállítások lehetőséget.
Ezután válasszuk ki a Debug fület. Itt találunk egy szövegdobozt, aminek a neve Command Line Arguments.
Amit ebbe a dobozba beírunk, azt minden egyes debug futtatáskor extra argumentumként meg fogja kapni a programunk. Egyes esetekben, főleg, ha fájlokkal dolgozik a programunk, hasznos lehet átállítani a Working Directory beállítást. Ha ezt módosítjuk, akkor hibakereső üzemmódban is abban a mappában fog futni a programunk.
A tömb egy olyan adatszerkezet, amely menet közben nem méretezhető át. Tehát ha új elemeket szeretnénk egy meglévő tömbhöz adni, az csak úgy fog működni, hogy létrehozunk egy új tömböt, ami az új elemek és a meglévő elemek tárolására alkalmas, ezután pedig bemásoljuk a meglévő elemeket és az új elemeket a teljesen új tömbünkbe.
Tömbökben referencia típusokat is alkalmazhatunk, viszont ebben az esetben nem elég példányosítani a tömböt, az egyes elemeket is példányosítani kell, mivel ebben az esetben a tömb csak az objektumra mutató referenciát tárolja, így példányosítás nélkül a tömb elemeinek értéke null lesz.
Az osztályokat nem muszáj a konstruktoruk segítségével példányosítani. Erre a célra vezették be a nyelvben az Object Initializer szintaxist, amivel egy osztály adattagjai úgy adhatóak meg, mint egy tömb elemei.
Ez akkor jön jól, ha van egy osztályunk, amely adattagokkal rendelkezik, de a konstruktor az objektum minden adattagjának beállításához nagyon összetett és komplikált lenne. Ebben az esetben nem érdemes konstruktort írni. Az objektum inicializáló szintaxis a következő:
var objektum = new Osztaly()
{
Adattag1 = érték,
Adattag2 = érték,
Adattag3 = érték
};
Ez a szintaxis csak olyan adattagok esetén alkalmazható, amelyek publikusan is írhatóak. Egyéb védelmi szinttel rendelkező adattagok nem írhatóak ezzel a módszerrel. Ezen adattagok beállítására továbbra is a konstruktor szintaxis használható.
Az adattagok ilyen jellegű megadását és a tömbök referencia tárolását az alábbi példa mutatja be:
using System;
namespace PeldaTombPeldanyositasa
{
//demo osztály 2 adattaggal
class Demo
{
public string Szoveg { get; set; }
public int Szam { get; set; }
//alapértelmezett konstruktor
public Demo()
{
Szoveg = "";
Szam = -1;
}
//paraméteres konstruktor
public Demo(string szoveg, int szam)
{
Szoveg = szoveg;
Szam = szam;
}
}
class Program
{
static void Main(string[] args)
{
//a tömb példányosítása még
//nem példányosítja az elemeket!
var tomb = new Demo[4];
tomb[0] = new Demo("Teszt", 42);
//Object initializer szintaxis
tomb[3] = new Demo()
{
Szoveg = "Masik",
Szam = 11
};
foreach (var elem in tomb)
{
if (elem == null)
{
Console.WriteLine("null");
}
else
{
Console.WriteLine("{0} ; {1}", elem.Szoveg, elem.Szam);
}
}
Console.ReadLine();
}
}
}
A program kimenete:
Teszt ; 42
null
null
Masik ; 11
Tömbök kezelését segítő metódusok, tulajdonságok
Korábban említettem, hogy minden tömb lényegében egy objektum. Ezen objektumok őse az Array osztály, amely a tömb adattároláson kívül tartalmaz még néhány tulajdonságot, amelyet használhatunk a tömbök kezelésénél:
int Length { get; }
Visszaadja az aktuális tömb elemeinek a számát.
long LongLength { get; }
Visszaadja az aktuális tömb elemeinek a számát hosszú egész típusban. Akkor jön jól, ha nagyon nagy méretű tömböket szeretnénk kezelni.
int Rank { get; }
Visszaadja a tömb dimenzióinak a számát.
Ezen tulajdonságokon kívül az Array osztály számos statikus metódust tartalmaz, amelyeket felhasználhatunk tömbök kezelésére. Ezek közül a leghasznosabbak és legfontosabbak:
Array.Clear(Array array, int index, int length);
Visszaállítja egy tömb elemeinek értéket az alapértelmezettre. Első paramétere a tömböt, második paramétere a visszaállítás kezdő indexét, utolsó paramétere pedig a visszaállítandó elemek számát adja meg.
Array.Copy(Array sourceArray, Array destinationArray, int length);
Array.Copy(Array sourceArray, Array destinationArray, long length);
A tömb elemeit másolja egy másik tömbbe. Az első paraméter a forrástömb, a második paraméter a cél tömb, a harmadik paraméter pedig a másolandó elemek számát adja meg.
Array.Copy(Array sourceArray, int sourceIndex, Array destinationArray, int destinationIndex, int length);
Array.Copy(Array sourceArray, long sourceIndex, Array destinationArray, long destinationIndex, long length);
Szintén tömb elemeinek másolása egy másik tömbbe, de ebben a változatban meghatározható a kezdőindex. Az első paraméter ugyanúgy a forrás tömb, a második paraméter a kezdő indexet határozza meg, hogy melyik elemtől induljon a másolás. A harmadik paraméter a cél tömb. A negyedik paraméter pedig a cél index. Az ötödik pedig az elemek száma.
int Array.IndexOf(Array array, object value);
int Array.IndexOf(Array array, object value, int startIndex);
Visszaadja, hogy az első paraméter által meghatározott tömbben a második paraméterben megadott objektum hányadik helyen szerepel. A három paraméteres változatban az utolsó paraméter a kezdőindexet határozza meg, amelytől kezdve a keresés indul.
int Array.LastIndexOf(Array array, object value);
int Array.LastIndexOf(Array array, object value, int startIndex);
Mint az IndexOf, viszont ez a metódus a keresett objektum utolsó előfordulási indexét adja vissza, nem az elsőt.
Array.Reverse(Array array);
Array.Reverse(Array array, int index, int length);
Megfordítja a tömbben szereplő elemek sorrendjét. Az első paraméter a tömböt,a második paraméter a kezdő elem indexét, a harmadik paraméter pedig az elemek számát határozza meg.
Array.Sort(Array array);
A tömb elemeinek sorbarendezése növekvő sorrendben. Egyedi osztályokat tartalmazó tömb esetén csak akkor fog működni, ha az osztály implementálja az IComparable<T> interfészt.
Array.Sort(Array keys, Array items);
Két tömb elemeinek a sorberendezése, méghozzá úgy, hogy az első paraméterként megadott tömb kulcsokat tartalmaz, amelyhez a második paraméterként megadott tömb értékek társulnak.
Stackalloc
Unsafe esetén speciális kulcsszó a stackalloc. Ez optimalizációs célt szolgál. C# esetén a dinamikusan létrehozott elemek (minden amit a new operátorral példányosítunk, pl: tömbök, objektumok) egy úgynevezett dinamikus memóriaterületen találhatóak, amit programozásban heap-nek nevezünk. A heap problémája, hogy nem garantálható benne összefüggő memóriaterület a dinamikus foglalás és a szemétgyűjtés miatt. Tehát töredezetté tud válni, ami azt jelenti, hogy lassabb lehet a program működése, mivel nem szekvenciális az olvasás.
Mondhatnánk, hogy ez nem akkora probléma, mert elég gyors a memória, viszont lehet olyan speciális eset, ahol számít. Éppen ezért bevezették a stackalloc kulcsszót. Ez azt csinálja, hogy a heap helyett a paraméter átadásra szolgáló veremben helyezi el az adott változót, ahol a verem felépítése miatt garantálható az egybefüggő terület.
Tehát használatával elméletileg gyorsul a program végrehajtása, mivel a modern CPU architektúrákban a nagy méretű L2 és L3 cache memóriának köszönhetően a paraméter átadási verem nagy valószínűséggel a CPU cache memóriába kerül, ami nagyságrendekkel gyorsabb, mint a RAM. Az alábbi program unsafe kontextusban egy ilyen tömb kezelést mutat be:
using System;
namespace PeldaUnsafe2
{
class Program
{
private static unsafe void Fibonaccci()
{
const int meddig = 40;
int* fib = stackalloc int[meddig];
int* p = fib; //a mutató így fib[0] elemet mutatja
*p++ = *p++ = 1; //első két elem 1-esre állítása
for (int i = 2; i < meddig; ++i, ++p)
{
*p = p[-1] + p[-2];
Console.Write("{0} ", fib[i]);
}
}
static void Main(string[] args)
{
Fibonaccci();
Console.ReadKey();
}
}
}
A program kimenete:
2 3 5 8 13 21 34 55 89 144 233 377 610 987 1597 2584 4181 6765 10946 17711 28657 46368 75025
121393 196418 317811 514229 832040 1346269 2178309 3524578 5702887 9227465 14930352 24157817
39088169 63245986 102334155
Implementált interfészek
A tömb ugyan implementálja az IList<T> interfészt, de ez nem azt jelenti, hogy a lista és a tömb viselkedésében azonos. Csupán behelyettesíthetőség miatt implementálja a tömb azIList<T> interfészt. Ez azt jelenti, hogy ha egy IList<T> helyére tömböt helyettesítünk, majd az Add metódust meghívjuk, akkor egy NotSupportedException kivétel fog keletkezni:
(new int[1] as IList<int>).Add(2); //NotSupportedException kivételt dob