Az object
osztály három fő metódust tartalmaz. Ezen függvényeket saját osztályainkban felüldefiniálhatjuk. A felüldefiniálás egy olyan folyamat, amikor a metódus neve, paraméterlistája és visszatérési típusa megmarad, azonban a metódus tényleges kódját a leszármaztatott osztályban lecseréljük.
Ezáltal a leszármaztatott osztályban az ősosztályban is meglévő metódus másképpen fog viselkedni, működni.
Nem minden metódus alkalmas felüldefiniálásra. Ahhoz, hogy felüldefiniálható legyen a jelentése, az ősosztályban a metódust a virtual
vagy abstract
kulcsszóval kell ellátni. A leszármaztatott osztályban a felüldefiniált metódus definíciójának meg kell egyeznie az ősosztálybeli definícióval, valamint a metódust meg kell jelölni az override
kulcsszóval. Példaként a ToString()
metódus esetén a definíció így néz ki:
public override string ToString()
{
return base.ToString();
}
A példában szereplő definíció éppen nem csinál hasznosat, mivel az ősosztály ToString()
metódusát hívja meg, vagyis nem definiálja éppen felül a metódus működését. Minden leszármaztatott osztályban az ősosztály elemeire a base
kulcsszó segítségével hivatkozhatunk.
Felüldefiniálás esetén nem kell megijedni a definíciók megalkotásától. A Visual Studio ilyen téren elég intelligens, így elegendő beírnunk az override
kulcsszót és egy szóköz lenyomása után az IntelliSense felismeri, hogy melyik metódusok definiálhatóak felül. A megfelelő metódus kiválasztása után a kódunkba a megfelelő függvénydefiníció fog bekerülni. A listában a már felüldefiniált függvények nem fognak megjelenni, mivel ennek nem lenne értelme.
Mivel minden osztályunk implicit módon az Object
osztályból öröklődik, ezért minden osztályunk rendelkezik három felül definiálható metódussal. Ezek:
string ToString();
Szöveggé alakítja az objektumunkat. Lényegében a Console.Write
vagy Console.WriteLine
híváskor ezen metódus kerül meghívásra, hogy értelmes szöveget kapjunk az objektumunkból.
int GetHashCode();
Egyedi azonosítót kell, hogy képezzen az objektumunk belső adattagjaiból. Arra használatos, hogy a switch-case és a szótár tárolás működjön az objektumunkon.
bool Equals(object obj);
Két objektum egyenlőségének vizsgálata. Az összehasonlító operátor működéséhez elengedhetetlen, illetve a keretrendszer belsőleg is alkalmazza számos helyen.
ToString()
A legkönnyebben felüldefiniálható metódus. Lényegében egy szöveget kell visszaadnunk a függvénnyel. Ez a szöveg fog aztán a képernyőn megjelenni, ha az objektumunkat szeretnénk kiíratni. Az adattagok formázott kiírására használható a String.Format metódus is.
GetHashCode()
A függvénynek egy egyedi azonosítót kell visszaadnia, ami egyértelműen beazonosítja az objektumunkat. Matematikában és informatikában az ilyen eljárásokat hasítófüggvényeknek, vagy angolosan hash függvényeknek nevezzük. Ezen eljárások bármilyen hosszúságú adatot fix hosszúságúra képeznek le. Az így kapott adat a hasító vagy hash érték. A definícióból adódóan a hasító értékből a bemeneti adat nem következtethető ki.
A végtelenből végesre történő konverzió miatt egyértelmű, hogy adódhat olyan eset, hogy a bemeneti adatok különbözőek, viszont a kimeneti értékek azonosak. Ezt a problémát ütközésnek nevezzük. Egy ideális hasító algoritmus esetén a célunk az ütközések minimalizálása lenne. Az ütközések minimalizálásának egyik módja a kimeneti bitek számának növelése, a másik módszer pedig az elérhető bitmennyiség maximális kihasználása.
A .NET keretrendszer belső működésében hasító értékként 4 byte-os egész számot rendel minden objektumhoz, így az egyetlen lehetőségünk az elérhető bitek számának szakszerű és maximális felhasználása. Több algoritmus szerint készíthetünk GetHashCode() függvényt az osztályunkhoz. A legegyszerűbb metódus az, ha lefuttatjuk minden adattagon a GetHashCode() függvényt, majd a kapott eredményeket kizáró vagy1 művelet segítségével kombináljuk.
A módszer hátránya, hogy nem egyedien azonosítja az objektumunkat. Tételezzük fel a következő szituációt:
Legyen egy osztályunk, amely két egész számot tárol, amiket X és Y névvel azonosítunk. X-hez rendeljük hozzá a 255, Y-hoz pedig a 192 értéket. Átváltás és az XOR művelet elvégzése után az alábbi eredményt kapjuk:
Ha az értékeket megcseréljük, (X = 192 és Y = 255) majd újra elvégezzük a műveletet, akkor ugyanazt a kimeneti eredményt kapjuk, mivel a kizáró vagy művelet kommutatív. Ebből adódóan hash képzésnél nem a legjobb ötlet ezen műveletet használni.
Sokkal hatékonyabb és jobb eredményeket kapunk az FNV hash algoritmus használatával. Az FNV algoritmus a nevét a három kitaláló, Glenn Fowler, Landon Curt Noll és Phong Vo neve után kapta. A fejlesztők célja egy gyors, minimális ütközésszámot produkáló algoritmus megalkotása volt. Az algoritmus weboldala a http://www.isthe.com/chongo/tech/comp/fnv/ címen lelhető fel. Itt számos implementációs megoldás található az algoritmusról. Az alábbi kódrészlet egy működő, általános célú implementációt mutat be:
public override int GetHashCode()
{
unchecked //a túlcsordulás nem probléma itt
{
int hash = (int)2166136261;
hash = (hash * 16777619) ^ adattag1.GetHashCode();
hash = (hash * 16777619) ^ adattag2.GetHashCode();
hash = (hash * 16777619) ^ adattag3.GetHashCode();
return hash;
}
}
Az implementáció vázlatos, mivel nem ellenőrzi az adattagokat null értékre!
GetHashCode modern megközelítéssel
A GetHashCode metódust implementálni saját osztályok esetén mindig is kényelmetlen volt, mivel a FNV algoritmust vagy valamelyik másikat az osztály működésére kellett szabni és ha a prímeket nem jól választottuk meg, akkor egy gyengébb hash implementációt kaptunk. Ezen változtattak a .NET Core megjelenésével. Bevezetésre került a HashCode
struktúra, ami központilag egyszerűsíti a GetHashCode
metódus felüldefiniálását:
public override int GetHashCode()
{
return HashCode.Combine(adattag1, adattag2, adattag3);
}
A Combine
statikus metódus 8 adattagig elboldogul a hash érték számítással. Ha az osztályunk több, mint 8 adattagot tartalmaz, akkor példányosítanunk kell a struktúrát, majd az Add
metódusával fel kell vennünk az adattagokat és végül a ToHashCode()
hívással kapjuk meg a hash értéket:
public override int GetHashCode()
{
var hash = new HashCode();
hash.Add(adattag1);
hash.Add(adattag2);
hash.Add(adattag3);
return hash.ToHashCode();
}
Ha olyan keretrendszerrel dolgozunk, ahol a HashCode
osztály elérhető és szükségünk van egy felüldefiniált GetHashCode()
metódusra, akkor erősen javasolt ennek az osztálynak a használata, mivel jóval biztonságosabb és robusztusabb kódot eredményez.
A GetHashCode()
esetén adódhat ötletnek, hogy mentsük le az értékét és ezt felhasználva döntsük el, hogy egy tárolt objektum egyenlő-e egy másikkal. Ez egy határozottan rossz ötlet. Biztonsági okokból egy objektum GetHashCode()
értéke két programfuttatás között eltérő értéket fog produkálni, mivel egy véletlenszerűen választott kezdő értékről indul a számítás. Ennek az oka, hogy ha minden esetben ugyan azt a determinisztikus értéket kapnánk futások között, akkor ennek a felhasználásával könnyen támadhatóvá válna az alkalmazásunk, ami webes alkalmazások esetén DoS2 támadást tenne lehetővé.
Equals(object obj)
A hash érték, ha jól van implementálva, akkor használható két objektum összehasonlítására. Ugyanis ha a hash érték egyezik, akkor a két objektum azonos, ha pedig különbözik, akkor nem egyenlőek. A .NET keretrendszer belső működésében az egyenlőség tesztelése lényegében így működik. Ha két objektum hash értéke eltér, akkor meg sem hívódik az Equals
függvény az egyenlőség tesztelésére. Viszont, ha a két hash érték azonos, akkor meghívódik a függvény, mivel az egyenlőséget okozhatta egy ütközés is.
Tehát ezen metódus a paraméterként kapott objektum összes adattagját össze kell, hogy vesse a felüldefiniált metódust tartalmazó objektum összes adattagjával. Az implementáció első lépéseként a paraméter objektumot át kell alakítani a felüldefiniálást tartalmazó típusra az as
operátor segítségével.
Ha a művelet eredménye null lesz, akkor további összehasonlításnak nincs értelme, mivel a két objektum típusa eltérő, ebben az esetben false
értéket kell visszaadni. Ha azonban az átalakítás sikeres volt, akkor minden adattagot összehasonlítunk a neki megfelelő adattaggal a másik objektumban.
Az alábbi példaprogram a három metódus felüldefiniálását mutatja be:
using System;
namespace PeldaObjectmetodusok
{
class peldaOsztaly
{
public int Egesz { get; set; }
public string Szoveg { get; set; }
public override string ToString()
{
return string.Format("{0}, {1}", Szoveg, Egesz);
}
public override int GetHashCode()
{
unchecked
{
int hash = (int)2166136261;
hash = (hash * 16777619) ^ Egesz.GetHashCode();
hash = (hash * 16777619) ^ Szoveg.GetHashCode();
return hash;
}
}
public override bool Equals(object obj)
{
if (obj == null) return false;
peldaOsztaly masik = obj as peldaOsztaly;
if (masik == null) return false;
return (Egesz == masik.Egesz) &&
(Szoveg == masik.Szoveg);
}
}
class Program
{
static void Main(string[] args)
{
peldaOsztaly a = new peldaOsztaly()
{
Egesz = 42,
Szoveg = "A objektum"
};
peldaOsztaly b = new peldaOsztaly()
{
Egesz = 33,
Szoveg = "B objektum"
};
peldaOsztaly c = new peldaOsztaly()
{
Egesz = 42,
Szoveg = "A objektum"
};
Console.WriteLine(a);
Console.WriteLine(b);
Console.WriteLine(c);
Console.WriteLine("Hash a: {0} ; b = {1} ; c = {2}",
a.GetHashCode(),
b.GetHashCode(),
c.GetHashCode());
Console.WriteLine("a.Equals(b): {0}", a.Equals(b));
Console.WriteLine("a.Equals(c): {0}", a.Equals(c));
Console.WriteLine("a == c: {0}", a == c);
Console.ReadKey();
}
}
}
A program kimenete:
A objektum, 42
B objektum, 33
A objektum, 42
Hash a: 366202889 ; b = 42693625 ; c = 366202889
a.Equals(b): False
a.Equals(c): True
a == c: False
A program működése az utolsó a == c kiírásnál okoz meglepetést, mivel a művelet eredménye false lesz. Ennek az az oka, hogy az osztályunk nem definiálta felül az egyenlőség operátort (később az operátorok átdefiniálásánál lesz róla szó). Mivel nem definiálta felül, ezért az alapértelmezett objektumokra működő operátor lesz meghívva. Ez pedig referencia típusok esetén referencia egyenlőséget figyel. Vagyis csak akkor adna a művelet igaz eredményt, ha az operátor hívása előtt azt mondanánk a kódunkban, hogy c = a.
Ebben az esetben c csak egy referenciát tárolna a korábban létező a objektumra, így a művelet eredménye igaz lenne.
Ugyanez a viselkedés igaz, ha nem definiáljuk felül az Equals
metódust. Az alapértelmezett, object
osztályból örökölt Equals
is csak referenciát hasonlít össze.
Itt kiegészítésnek megjegyzem, hogy a string
típus furcsán viselkedik ilyen szempontból, mert ő ugyan referencia típus, de érték típusként viselkedik, vagyis a ==
jel műküdését átdefiniálja. Nézzük meg az alábbi miniprogramot.
string a = "hello";
string b = "hello";
Console.WriteLine(a.Equals(b));
Console.WriteLine(a == b);
Console.WriteLine(object.ReferenceEquals(a, b));
A program kimenete:
True
True
True
Az első két összehasonlítás a korábbiak alapján rendben van, viszont meglepő módon a ReferenceEquals is megegyezik, holott elvileg két különböző referenciáról beszélünk. Ez a string internálás miatt van, ami ugyanazon string értékre csak egy területet foglal a heapben, ezért a referenciájuk is azonos lesz. Viszont ha szövegek nem azonosak, vagy műveleteket végzük rajtuk, akkor a referencia is különböző:
string a = "hello";
//hello lesz az elvégzés után, de
//compile time hello1 az értéke, ami internálódik.
string b = "hello1".Substring(0, 5);
Console.WriteLine(a.Equals(b));
Console.WriteLine(a == b);
//Itt különböző referenciát kapunk.
Console.WriteLine(object.ReferenceEquals(a, b));
A program kimenete:
True
True
False
-
Boole algebrában a kizáró vagy művelet azon állapotokat zárja ki, amikor a két bemenet azonos állapotú, vagyis csak eltérő bit kombinációk esetén ad igaz állapotot a kimenetén.↩
-
"A szolgáltatásmegtagadással járó támadás (denial-of-service vagy DoS), más néven túlterheléses támadás, illetve az elosztott szolgáltatásmegtagadással járó támadás (distributed denial-of-service, DDoS) informatikai szolgáltatás teljes vagy részleges megbénítása, helyes működési módjától való eltérítése." – https://hu.wikipedia.org/wiki/Szolg%C3%A1ltat%C3%A1smegtagad%C3%A1ssal_j%C3%A1r%C3%B3_t%C3%A1mad%C3%A1s↩