A delegate egy olyan tÃpus a .NET keretrendszerben, ami egy, vagy több metódusra hivatkozik. Maga a delegate egy objektum, ami egy listát tárol a meghÃvandó metódusokról, amelyek rá vannak akasztva. Ez sajnos magával vonja azt, hogy ez egy referencia lesz a metódust szolgáltató osztályra is. Ez azt jelenti, hogy a szemétgyűjtÅ‘ csak akkor fogja tudni eltakarÃtani a delegate-re feliratkozott metódus osztályát, ha a delegate-rÅ‘l leiratkozunk elÅ‘ször.
Ez remek módja annak, hogy Memory Leak1-et készÃtsünk C# nyelven. Felmerülhet akkor a kérdés, hogy mégis mi értelme az egésznek?
A delegate tÃpus legnagyobb elÅ‘nye, hogy segÃtségével egy metódusnak egy másik metódust tudunk paraméterként átadni, ami hasonlóvá teszi Å‘ket a C függvény pointer megoldásához.
Nézzünk egy példát, ami bemutatja a delegate tÃpus elÅ‘nyét. Tételezzük fel, hogy egy olyan kódrészletet kell Ãrnunk, ami egy tömb értékeit módosÃtja valamilyen logika szerint, de ezt a logikát a fejlesztés pillanatában nem ismerjük, vagy magát a tömböt nem szeretnénk kipublikálni az osztályból.
Ekkor deklarálhatunk egy metódust, ami egy delegate-en keresztül a tömb összes elemére meghÃv egy módosÃtó metódust, ami már lehet az osztályon kÃvül is.
using System;
using System.Collections;
namespace PeldaDelegate
{
delegate int Modosito(int bemenet);
class DelegateDemo: IEnumerable
{
private int[] _tomb;
public DelegateDemo()
{
_tomb = new int[] { 1, 2, 3, 4, 5 };
}
public IEnumerator GetEnumerator()
{
return _tomb.GetEnumerator();
}
public void Modosit(Modosito modosito)
{
if (modosito == null) return; // hibavédelem
for (int i=0; i<_tomb.Length; i++)
{
_tomb[i] = modosito(_tomb[i]);
}
}
}
class Program
{
static void Main(string[] args)
{
var delegateDemo = new DelegateDemo();
Kiir(delegateDemo);
delegateDemo.Modosit(Negyzet);
Kiir(delegateDemo);
delegateDemo.Modosit(Duplaz);
Kiir(delegateDemo);
Console.ReadKey();
}
private static void Kiir(DelegateDemo delegateDemo)
{
Console.WriteLine("Elemek:");
foreach(var item in delegateDemo)
{
Console.WriteLine(item);
}
Console.WriteLine();
}
private static int Duplaz(int bemenet)
{
return 2 * bemenet;
}
private static int Negyzet(int bemenet)
{
return bemenet * bemenet;
}
}
}
A program kimenete:
Elemek:
1
2
3
4
5
Elemek:
1
4
9
16
25
Elemek:
2
8
18
32
50
A kódból jól látható, hogy a tÃpus definiálása csupán annyiban tér el egy metódus deklarációjától, hogy a delegate kulcsszó megelÅ‘zi, valamint a metódus belseje nincs kifejtve, mivel ez az implementáció dolga.
A kódból az is látszik, hogy a delegate ugyanúgy meghÃvható, mint egy metódus a felhasználási helyén. Fontos viszont, hogy csak olyan metódust lehet a delegate helyére behelyettesÃteni, ami megfelel a definiált metódus szignatúrának: jelen esetben int visszatérési érték, és egy darab int paraméter.
Delegate tÃpus csak osztályon belüli tagként, vagy osztályon kÃvül névtér elemeként deklarálható, vagyis metóduson, tulajdonságon belül nem deklarálhatunk ilyen tÃpusokat.
Több feliratkozó
Ha egy delegate adattagként van kipublikálva, akkor több metódus implementációt is ráakaszthatunk a += operátorral:
public delegate void Foo();
class AdattagDelegate
{
public Foo FooDelegate;
}
var x = new AdattagDelegate();
x.FooDelegate += Feliratkozo1;
x.FooDelegate += Feliratkozo2;
Ebben az esetben a FooDelegate meghÃvásakor az összes feliratkozó sorban meghÃvásra kerül. A feliratkozók listáját a delegált tÃpus példányán a GetInvocationList() metódussal lehet lekérdezni, ami egy tömbben visszaadja az összes feliratkozó metódust. Ezek közül manuálisan is hÃvhatunk feliratkozókat.
A fentebb emlÃtett példakód esetén leiratkozni sem árt. Leiratkozni a -= operátorral tudunk, ami után a leiratkozni kÃvánt metódus nevét kell megadni.
Nem gyÅ‘zöm hangsúlyozni, hogy leiratkozás elmulasztása esetén a delegált metódust kipublikáló osztály és a tényleges kód implementációt tartalmazó osztály között egy referencia marad fent. Ez azt jelenti, hogy az osztályok nem lesznek felszabadÃtva a szemétgyűjtÅ‘ által, vagyis memória szivárgás fog keletkezni.
Mikor egyenlő két delegate?
A rövid válasz itt a sosem lenne. Két delegate még akkor sem egyenlÅ‘, ha megegyezik a felépÃtésük és a paraméterlistájuk, mert név szerint azonosÃtottak.
namespace PeldaDelegate2
{
class Program
{
public delegate void Foo();
public delegate void Bar();
public static void Valami() { }
static void Main(string[] args)
{
Foo x = Valami;
//Hibát fog eredményezni:
//Bar y = x;
}
}
}
Azonban egy adott tÃpusú delegate példányai között lehet értelmezni az egyenlÅ‘ség kérdését, sÅ‘t a == és a != operátorok is rendelkezésre állnak.
using System;
namespace PeldaDelegate3
{
class Program
{
public delegate void Foo();
static void Implementacio1() { }
static void Implementacio2() { }
static void Main(string[] args)
{
Foo test1 = null;
Foo test2 = null;
Console.WriteLine(test1 == test2); //true
test1 = Implementacio1;
test2 = Implementacio2;
Console.WriteLine(test1 == test2); //false
test1 = Implementacio1;
test2 = Implementacio1;
Console.WriteLine(test1 == test2); //true
Console.ReadKey();
}
}
}
A program kimenete:
True
False
True
Kovariáns és kontravariáns viselkedés
A delegáltak bemeneti tÃpusok tekintetében kontravariáns viselkedést valósÃtanak meg:
namespace PeldaDelegate4
{
class Allat { }
class Novenyevo : Allat { }
class Husevo : Allat { }
delegate void HusevoViselkedes(Husevo husevo);
class Program
{
static void AltalanosAllatViselkedes(Allat allat) { }
static void Main(string[] args)
{
//megengedett kontravariáns viselkedés
HusevoViselkedes d = AltalanosAllatViselkedes;
}
}
}
Viszont, ha a delegált tÃpus visszatérési tÃpusáról beszélünk, akkor kovariáns viselkedés definiált:
using System;
namespace PeldaDelegate5
{
class Allat { }
class Novenyevo : Allat { }
class Husevo : Allat { }
delegate Allat AllatKeszito();
class Program
{
static Husevo HusevoKeszito() { return new Husevo(); }
static Novenyevo NovenyevoKeszito() { return new Novenyevo(); }
static void Main(string[] args)
{
AllatKeszito hivo = HusevoKeszito;
Allat allat = hivo();
Console.WriteLine(allat.GetType().Name); //Husevo
hivo = NovenyevoKeszito;
allat = hivo();
//castolni kell.
//Pattern match vagy as operátorral szebb és jobb lenne
Novenyevo cast = (Novenyevo)allat;
Console.WriteLine(cast.GetType().Name); //Novenyevo
Console.ReadKey();
}
}
}
A program kimenete
Husevo
Novenyevo
A fenti két példában a delegate definiciója szerint olyan metódusra tud mutatni, ami void visszatérésű és egy Husevo parametere van. Ehhez képest kap egy metódust, ami ugyan void visszatérésű, de Allat parametere van. Mindez az öröklés szabályai miatt működik, miszerint a Husevo az egy Allat.
Névtelen metódusok
A delegate-et megvalósÃtó metódust deklarálhatjuk a hÃvás helyén is. Ekkor egy névtelen metódust kapunk, mivel nem tudunk rá névvel hivatkozni. Ez megkönnyÃti a program Ãrást, fÅ‘leg akkor ha a delegate-et megvalósÃtó kód csak egy-két sorból, utasÃtásból áll. Cserébe viszont a metóduson belül nem használhatunk ugró utasÃtásokat (goto, break, continue).
További limitáció, hogy unsafe blokk nem használható benne, valamint a külső, a deklarációt tartalmazó blokk in, out és ref paramétereihez nem férünk hozzá, viszont a normál változókhoz igen.
using System;
namespace PeldaDelegate6
{
class Program
{
delegate void Foo(int p);
static void Main(string[] args)
{
Foo foo = delegate (int parameter)
{
Console.WriteLine("parameter: {0}", parameter);
};
foo(2);
Console.ReadKey();
}
}
}
A program kimenete:
parameter: 2
Névtelen metódusokkal még könnyebb memory leak-et eredményezni, mivel ezen metódusok nem tudnak leiratkozni egy delegate-ről, mert ugye nincs nevük, ami elengedhetetlen lenne a leiratkozáshoz.
Névtelen metódusok és lokális változók
A névtelen metódusoknak hozzáférésük van a lokális változókhoz, szóval az alábbi kód lefordul és működőképes:
using System;
namespace PeldaDelegate7
{
class Program
{
delegate void Pelda();
private static void Main(string[] args)
{
Pelda[] peldak = new Pelda[10];
for (int i = 0; i < 10; i++)
{
peldak[i] = () => Console.Write(i + " ");
}
foreach (var pelda in peldak)
{
pelda();
}
}
}
}
A program kimenete:
10 10 10 10 10 10 10 10 10 10
Azonban nem feltétlen azt teszi, amit elvárnánk. Logikus feltételezés lenne, hogy a peldak tömb bejárásában pelda() hÃváskor 0 és 9 közötti értékeket kapjunk. Mint láthatjuk, nem ez történik. Ennek az az oka, hogy a lambda metódus létrehozásakor a háttérben egy külön osztály generálódik, amibe átmásolódik az i változó értéke minden egyes alkalommal, amikor a peldak[i] tömbnek értéket adunk. Az i++ és a for ciklus feltétele miatt az i változó fel fog venni egy 10-es értéket. Ugyan a 10. elemnek nem adunk értéket, mert a ciklus magja nem hajtódik végre, de ez az érték átmásolódik a névtelen delegate-et megvalósÃtó osztályba is, mivel az i változó a két osztály között megosztottan létezÅ‘dik. Ezt nevezi a C# nyelv Variable Closure-nek.
-
Programhiba, amelynek következtében a már használaton kÃvüli memóriablokkok sem kerülnek felszabadÃtásra, és ami Ãgy hosszú távon a szabad memória teljes elfogyását okozza. – https://pcforum.hu/szotar/memory+leak↩