Grafikus C# – Windows Forms tanfolyam › 9.8 lecke

Windows Forms · 9.8 lecke · erőforrás-kezelés

Képek és grafikus erőforrások felszabadítása

A C# a memóriát magától kezeli (szemétgyűjtő), a grafikai objektumok mögött azonban rendszererőforrások állnak: ablakkezelők, képpont-tömbök, rajzeszközök. Ezeket a szemétgyűjtő nem látja pontosan, ezért te felelsz a felszabadításukért. Ha elmulasztod, a program lassul, egyre több memóriát eszik, végül furcsa hibákkal (A generic error occurred in GDI+, OutOfMemoryException) leáll. Ebben a leckében megtanulod a Dispose és a using szabályait, azt, hogy mit nem szabad felszabadítani, a saját osztály IDisposable megvalósítását, és hogyan mérheted a szivárgást.

  • kb. 100 perc
  • közepes–haladó szint
  • Dispose és using
Anime stílusú fiú és lány egy C# programozási feladatot beszél meg

A lecke végére

  • tudod, mely objektumokat kell felszabadítani (Pen, Brush, Font, Bitmap, Image, Graphics.FromImage, Region, GraphicsPath), és melyeket nem (e.Graphics, Pens, Brushes);
  • a using utasítással és (ha a projekt támogatja) a using deklarációval dolgozol;
  • a hosszú életű erőforrásokat a Form zárásakor szabadítod fel;
  • saját osztályt IDisposable felülettel látsz el;
  • méred a szivárgást (memória, GDI-objektumok száma), és felismered a tüneteit;
  • kezeled az ObjectDisposedException hibát, és a tulajdonjog szabályát (aki létrehozza, az szabadítja fel).

Mielőtt elkezded

Ehhez a leckéhez a 9.1–9.4 lecke (Bitmap, Graphics, Pen, Brush) és az 5.6–5.7 lecke (a Form életciklusa, FormClosed) szükséges. Az IDisposable mintát a 12. fejezet általánosabban tárgyalja.

9.7 Alakzatok →

Miért nem elég a szemétgyűjtő?

A szemétgyűjtő (garbage collector, GC) a memóriát szabadítja fel, amikor úgy ítéli, hogy szükség van rá. A Bitmap, Pen, Font, Graphics ugyanakkor egy kis felügyelt „burok” egy nem felügyelt erőforrás körül (GDI+ objektum, ablakkezelő, DC). A GC két dolgot nem tud jól:

1. Nem tudja, mekkora a mögötte lévő memória

Egy 4000 × 3000 pixeles Bitmap felügyelt része néhány tíz bájt, a nem felügyelt része viszont ~46 MB. A GC csak az előbbit látja, ezért „nem sürgős” a felszabadítás: a memória hamarabb fogy el, mint ahogy a GC lefutna.

2. Nem tudja, hány rendszerkezelő fogy

A Windows folyamatonként korlátozott számú GDI-kezelőt enged (alapból 10 000). Ha elfogy, a rajzolás hibát ad (Parameter is not valid, A generic error occurred in GDI+), vagy az ablak üres marad. A GC csak jóval később, a finalizerekkel szabadítja fel.

DisposeAz IDisposable felület Dispose() metódusa azonnal felszabadítja a nem felügyelt erőforrást. Aki IDisposable objektumot létrehoz, azt a használat végén Dispose-olnia kell.
FinalizerHa elfelejted, a GC egyszer majd lefuttatja az objektum finalizerét, és az felszabadítja az erőforrást. De nem kiszámítható, mikor: addig az erőforrás foglalt marad.
A tünetekNövekvő memóriahasználat, lassulás hosszú futás után, nem látszó vagy fehér rajz, OutOfMemoryException vagy általános GDI+ hiba, olyan, amely „csak egy óra után” jelentkezik.
Gyakori okA Paint kezelő vagy az időzítő ismétlődően új Pen, Brush, Font, Bitmap objektumot készít, és nem szabadítja fel: pár perc alatt több tízezer objektum gyűlik össze.

Jósolj!

Egy időzítő 50 ms-onként egy 1000 × 1000 pixeles Bitmapot hoz létre, és soha nem szabadítja fel; semmi nem hivatkozik rájuk. Mi történik 10 perc alatt a program memóriájával?

