C# - Mutatók használata .NET-ben

forráskód letöltése
A mutatók – talán érthető módon – az egyik legritkábban használt elemei a C# nyelvnek. Azonban vannak olyan helyzetek - például nem-menedzselt kód szükségszerű elérésekor - amikor használatuk elkerülhetetlen. Cikkünkben a mutatók témakörébe kalandozunk, ismertetve azokat az alapvető feltételeket, melyek szükségesek a használatukhoz. Továbbá bemutatunk néhány olyan elemet, melyek különleges lehetőséget biztosítanak menedzselt kód esetében is.
A mutatók használatához a hívó metódust az unsafe kulcsszóval kell megjelölni, hogy a CLR felé jelezzük ezt a szándékunkat. A projektjeink tulajdonságlapján pedig engedélyeznünk kell az unsafe blokk használatát.
Az unsafe kulcsszó a nyelvben azt jelenti, hogy a CLR nem tudja sem azonosítani, sem megerősíteni, hogy az adott programrészlet típusai biztonságosak-e. Általában nem is lehet eldönteni, hogy egy kód típus-biztos, vagy sem, azonban a .NET futtatórendszere elhelyez néhány korlátozást arra a kódra vonatkozóan, mely mutatókat használ, és amelyet éppen IL-re fordít. A típusok biztonsága itt azt jelenti, hogy biztosított az a szabály, mely szerint egy mutató nem mutat nem-megfelelő memóriacímre, vagy egy nem-megfelelő típus memóriaterületére.
A típus-biztonság akkor használható igazán, amikor két alkalmazás vagy DLL ugyanazon a memória címtartományon osztozik, és amely megakadályozza, hogy a két program valamelyike a másik memóriaterületén található címre hivatkozzon. Ez a megoldás eddig csak úgy érvényesült, hogy az egyes programok külön process-eit képezik az operációs rendszernek, külön memóriaterülettel.
Az unsafe kulcsszóval megcímkézett kód használatának egyetlen pici hátránya van. A hagyományos pointer-ek nem generálnak kivételt abban az esetben, ha a típuskonverzió nem megfelelő. Továbbá növelhetők és csökkenthetők, valamint konvertálhatók egésszé és vissza szabadon, mellyel ismét csak azt a kockázatot növelik, hogy nem-megfelelő memóriaterületre hivatkoznak. Minden további nélkül hivatkozhatnak máshol már felszabadított memóriaterületre, vagy olyan objektumra, mely már a szemétgyűjtés áldozata lett, vagy akár a verem egy már felszabadított részére.
Ugyanakkor pont ezekért a tulajdonságokért használjuk a mutatókat különösen akkor, amikor a menedzselt kódunkból nem-menedzselt erőforrásokat, például Windows API metódusokat kell elérnünk. Könnyen tudunk ugyanis tetszőleges típusokat bájtfolyamként szerializálni, illetve deszerializálni.
Nagyon hasznos, ha a pointer-eket használó kódjainkat egy külön osztályba csoportosítjuk, jól elkülönítve a menedzselt függvényektől.
Mutató adattípus
A következőkben ismertetünk néhány hasznos fejezetet a mutatók világából. Azt már nagyon megtanulhattuk, hogy a C# nyelvben minden objektum. Ez a megállapítás talán csak mutatókra nem érvényes, hiszen ezek ugyan elismert részei a CLR-nek, azonban nem leszármazottai sem az Object, sem a ValueType osztálynak. Önmaguk gyökerei, nincs tagfüggvényük sem.
Ennek megfelelően nem dobozolhatók Object típusba, és nem hívhatók meg rájuk az olyan polimorf függvények sem, mint a Console.WriteLine. De el tudunk készíteni számtalan mutató típust a következőképpen:
Type.Create("System.Void*")
Vagy:
typeof(int*)
Ugyanakkor nem lehet például a következőképpen példányosítani őket:
Activator.CreateInstance()
Viszont indirekt módon konvertálható át IntPtr típusúvá, melynél garantálható, hogy a mérete azonos lesz a pointer által mutatott típus méretével. Az IntPtr.Size tulajdonságából lehet megtudni, hogy az adott mutatónak mekkora az aktuális mérete. Különösen hangsúlyos ez most, hogy a .NET-ben írt kódot van lehetőségünk Unix, PDA vagy Win64 rendszerben futtatni.
A deklarációkat, melyek pointer-eket használnak, meg kell jelölni a CLSCompliant attribútummal: [CLSCompliant(false)].
Amennyire nem számottevő elemei a pointer-ek a .NET Framework-nek, annyira fontos alkotói az IL-nek. A pointer-eknek alapvetően három típusa van, melyeket most egy-egy példában bemutatunk.
  • Nem-menedzselt pointer-ek
  • Menedzselt pointer-ek
  • System.TypedReference mutatók
