A generikus programozási modell típus független programozást tesz lehetővé, ami nagyon jól jön, ha adattárolási szerkezetekről beszélünk. A tárolási szerkezeteknél a tárolt adat típusa nem igazán lényeges, mivel a tárolási algoritmusok és az adatkezelő mechanizmusok nem függenek a tárolt adatok típusától, így ennek segítségével a tárolási mechanizmusok általánosíthatóak bármilyen típusra.
Természetesen az általánosítás megoldható lenne C# esetén úgy is, hogy minden tárolási szerkezetet úgy valósítunk meg, hogy object típusú elemeket tároljon. Mivel minden típus az object osztály leszármazottja, így bármilyen típussal működnének. A vázolt megoldás legnagyobb problémája azonban a boxing -unboxing probléma lenne. Ez nagy mennyiségű adat olvasása és írása esetén jelentősen lelassítaná a programunkat.
A vázolt megoldás hátránya továbbá az, hogy mivel minden objektum konvertálható object típusra, ezért előfordulhat olyan eset, hogy a kollekcióba nem illő elem kerül be, ami kivételkor konvertálási problémát okozhat. Tehát ilyen esetben az olvasási műveletet extra kivételkezeléssel kell ellátni, ami megint csak a sebesség rovására megy.
A .NET keretrendszer első változatában még nem rendelkezett generikus programozási képességekkel. Ez a funkció a .NET második kiadásában mutatkozott be és drámaian gyorsította a programok működését.
Összefoglalva tehát a generikus programozás lehetősége növeli az újrafelhasználhatóságot, a típusbiztonságot és a hatékonyságot.
A generikus programozás használható osztályok és külön metódusok esetén is. Az alábbi példa egy generikus metódust mutat be:
public static void Generic<T1, T2>(T1 elso, T2 masodik)
{
}
A példa metódus két különböző típussal dolgozik, amelyekre T1 és T2 típus néven hivatkozik a kód. A T1 és T2 általános típusjelölőt a metódus neve után elhelyezett relációs jelek között definíció vezeti be. Ezután a metóduson belül bárhol használható a T1 és T2 típus jelölő szó.
Osztályszinten is alkalmazhatóak generikus típusok. Ilyen esetben az osztály neve után kell szerepeltetni a típusokat. A típusokra megkötések is alkalmazhatóak a where kulcsszó használatával. Az alábbi példa egy ilyen osztályt mutat be:
using System;
namespace PeldaGenerikus
{
class Generikus<T> where T : struct
{
private T valtozo;
//a konstruktor privát jelen esetben, mivel
//a konstruktorok nem lehetnek generikusak!
private Generikus() {}
public static Generikus<T> Letrehoz(T parameter)
{
Generikus<T> vissza = new Generikus<T>();
vissza.valtozo = parameter;
return vissza;
}
public override string ToString()
{
return string.Format("valtozo tárolt típusa: {0}, Értéke: {1}",
valtozo.GetType().Name, valtozo);
}
}
class Program
{
static void Main(string[] args)
{
var teszt1 = Generikus<int>.Letrehoz(22);
var teszt2 = Generikus<double>.Letrehoz(33.2);
var teszt3 = Generikus<char>.Letrehoz('A');
//az alábbi hibát fog dobni, mert a string osztály!
//Generikus<string> teszt4 = Generikus.Letrehoz("Teszt");
Console.WriteLine(teszt1);
Console.WriteLine(teszt2);
Console.WriteLine(teszt3);
Console.ReadKey();
}
}
}
A program kimenete:
valtozo tárolt típusa: Int32, Értéke: 22
valtozo tárolt típusa: Double, Értéke: 33,2
valtozo tárolt típusa: Char, Értéke: A
A példa programban T névvel van jelölve a generikus típus, amit a where kulcsszó utáni típus megadás korlátoz. Jelen esetben struktúrákra, vagyis érték típusokra. Többek között a kikommentelt teszt sor ezért okozna fordítási hibát. A where kulcsszó után nem csak általános típus jelölőket (struct, class) adhatunk meg, hanem konkrét típusokat is. Például megadhatnánk a char típust is.
A típus korlátok kapcsán érdemes megjegyezni, hogy ezen korlátok alsóak. Ez azt jelenti, hogy ha class típust adunk meg, akkor minden class típussal működik, ha pedig egy konkrét osztályt adnánk meg korlátként, akkor csak azzal a típussal és az abból leszármaztatott típusokkal működne együtt az osztály (lásd boxing, unboxing). Természetesen korlátnak használhatunk egy tetszőleges felületet is.
A típus korlátok esetén a class igen megengedő. Többek között megengedi azt, hogy az osztály statikus vagy absztrakt legyen. Mivel az ilyen osztályok nem példányosíthatóak és bizonyos esetekben elvárás, hogy példányosítható legyen egy osztály, bevezették a new() korlátot is. Ez megköveteli azt, hogy a típus paraméter osztály legyen és azt, hogy az osztálynak rendelkeznie kell egy publikus, paraméter nélküli konstruktorral. Ha több megkötés is van, akkor a new() utolsó kell legyen.
A példában is látható módon az osztály lehet generikus, viszont a konstruktora nem lehet generikus. Ennek az az oka, hogy ez az öröklődés kezelést igencsak megbonyolítaná, illetve az ilyen osztályok jelentős mértékben lassítanák a teljes keretrendszer működését.
Viszont van megoldás a problémára. Létrehozunk egy statikus metódust, ami jelen esetben egy generikus objektumot ad vissza, a paraméterként kapott T típusú objektumot pedig elhelyezi az objektum belsejében.
Ezt a programozási megközelítést a szakirodalom Factory pattern néven említi. Ez egy tervezési minta1, amely objektumokat gyárt. A Factory (gyár) minta leginkább olyan esetekben hasznos, ha egy osztályhoz több, azonos paraméter listával rendelkező, de eltérő módon viselkedő konstruktort szeretnénk létrehozni.
Sima konstruktorok használatával a fentebb vázolt megoldás nem lehetséges, mivel a polimorfizmus csak olyan esetekben tud működni, ha az azonos nevű függvényeknek a paraméter listájában van eltérés.
Az ICollection<T> interfész
A generikus programozás nagy előnye, hogy típus függetlenül tudunk létrehozni adattároló szerkezeteket. A .NET esetén ezen adattároló szerkezetek a System.Collections névtérben vannak elhelyezve. Ezen névtér osztályai object elemek tárolására vannak felkészítve. Leginkább kompatibilitási okok miatt maradtak meg. Használatukat nem igen javaslom, mivel a rengeteg boxing/unboxing művelet igen negatívan hat a programunk sebességére.
A System.Collections.Generic névtér tartalmazza ezen adattároló szerkezetek generikus implementációit. Ebben a fejezetben csak ezen névtérben található osztályokkal foglalkozok részletesen.
Az összes adattároló szerkezet implementálja az ICollection<T> generikus felületet, ebből adódóan az összes szerkezet rendelkezik az általa biztosított metódusokkal és tulajdonságokkal2. A későbbiek folyamán az egyes szerkezeteknél csak az adott szerkezetre specifikusan jellemző metódusok és tulajdonságok kerülnek majd kifejtésre.
Az ICollection<T> elemei:
int Count { get; }
Visszaadja a kollekcióban tárolt elemek számát.
bool IsReadOnly { get; }
Igaz értéket ad vissza, ha a kollekció csak olvasható, vagyis a mérete nem változtatható.
void Add(T item)
Egy új elemet ad hozzá a kollekcióhoz. Amennyiben a kollekció csak olvasható, akkor NotSupportedException típusú kivételt vált ki a művelet.
void Clear()
Törli a kollekcióban tárolt elemeket. Amennyiben a kollekció csak olvasható, akkor NotSupportedException típusú kivételt vált ki a művelet.
bool Contains(T item)
Igaz értéket ad vissza, ha a kollekció tartalmazza a paraméterként megadott elemet.
void CopyTo(T[] array, int arrayIndex)
A kollekció összes elemét átmásolja az első paraméterként megadott tömbbe. A második paraméter a másolás kezdő indexét határozza meg a tömbön belül.
IEnumerator<T> GetEnumerator()
A foreach ciklus működéséhez kell. Az ICollection<T> felület megvalósítja az IEnumerable<T> felületet is. Az IEnumerable<T> az IEnumerable felület generikus változata. IEnumerable implementálás esetén a generikus változatot érdemes előnyben részesíteni.
bool Remove(T item)
Eltávolítja a paraméterként megadott elemet a kollekcióból. Amennyiben a kollekció csak olvasható, akkor NotSupportedException típusú kivételt vált ki a művelet.
Az IEquatable<T> interfész
Az object osztály metódusainál említésre került, hogy minden osztály rendelkezik egy Equals metódussal. Ennek a bemeneti paramétere egy object típusú változó. Ezzel azonban van egy apró probléma, mégpedig az, hogy ha struktúra esetén szeretnénk implementálni egyedi összehasonlító logikát, akkor minden egyes Equals híváskor egy boxing és másolás fog történni. Ezt elkerülendő vezették be a generikus IEquatable<T> interfészt, ami ugyanúgy egy Equals metódust definiál, de T bemenő paraméterrel, így elkerülhető struktúrák összehasonlításánál a boxing.
Az IEquatable<T> interfész implementálása minden olyan osztály esetén ajánlott, ahol az Equals felüldefiniálásra kerül.
Az IComparable<T> interfész
Az IComparable<T> interfész egy int visszatérésű Compare metódust definiál, amivel a jelenlegi objektumunkat össze tudjuk hasonlítani egy másik, T típusú objektummal. Ennek leginkább rendezésnél van jelentősége, mivel így megoldhatjuk azt, hogy az objektumunk egy numerikus értékké konvertálódjon, ami alapján sorba rendezhetjük. Belsőleg ezen interfész implementácójára támaszkodnak a LINQ rendező metódusai, amelyekről a későbbiekben lesz szó.
A Compare egy T típusú objektumhoz képest hasonlítja a jelenlegi példányt. A visszatérési értékre az alábbi szabályok vonatkoznak:
-
A visszatérési értékének 0-nak kell lennie, ha az aktuális objektum és a paraméterként kapott megegyezik.
-
A visszatérési értékének -1-nek kell lennie, ha az aktuális objektum kisebb, mint a paraméterként kapott.
-
A visszatérési értékének 1-nek kell lennie, ha az aktuális objektum nagyobb, mint a paraméterként kapott.
Az alábbi példa az IEquatable\<T> és IComparable\<T> implementációját mutatja be:
internal class Tanulo : IEquatable<Tanulo?>, IComparable<Tanulo>
{
public Tanulo(string name, double atlag)
{
Name = name;
Atlag = atlag;
}
public string Name { get; }
public double Atlag { get; }
//kompatibilitás miatt az equals-t is felül kell írni
public override bool Equals(object obj)
{
return Equals(obj as Tanulo);
}
//Az equals miatt a GetHashCode is felülírandó
public override int GetHashCode()
{
return HashCode.Combine(Name, Atlag);
}
//típusos összehasonlítás
public bool Equals(Tanulo other)
{
return other != null &&
Name == other.Name &&
Atlag == other.Atlag;
}
//Átlag alapján összehasonlítás
//A double implementálja az IComparable interfészt
public int CompareTo(Tanulo other)
{
return Atlag.CompareTo(other.Atlag);
}
}
IEqualityComparer<T> és IComparer<T>
Az IEquatable\<T> és IComparable\<T> felületek lehetőséget adnak egyenlőség vizsgálatára és összehasonlításra, de mi van akkor, ha a típus nem implementálja ezeket és nem is tudjuk módosítani a típusunkat? Vagy esetlegesen arra lenne szükségünk, hogy a típus által definiált egyenlőségtől vagy sorbarendezéstől eltérő viselkedést implementáljunk?
Ennek a problémának az áthidalására találták ki a IEqualityComparer\<T> és IComparer\<T> interfészeket. Ezekkel olyan osztályokat definiálhatunk, amik a típus alapértelmezett Equals vagy Compare metódusától eltérő viselkedést valósítanak meg, de a fogyasztó oldalon hasonlóan egyszerűen használhatóak.
Az alábbi példa a korábban definiált Tanulo osztály esetén mutatja be az IEqualityComparer\<T> implementálását:
internal class TanuloAtlagEqualityComparer : IEqualityComparer<Tanulo>
{
public bool Equals(Tanulo x, Tanulo y)
{
if (x == null || y == null)
return false;
return x.Atlag.Equals(y.Atlag);
}
public int GetHashCode(Tanulo obj)
{
return obj.Atlag.GetHashCode();
}
}
Az alábbi példa a korábban definiált Tanulo osztály esetén mutatja be az IComparer\<T> implementálását:
internal class TanuloNameComparer : IComparer<Tanulo>
{
public int Compare(Tanulo x, Tanulo y)
{
//null értékek hátra sorolása
if (x == null || y == null) return 1;
return x.Name.CompareTo(y.Name);
}
}
-
“Programtervezési mintának (angolul Software Design Patterns) nevezik a gyakran előforduló programozási feladatokra adható általános, újrafelhasználható megoldásokat. Egy programtervezési minta rendszerint egymással együttműködő objektumok és osztályok leírása” – https://hu.wikipedia.org/wiki/Programtervezési_minta↩
-
Egyes osztályok explicit módon implementálják a felületet, vagyis csak megfelelő konvertálás után érhetőek el bizonyos tulajdonságok és metódusok. Az ilyen fajta elrejtésnek általában oka van. Elképzelhető, hogy az adott metódus vagy tulajdonság az expliciten implementált osztályban nem értelmezhető↩