Jóslat ellenőrzése

A memóriahasználat gyorsan és tartósan nő (a Bitmap ~4 MB, 10 perc alatt ez 12 000 objektum, ami több gigabájt), mert a GC nem érzékeli a nem felügyelt méretet, és ritkán futtatja a finalizereket. Pontos viselkedés gépenként és verziónként eltérő, de a Dispose nélküli kód mindig kockázatos.

A Dispose és a using utasítás

Form1.csC#
// 1. kézi Dispose: hibás, ha közben kivétel keletkezik (a Dispose kimarad)
Pen toll = new Pen(Color.Black, 2);
g.DrawLine(toll, 0, 0, 100, 100);
toll.Dispose();

// 2. using utasítás: a blokk végén (kivétel esetén is) automatikusan Dispose
using (Pen toll2 = new Pen(Color.Black, 2))
{
g.DrawLine(toll2, 0, 0, 100, 100);
}

// több erőforrás egymás után: egy blokkba fűzhetők (a belső előbb szabadul fel)
using (Bitmap kep = new Bitmap(400, 300))
using (Graphics g2 = Graphics.FromImage(kep))
using (Brush ecset = new SolidBrush(Color.Gold))
{
g2.FillRectangle(ecset, 0, 0, 400, 300);
kep.Save(utvonal, System.Drawing.Imaging.ImageFormat.Png);
} // itt szabadul fel: ecset, g2, kep (fordított sorrendben)