A példák talán kicsit erőltetettnek tűnnek, de vegyük figyelembe, hogy csupán a használatuk módját áll szándékunkban bemutatni, felhívva a figyelmet néhány konverziós apróságra.
Nem-menedzselt pointer-ek
A nem-menedzselt mutatók tulajdonképpen azok a hagyományos pointer-ek, melyek minden más nyelvből, például a C++-ból ismerősek lehetnek. A nem-menedzselt mutatók csak érték-típusokra hivatkozhatnak, azokból is olyanokra, melyek nem tartalmaznak referenciát objektumokra. Ezek rengeteg lehetőséget tartalmaznak, mely a C# készítőinek azon törekvésén alapul, hogy megtartsák a nehézkes és hozzáférhetetlen objektumtípus elérésének lehetőségét.
A végezhető műveleteket tartalmazza a következő táblázat:
Művelet Jelentés
pointer++ Mutató növelése
pointer-- Mutató csökkentése
pointer + integer Pointer aritmetika
pointer - pointer Mutatók különbsége
(void *) pointer Pointer konverzió
(void *) integer Egész konverzió
A mellékelt példában néhány konkrét feladatot is elvégeztünk mutatókkal, melyekből kitűnik, hogy néhány speciális kulcsszó is használható.
A példaprogram első füle alatt egy lista elemeit másolhatjuk át egy másik listába mutató segítségével, kétféle módszerrel. Az első módszerben deklarálunk egy IntPtr típusú objektumot, melynek átadható a mutató.
IntPtr up;
A lista kiválasztott elemét átadjuk egy char típusú mutatónak.
fixed(char *p = elements[listBox1.SelectedIndex])
{
Majd a mutatót explicit módon konvertáljuk IntPtr típusra, és a mutató-objektumból nyerjük ki az éppen mutatott értéket.
  up = (IntPtr)p;
  listBox2.Items.Add(Marshal.PtrToStringAuto(up));
}
A fixed kulcsszóval a mutató a verem területén foglal le bizonyos címtartományt.
Egy másik módszer - a programunkban úgy próbálhatjuk ki, ha a jelölőnégyzetet bejelöljük az első fülön – a stackalloc kulcsszó használata, mely dinamikus veremfoglalásra ad lehetőséget. Ez a módszer nagyon gyors, és a memóriaterület felszabadul, miután a hívott metódus visszatér.
A módszer azonban korlátokkal is bír. Jól használható tömbök esetén, ha a tömb mérete nem haladja meg a néhány száz kB-ot. A függvények általában nem használnak túl sok veremterületet, hacsak nem egy mély fában kereső rekurzív függvényről van szó. Ennek érdekében kerüljük a többszörös dinamikus helyfoglalást egy időben, ellenkező esetben a StackOverflowException kivétel generálódik.
A szemétgyűjtés számára a nagyméretű (85 kB feletti), ideiglenes tömbök jelentik a legnagyobb nehézséget, mert elkülönítetten tárolódnak a HEAP-ben, és csak a teljes szemétgyűjtéssel söpörhetők ki. Ezek nem tömöríthetők, így igen nagy a memóriapazarlás.
A példánkban így foglaltunk le helyet a kiválasztott elem számára a veremben:
char *ptr = stackalloc char[elements[listBox1.SelectedIndex].Length];
Egy ciklussal kiírtuk a verembe a kiválasztott elem karaktereit sorban a mutató által mutatott memóriacímtől kezdődően.
for (int i=0;i<elements[listBox1.SelectedIndex].Length;i++)
{
  *(ptr + i) = elements[listBox1.SelectedIndex][i];
}
Majd egy másik ciklussal beolvastuk a karaktereket onnan.
string s = "";
for(int j=0;j<elements[listBox1.SelectedIndex].Length;j++)
{
  s += ptr[j].ToString();
}
listBox2.Items.Add(s);
Álljon itt most további néhány példa a mutatók konverziójával kapcsolatban. Elsőként a helyes konverziók:
char c = 'A';
char* pc = &c;
void* pv = pc;
int* pi = (int*)pv;
És amelyek hibát okoznak:
int i = *pi;
*pi = 123456;
Menedzselt mutatók
A menedzselt és a nem-menedzselt mutatók közötti alapvető eltérés, hogy az előbbieket képes a szemétgyűjtés ellenőrizni, az utóbbiakat pedig nem. A nem-menedzselt mutatók által mutatott típusok adatai egy szemétgyűjtés után, ha például a cím a HEAP egy alsó tartományába kerül át, esetleg nem lesznek elérhetők, vagy a cím hibássá válik.
Egy kivétel akad itt is. Amennyiben egy nagyméretű tömbre mutat egy másik objektum-hivatkozás is, akkor nem esik áldozatául a szemétgyűjtésnek, a cím nem szabadul fel.
A menedzselt mutatók mind Managed C++-ban, mind IL-ben elérhetőek, de C#-ban csak indirekt módon. A REF és OUT módú paraméterek is menedzselt pointer-ekként vannak implementálva.
A menedzselt mutatókkal hivatkozhatunk objektumok tagjaira is, ahogy látható lesz ez példánkban is. Ekkor a forrás elején deklarált MyClass osztályunk példányának int típusú tagjának címét helyezzük el egy mutatóban.
A menedzselt mutatók csak függvényparaméterek vagy lokális változók lehetnek, nem használhatóak struktúrákban és nem lehetnek globális vagy statikus változók.
System.TypedReference mutatók
A TypedReference mutatók alapvetően menedzselt mutatók, melyek egyidejűleg típusinformációkat is hordoznak. Felhasználásukra így azok a jellemzők érvényesek, mint a menedzselt mutatókra.
Az ilyen típusú mutatók a ValuType típusból származnak, vagyis közvetve az Object osztályból öröklik jellemzőiket. A példánk második füle alatt ki is próbálhatjuk használatát. A kód elején példányosítottuk a MyClass osztályunkat, egy véletlen számot adva át a konstruktornak paraméterül.
Random r = new Random();
c = new MyClass(r.Next(0,1000));
Az objektum tagjának címét egy egész típusú mutatóban helyezzük el, majd annak tartalmát kiírjuk a szövegmezőbe.
fixed (int *ptr = &c.a)
{   
  textBox1.Text = ptr->ToString();
}
Létrehozunk egy TypedReference típusú mutatót.
TypedReference tref = new TypedReference();
A __makeref kulcsszóval adhatunk át értéket a mutatónak.
tref = __makeref(c.a);
A típusra vonatkozó információkat a __reftype kulcsszóval kaphatjuk meg.
Type t = __reftype(tref);
...
A pointer által mutatott érték a __refvalue kulcsszóval változtatható.
__refvalue(tref,int) += 100;
Majd ezzel adható értékül egy változónak is. Viszont itt sem alkalmazható például a string típus. Ehelyett a változót tudjuk karakterlánccá konvertálni.
int res = __refvalue(tref,int);
textBox3.Text = res.ToString();
A TypedReferenece mutatótípus biztonságos kódot produkál, és jól manipulálható statikus metódusokkal is.
Ismeretes még egy igen hasznos kulcsszó, melynek segítségével a függvények paraméterlistáját lehet átadni egy lépésben. A példaprogramunk harmadik füle alatt egy lista elemeit adhatjuk át paraméterként a függvényünknek, az __arglist kulcsszó segítségével.
A függvény deklarációjában a következőt kell tenni: paraméterként csak a kulcsszót kell feltüntetni.
private void MyFunction(__arglist)
{
Majd az ArgIterator objektumnak átadni.
ArgIterator iter = new ArgIterator(__arglist);
Ekkor egy ciklussal feldolgozzuk a listát, majd minden érték címét egy TypedReference mutatónak adjuk át.
while (iter.GetRemainingCount() != 0)
{
  TypedReference tr = iter.GetNextArg();
  object o = __refvalue(tr, object);    
  label7.Text += o.ToString() + ",";
}
Mindegyiket objektumként kapjuk vissza, mely már könnyen karakterlánccá konvertálható.
A függvény hívása a következőképpen történik:
MyFunction(__arglist(listBox3.Items[0],listBox3.Items[1],listBox3.Items[2]));
GCHandle struktúra
A menedzselt objektumok nem-menedzselt kódból történő elérésének egyik módja, hogy használjuk a GCHandle struktúrát. A struktúra legfontosabb tulajdonsága, hogy akkor sem engedi a menedzselt objektumot kisöpörni a memóriából, ha már csak egy nem-menedzselt objektum hivatkozik rá (ellenkező esetben a memóriaterület felszabadul).
Azt is megakadályozza, hogy az adott cím egy más szegmensre tolódjon a memórián belül. Az azonban lényeges, hogy a nem-menedzselt kódnak rendelkeznie kell a megfelelő jogosultságokkal ehhez, melyet a SecurityPermission osztály segítségével adhatunk meg.
A programunk negyedik füle alatt ugyancsak példányosítjuk gombnyomásra a MyClass osztályt.
MyClass cc = new MyClass(150);
Létrehozzuk a GCHandle példányt.
GCHandle gc = GCHandle.Alloc(cc.a,GCHandleType.Pinned);
Kinyerjük a tagváltozó címét egy int* mutatóba.
int *ptr = (int *)gc.AddrOfPinnedObject();
Majd az így kapott mutató által mutatott címről beolvassuk a változó értékét a szövegmezőbe.
textBox4.Text = ptr->ToString();