// 3. using deklaráció (C# 8, .NET Core 3.0 / .NET 5+ projektben): a metódus végén szabadul fel
// using var toll3 = new Pen(Color.Red); // régi .NET Framework projektben nem fordul le
Mi a using?A using (x) { ... } egy try–finally rövidítése: a finally ágban hívja az x.Dispose() metódust. Így akkor is felszabadul az erőforrás, ha a blokkban kivétel keletkezik.
Fordított sorrendEgymásba fűzött using esetén a legutóbb létrehozott szabadul fel elsőnek: a Graphics előbb, mint a Bitmap, amelyre rajzol. Fordítva hibát okozna.
using deklarációA using var x = ...; rövidebb (nincs blokk), és a tartalmazó blokk (metódus) végén szabadít fel. Csak újabb nyelvi verzióban (C# 8) fordul; a régi .NET Framework projektben (0.4 lecke) a blokkos alak kell.
Mikor nem jó a using?Ha az objektumnak a metódus után is élnie kell (például a PictureBoxnak adott kép, osztály mezője), nem szabad using blokkba tenni: akkor a mezőben tárolt objektumot az szabadítja fel, aki a mezőt birtokolja (lásd lent).

Jósolj!

A következő kód a using blokkon belül jön létre a Bitmap, majd a PictureBoxnak adja át: using (Bitmap b = new Bitmap(100, 100)) { pbKep.Image = b; }. Mit lát a felhasználó?

Jóslat ellenőrzése

A PictureBox a következő rajzoláskor piros X-et (vagy kivételt) mutat: a using a blokk végén felszabadította a képet, amelyet a doboz még használ. A megjelenített kép felszabadítása a doboz (és a kép) birtokosának dolga, és csak a kicserélése után történhet (9.1 lecke).

Mit kell és mit NEM szabad felszabadítani?

ObjektumFelszabadítsam?Miért?
new Pen, new SolidBrush, new Font, new StringFormatIgenTe hoztad létre, rendszererőforrás.
new Bitmap, Image.FromFile, Image.FromStreamIgenMemória és fájl-/adatfolyam-kezelő.
Graphics.FromImage, CreateGraphics, Graphics.FromHwndIgenTe kérted: rajzfelület (DC) tartozik hozzá.
GraphicsPath, Region, Matrix, ImageAttributesIgenGDI+ erőforrások.
Properties.Resources.kepIgen (vagy tárold változóban)Minden hívás új példányt ad.
e.Graphics a Paint eseménybenNemA keretrendszeré: ő szabadítja fel.
Pens.Black, Brushes.Red, SystemFonts, SystemColorsNemKözös, gyorsítótárazott objektumok (módosítani sem lehet).
A vezérlő Font tulajdonsága (örökölt)NemA vezérlő birtokolja; a saját cserélt betűt viszont te szabadítod.
A PictureBox.Image értékeTeA doboz általában nem szabadítja fel a hozzárendelt képet.

A tulajdonjog szabálya

Aki létrehozza az objektumot, az szabadítja fel, és csak akkor, amikor már senki nem használja. Ha átadsz egy képet egy másik osztálynak (PictureBox, saját osztály), előre tisztázd, ki a tulajdonos. A zavar (kétszeri felszabadítás, vagy használat felszabadítás után) a leggyakoribb hiba: ObjectDisposedException.

Form1.csC#
// a Form mezői: hosszú életű, gyakran használt erőforrások (nem készítjük újra minden Paint-ben)
private Pen racsToll = new Pen(Color.Gainsboro);
private Font cimBetu = new Font("Segoe UI", 16, FontStyle.Bold);
private Bitmap hatter;

private void Form1_FormClosed(object sender, FormClosedEventArgs e)
{
racsToll.Dispose();
cimBetu.Dispose();

if (hatter != null) { hatter.Dispose(); }

if (pbKep.Image != null) { pbKep.Image.Dispose(); } // a doboz képe
}
Mikor hosszú életű?A ritkán változó, gyakran használt erőforrást (rács tolla, címbetű) érdemes egyszer létrehozni, és a Form zárásakor szabadítani. A Paint-ben minden alkalommal létrehozott Font és Bitmap drága; a Pen, Brush kevésbé.
FormClosedA Form zárásakor (5.6 lecke) a FormClosed eseménybe írjuk a felszabadítást. A Designer által generált Dispose(bool) metódust ne módosítsd (a Designer.cs fájl újragenerálódik).
Kicserélt képÚj kép beállításakor előbb az újat kell beállítani, utána a régit Dispose-olni (9.1 lecke Mutat metódusa).
Null és kétszeri DisposeAz IDisposable a legtöbb osztálynál idempotens: a második Dispose() hívás nem hibás. A null ellenőrzés viszont kell (?.Dispose() újabb nyelvi verzióban).

Jósolj!

A Paint kezelő végén Brushes.Red.Dispose(); sor áll. Mi történik a következő rajzoláskor, amikor valahol újra Brushes.Red-et használsz?

Jóslat ellenőrzése

A Brushes.Red közös (gyorsítótárazott) objektum: a felszabadítása után a program hibát (ArgumentException vagy ObjectDisposedException) ad, ha valamely rajzolás újra használja. Ezért a Brushes és a Pens osztályokét soha ne szabadítsd fel.

Saját osztály IDisposable felülettel

Ha egy osztályod IDisposable mezőt tart (Bitmap, Pen), akkor az osztály is IDisposable legyen: a Dispose a belső erőforrásokat szabadítja fel. Így a használó a megszokott módon (using) kezelheti. A 9.6–9.7 lecke rajzoló modelljének pufferképe például így nézhet ki:

RajzModell.csC#
public class RajzModell : IDisposable
{
private Bitmap vaszon;
private Pen toll;
private bool felszabaditva = false;

public RajzModell(int szelesseg, int magassag)
{
vaszon = new Bitmap(szelesseg, magassag);
toll = new Pen(Color.Black, 2);

using (Graphics g = Graphics.FromImage(vaszon)) { g.Clear(Color.White); }
}

public void Vonal(Point a, Point b)
{
EllenorizFelszabaditas(); // felszabadított objektum használata: érthető hiba

using (Graphics g = Graphics.FromImage(vaszon))
{
g.DrawLine(toll, a, b);
}
}

public Bitmap Kep { get { EllenorizFelszabaditas(); return vaszon; } }

private void EllenorizFelszabaditas()
{
if (felszabaditva)
{
throw new ObjectDisposedException(nameof(RajzModell));
}
}

public void Dispose()
{
if (felszabaditva) { return; } // kétszeri hívás ártalmatlan

felszabaditva = true;

if (toll != null) { toll.Dispose(); toll = null; }
if (vaszon != null) { vaszon.Dispose(); vaszon = null; }
}
}

// használat: a Formban mezőként, a FormClosed-ben Dispose; vagy rövid életű using blokkban
using (RajzModell modell = new RajzModell(400, 300))
{
modell.Vonal(new Point(0, 0), new Point(100, 100));
modell.Kep.Save("rajz.png");
}
IDisposable megvalósításaEgyszerű esetben (csak felügyelt IDisposable mezők, nincs saját nem felügyelt erőforrás) a Dispose() a mezők Dispose hívása, finalizer nélkül. A teljes „Dispose-minta” (Dispose(bool), finalizer) csak közvetlen nem felügyelt erőforrásnál kell.
IdempotensA felszabaditva jelző megakadályozza a kétszeri felszabadítást. A mezők null értékre állítása a további használatot hibássá teszi (érthető hibával, ha ellenőrzöd).
ObjectDisposedExceptionA felszabadított objektum használatára a .NET ObjectDisposedException kivételt dob. A saját osztályodban is ezt érdemes dobni, hogy a hiba érthető legyen.
Tulajdonos a FormHa az osztályt a Form mezőjeként tartod, a FormClosed-ben hívd a Dispose() metódust. Ez az „aki létrehozza, szabadítja” szabály alkalmazása.

Jósolj!

A RajzModell Dispose() metódusát meghívod, majd még egyszer meghívod, és utána a modell.Kep tulajdonságot olvasod. Mi történik?

Jóslat ellenőrzése

A második Dispose() ártalmatlan (a jelző miatt nem csinál semmit), a Kep olvasása viszont ObjectDisposedException kivételt dob (érthető üzenettel: a RajzModell felszabadított objektum).

A szivárgás mérése

A szivárgás gyanúját mérni lehet: (1) a Feladatkezelőben (Részletek fül, oszlopok: Memória (privát munkakészlet), GDI-objektumok), vagy (2) a saját programból, kiírva az állapotsorba:

Form1.cs (using System.Diagnostics; using System.Runtime.InteropServices;)C#
// a folyamat GDI-objektumainak száma (Windows API): 0 = GDI objektumok, 1 = USER objektumok
[DllImport("user32.dll")]
private static extern uint GetGuiResources(IntPtr folyamat, uint jelzo);

private int GdiObjektumok()
{
using (Process p = Process.GetCurrentProcess())
{
return (int)GetGuiResources(p.Handle, 0);
}
}

private long PrivatMemoriaMB()
{
using (Process p = Process.GetCurrentProcess())
{
p.Refresh();
return p.PrivateMemorySize64 / (1024 * 1024);
}
}

private void tmrMeres_Tick(object sender, EventArgs e)
{
tsslMeres.Text = "Memória: " + PrivatMemoriaMB() + " MB GDI-objektumok: " + GdiObjektumok();
}

// a finalizerek kényszerítése: csak kísérletnek, a valós programban NEM megoldás!
GC.Collect();
GC.WaitForPendingFinalizers();
GDI-objektumokA GetGuiResources a folyamat által birtokolt GDI-kezelők számát adja (a Feladatkezelő „GDI-objektumok” oszlopa). Normális program néhány száz; ha percek alatt ezrekre nő, szivárgás van (a határ alapból 10 000). A CreateGraphics() felszabadítás nélkül, a GetHbitmap() és az ikonok kezelői jól látszanak itt.
Privát memóriaA PrivateMemorySize64 a folyamat privát memóriáját adja. A felszabadítatlan Bitmapok miatt ez nő. A számok gépenként és .NET-verziónként eltérnek, a trend (nő-e folyamatosan) az érdekes.
GC.CollectA GC.Collect() és a GC.WaitForPendingFinalizers() kísérletre jó: megmutatja, hogy a finalizerek lefutása után a memória csökken. Éles programban nem javítja a hibát: a Dispose hívás a megoldás.
P/InvokeA DllImport a Windows beépített függvényét hívja (a „platform meghívás”): haladó téma, itt csak mérésre használjuk. Más operációs rendszeren (a .NET MAUI, 15. fejezet) nem működik.

Jósolj!

Egy gombnyomás 3000-szer meghívja a pnlRajz.CreateGraphics() metódust Dispose nélkül. Mit mutat a GDI-objektumok száma, és mi történik, ha a kód 10 000 hívásig futna?

Jóslat ellenőrzése

A GDI-objektumok száma ezrekkel nő (minden CreateGraphics egy DC-t foglal). A 10 000-es határnál a rajzolás meghibásodik (általános GDI+ hiba, vagy a vezérlők üresen maradnak). A finalizerek lefutásáig a kezelők nem szabadulnak fel; a pontos viselkedés a rendszeren múlik.

📷 Képernyőkép helye

Mit tegyél ide: a szivárgásvadász program a mérőszámokkal és a gombokkal, valamint a Feladatkezelő „GDI-objektumok” oszlopa.Javasolt fájlnév: winforms-9-8-szivargas.webp · Alt szöveg: A szivárgásvadász program

Hibakeresés: te vagy a hibadetektív

Mi a baj, és hogyan javítod?

Gondold végig az okot, és csak utána nyisd le a megoldást.

1. A program memóriája folyamatosan nő

private void tmrAnim_Tick(object sender, EventArgs e)
{
pbKep.Image = new Bitmap(400, 300); // a régi kép soha nem szabadul fel
}
Hiba és javítás

A régi kép felszabadítása hiányzik. Őrizd meg a régit, állítsd be az újat, majd régi.Dispose() (lásd a 9.1 lecke Mutat metódusát).

2. Piros X a képen

A PictureBox képe hirtelen piros X-szel jelenik meg.

Ok és javítás

Valahol felszabadítottad azt a képet, amelyet a doboz még használ (using blokk vagy kézi Dispose). A megjelenített képet ne szabadítsd fel; csak a kicseréltet.

3. ArgumentException a Brushes használatakor

using (Brush b = Brushes.Red)   // a közös ecset használata using-gal
{
g.FillRectangle(b, 0, 0, 50, 50);
}
Hiba és javítás

A Brushes.Red közös: a using felszabadítaná. Közvetlenül használd (g.FillRectangle(Brushes.Red, ...)), vagy hozz létre saját ecsetet: new SolidBrush(Color.Red).

4. A fájl nem törölhető a kép megjelenítése után

A képfájl zárolva marad.

Ok és javítás

Az Image.FromFile képet nem szabadítod fel (vagy a program a fájlból tölti közvetlenül). Töltsd másolattal (new Bitmap(forras)) és szabadítsd a forrást (9.1 lecke).

5. Image.FromStream után a kép elromlik

using (MemoryStream ms = new MemoryStream(bajtok))
{
kep = Image.FromStream(ms); // a kép az adatfolyamra támaszkodik
} // az adatfolyam lezárult: a kép használata hibás
Hiba és javítás

Az Image.FromStream által adott képnek az adatfolyam a kép teljes élettartama alatt nyitva kell maradnia. Másolatot készíts a blokkon belül: kep = new Bitmap(Image.FromStream(ms));, a közbülső képet felszabadítva.

6. Kétszeri felszabadítás kivételt dob

A program ObjectDisposedException kivétellel áll le zárás közben.

Ok és javítás

Ugyanazt az objektumot két helyen szabadítod fel, vagy már felszabadított objektumot használsz. Tisztázd a tulajdonost; a saját osztály Dispose metódusa legyen idempotens, és ellenőrizd a használatot.

Mini projekt: szivárgásvadász

Készíts kísérleti programot, amellyel látod a szivárgás hatását, és a helyes mintát:

  1. Mérés: az tmrMeres (500 ms) kiírja az állapotsorba a privát memóriát (MB) és a GDI-objektumok számát (a lecke GdiObjektumok és PrivatMemoriaMB metódusa); egy kis Chart-szerű vonal a mérések történetéhez (List<long>, a panel Paint eseményében rajzolva, 9.9 lecke előkészítése).
  2. 1. kísérlet (Bitmap): 100 Bitmap felszabadítás nélkül és 100 Bitmap using-gal gomb (1000 × 1000 pixel, mindegyikre egy vonal rajzolva); a mérés mutatja a különbséget; a GC.Collect gomb megmutatja, hogy a finalizerek lefutása után csökken a memória.
  3. 2. kísérlet (GDI-kezelők): 1000 CreateGraphics felszabadítás nélkül és using-gal gomb; a GDI-objektumok számának változása.
  4. 3. kísérlet (PictureBox): a Képcsere helyesen és Képcsere szivárgással gomb 200 képet cserél egymás után a pbKep dobozban (a helyes: az új beállítása után a régi Dispose).
  5. 4. kísérlet (ObjectDisposedException): a Használat felszabadítás után gomb a RajzModell felszabadítása után hívja a Kep tulajdonságot; a kivétel érthető üzenettel látszik az állapotsorban (nem fagy le).
  6. Szabálylista: a Formon egy olvasható „Felszabadítási szabályok” panel (aki létrehozza, szabadítja; using; e.Graphics, Pens, Brushes nem; régi kép a csere után; hosszú életű erőforrás a FormClosed-ben).
  7. Biztonság: a kísérletek gombjai a futás alatt letiltottak; a nagy memóriás kísérletek mennyisége korlátozott (hogy ne fogyjon el a gép memóriája); a Visszaállítás gomb a GC.Collect() hívással tisztít.
  8. Teszteld: minden kísérlet előtt és után a mérőszámok, a Feladatkezelő oszlopai, a helyes és a hibás változat különbsége, a program stabilitása.
Megoldás megnyitása (próbáld ki előbb önállóan!)

A kísérletek a kódban rövid, ismétlődő ciklusok. A „szivárgó” változat szándékosan hibás kód (csak a kísérlethez!): a valós programban soha nem írunk ilyet.

Form1.cs (using System.Diagnostics; using System.Runtime.InteropServices; using System.Collections.Generic; using System.Linq;)C#
[DllImport("user32.dll")]
private static extern uint GetGuiResources(IntPtr folyamat, uint jelzo);

private List<long> elozmeny = new List<long>();

private int GdiObjektumok()
{
using (Process p = Process.GetCurrentProcess()) { return (int)GetGuiResources(p.Handle, 0); }
}

private long PrivatMemoriaMB()
{
using (Process p = Process.GetCurrentProcess())
{
p.Refresh();
return p.PrivateMemorySize64 / (1024 * 1024);
}
}

private void tmrMeres_Tick(object sender, EventArgs e)
{
long mb = PrivatMemoriaMB();

elozmeny.Add(mb);

if (elozmeny.Count > 120) { elozmeny.RemoveAt(0); }

tsslMeres.Text = "Memória: " + mb + " MB GDI-objektumok: " + GdiObjektumok();
pnlGrafikon.Invalidate();
}

private void pnlGrafikon_Paint(object sender, PaintEventArgs e)
{
Graphics g = e.Graphics;
g.Clear(Color.White);

if (elozmeny.Count < 2) { return; }

long max = Math.Max(1, elozmeny.Max());
PointF[] pontok = new PointF[elozmeny.Count];

for (int i = 0; i < elozmeny.Count; i++)
{
pontok[i] = new PointF(i * (pnlGrafikon.Width - 1f) / (elozmeny.Count - 1), pnlGrafikon.Height - 1 - elozmeny[i] * (pnlGrafikon.Height - 2f) / max);
}

using (Pen toll = new Pen(Color.SteelBlue, 2)) { g.DrawLines(toll, pontok); }
}

private void Kiserlet(Action munka)
{
pnlKiserletek.Enabled = false;

try { munka(); }
finally { pnlKiserletek.Enabled = true; }
}

private void btnBitmapSzivarog_Click(object sender, EventArgs e)
{
Kiserlet(() =>
{
for (int i = 0; i < 100; i++)
{
Bitmap b = new Bitmap(1000, 1000); // SZÁNDÉKOSAN hibás: nincs Dispose

using (Graphics g = Graphics.FromImage(b)) { g.DrawLine(Pens.Black, 0, 0, 999, 999); }
}
});
}

private void btnBitmapUsing_Click(object sender, EventArgs e)
{
Kiserlet(() =>
{
for (int i = 0; i < 100; i++)
{
using (Bitmap b = new Bitmap(1000, 1000))
using (Graphics g = Graphics.FromImage(b))
{
g.DrawLine(Pens.Black, 0, 0, 999, 999);
}
}
});
}

private void btnGdiSzivarog_Click(object sender, EventArgs e)
{
Kiserlet(() =>
{
for (int i = 0; i < 1000; i++) { Graphics g = pnlGrafikon.CreateGraphics(); } // SZÁNDÉKOSAN hibás
});
}

private void btnGdiUsing_Click(object sender, EventArgs e)
{
Kiserlet(() =>
{
for (int i = 0; i < 1000; i++) { using (Graphics g = pnlGrafikon.CreateGraphics()) { } }
});
}

private void btnGc_Click(object sender, EventArgs e)
{
GC.Collect();
GC.WaitForPendingFinalizers();
GC.Collect();
}

private void KepCsere(bool helyes)
{
for (int i = 0; i < 200; i++)
{
Bitmap uj = new Bitmap(600, 400);

using (Graphics g = Graphics.FromImage(uj)) { g.Clear(Color.FromArgb(i, 255 - i, 128)); }

Image regi = pbKep.Image;

pbKep.Image = uj;

if (helyes && regi != null) { regi.Dispose(); }
}
}

private void btnKepHelyes_Click(object sender, EventArgs e) { Kiserlet(() => KepCsere(true)); }
private void btnKepSzivarog_Click(object sender, EventArgs e) { Kiserlet(() => KepCsere(false)); }

private void btnFelszabaditott_Click(object sender, EventArgs e)
{
RajzModell m = new RajzModell(100, 100);

m.Dispose();

try
{
Bitmap b = m.Kep;
}
catch (ObjectDisposedException ex)
{
tsslMeres.Text = "ObjectDisposedException: " + ex.ObjectName;
}
}

private void Form1_FormClosed(object sender, FormClosedEventArgs e)
{
if (pbKep.Image != null) { pbKep.Image.Dispose(); }
}
📷 Képernyőkép helye

Mit tegyél ide: a szivárgásvadász a memória-grafikonnal: a szivárgó kísérlet után emelkedő, a using után lapos vonal.Javasolt fájlnév: winforms-9-8-kesz.webp · Alt szöveg: A szivárgásvadász és a memóriagrafikon

Három önálló gyakorlófeladat

Egyszerre csak egyet csinálj: tervezd meg, írd meg, futtasd, ellenőrizd, és csak utána nyisd ki a megoldást.

1. Gyorsítótárazott tollak

Írj EszkozTar osztályt, amely a kért színű és vastagságú tollat egyszer hozza létre, és újra visszaadja, a Dispose pedig mindet felszabadítja. Melyik adatszerkezetet használod?

Egy lehetséges megoldás

Szótár (Dictionary<string, Pen>), a kulcs a szín és a vastagság; a Dispose végigmegy az értékeken (a 9.4 lecke TollGyar osztálya).

2. Bélyegképek felszabadítása

Egy mappa képeit bélyegképként mutatod egy FlowLayoutPanelben. A mappa váltásakor mit kell tenned a régi bélyegképekkel?

Egy lehetséges megoldás

Végigmész a panel vezérlőin visszafelé: a PictureBox.Image értékét Dispose-olod, magát a vezérlőt is (Controls.Remove, Dispose), és csak utána töltöd fel újra.

Form1.csC#
for (int i = flpKepek.Controls.Count - 1; i >= 0; i--){    PictureBox pb = flpKepek.Controls[i] as PictureBox;    flpKepek.Controls.RemoveAt(i);    if (pb != null)    {        if (pb.Image != null) { pb.Image.Dispose(); }        pb.Dispose();    }}

3. Hibabiztos using

Egy módszer három erőforrást nyit meg, és a harmadik létrehozása hibázhat. Biztosítja-e a using az első kettő felszabadítását?

Egy lehetséges megoldás

Igen: az egymásba fűzött using blokkokban az első kettő már létrejött, és a kivétel esetén is felszabadul (a finally miatt). A kézi Dispose hívások ezt nem biztosítják.

Gyakorlótár: 6 további feladat

Három szinten gyakorolhatsz: alap, közepes és kihívás. Csak a próbálkozás után nyisd le a megoldást.

Alap szint

1. Felszabadítsam?

Döntsd el: (a) new Font(...); (b) e.Graphics; (c) Pens.Black; (d) Graphics.FromImage(b).

Megoldás megnyitása

(a) igen; (b) nem; (c) nem; (d) igen.

2. using átírása

Írd át try–finally alakra: using (Pen p = new Pen(Color.Red)) { g.DrawLine(p, 0, 0, 5, 5); }.

Megoldás megnyitása

A fordító ezt generálja:

Form1.csC#
Pen p = new Pen(Color.Red);try{    g.DrawLine(p, 0, 0, 5, 5);}finally{    if (p != null) { p.Dispose(); }}

Közepes szint

3. Ki a tulajdonos?

Egy metódus Bitmapot készít, és egy másik osztály konstruktorának adja át, amely mezőben tárolja. Ki szabadítja fel?

Megoldás megnyitása

Ezt meg kell állapodni és dokumentálni: leggyakrabban az átvevő osztály (amely tartja) a tulajdonos, és az IDisposable megvalósításával szabadítja fel. A hívó ilyenkor nem Dispose-ol. Ha az átvevő csak használja, a hívó marad a tulajdonos.

4. Hosszú életű vagy rövid életű?

Hol hozod létre: (a) a rács tollát (mindig ugyanaz); (b) az aktuális alakzat tollát (más a színe); (c) a háttérképet?

Megoldás megnyitása

(a) mezőben, a FormClosed-ben szabadítva; (b) using blokkban a Paint-ben (vagy gyorsítótárban); (c) mezőben, és a méretváltozáskor újra készítve (a régi felszabadítva).

Kihívás szint

5. Finalizer megfigyelése

Írj kis osztályt ~Megfigyelo() finalizerrel, amely kiírja, hogy lefutott, és mutasd meg, mikor fut le a GC.Collect() után. Mit tanulsz belőle?

Megoldás megnyitása

A finalizer nem kiszámítható időben fut (a GC.Collect hívásig vagy később); ezért nem szabad rá bízni a rendszererőforrás időbeni felszabadítását. Ez a Dispose indoka.

Megfigyelo.csC#
public class Megfigyelo{    ~Megfigyelo()    {        System.Diagnostics.Debug.WriteLine("A finalizer lefutott.");    }}// használatnew Megfigyelo();   // azonnal elérhetetlenGC.Collect();GC.WaitForPendingFinalizers();

6. WeakReference a gyorsítótárban

Egy képgyorsítótár a nem használt képeket felszabadíthatja, ha a memória fogy. Mit használsz az objektum élettartamának lazításához?

Megoldás megnyitása

A WeakReference gyenge hivatkozás: a GC felszabadíthatja az objektumot, ha más nem hivatkozik rá; a gyorsítótár ilyenkor újra betölti. A Bitmap Dispose hívását ilyenkor sem lehet elhagyni, ha a kép tulajdonosa te vagy, ezért a megoldás összetett; legtöbbször a korlátozott méretű LRU gyorsítótár egyszerűbb (haladó téma).

Gyors önellenőrzés

Válaszolj fejben, és csak utána nyisd le a választ.

1. Miért kell Dispose, ha van szemétgyűjtő?

A GC a nem felügyelt erőforrásokat (GDI-kezelő, kép memória) későn és kiszámíthatatlanul szabadítja fel; a Dispose azonnal.

2. Mit csinál a using utasítás?

A blokk végén (kivétel esetén is) meghívja a Dispose metódust (try–finally).

3. Mit nem szabad felszabadítani?

Az e.Graphics, a Pens, Brushes, SystemFonts, SystemColors közös objektumait.

4. Mi a tulajdonjog szabálya?

Aki létrehozza, az szabadítja fel, amikor már senki nem használja; átadáskor tisztázni kell.

5. Mikor szabadítod fel a régi képet?

Az új kép beállítása után.

6. Hogyan mérsz szivárgást?

A Feladatkezelő (Memória, GDI-objektumok) vagy a programból a PrivateMemorySize64 és a GetGuiResources; a trend számít.

7. Mi az ObjectDisposedException?

Hiba, amelyet a felszabadított objektum használata vált ki.

Mit tudsz már?

  • Tudod, miért nem elég a szemétgyűjtő, és mikor kell Dispose.
  • A using utasítással dolgozol, és tudod, mit nem szabad felszabadítani.
  • A hosszú életű erőforrásokat a FormClosed-ben szabadítod fel.
  • Saját osztályt IDisposable felülettel látsz el.
  • Méred a szivárgást, és felismered a tüneteit.

Következő lecke: Grafikonok és adatok szemléltetése

Adatok vizualizálása saját rajzolással: oszlop-, vonal- és kördiagram, tengelyek, jelmagyarázat, és a beépített Chart vezérlő.