Ha a kód Ãrás és olvasás aránya elÅ‘kerül egy beszélgetés kapcsán, akkor 70/30 arányt szoktak mondani az olvasás javára. Vagyis egy fejlesztÅ‘ tipikusan több idÅ‘t tölt el a kód olvasásával, értelmezésével, mint Ãrásával. Ez az arányszám szerintem rendben is van, mivel a 70%-ba beleértendÅ‘ a debuggolás is, mivel ha lehetÅ‘ségünk van olvasni a kódot, akkor interaktÃvan, működés közben is megtehetjük azt.
A debuggolás folyamatára a legtöbben a hibakeresés és javÃtás miatt asszociálnak, de nem feltétlen csak hibák keresésére alkalmas. Például, ha egy ismeretlen kódon kell dolgoznom, akkor általában nem a dokumentációval kezdek, hanem a kóddal, mert abban van úgyis az igazság. Persze sokat segÃt, ha az ember rendelkezik tudással a szoftver felépÃtésérÅ‘l, fÅ‘leg ha hibákat kell megtalálni.
Ez azonban csak egy része a dolognak, tudni kell alkalmazni a debuggert. Ha ezt hatékonyan tudja használni valaki, akkor bármekkora projekt fejlesztésébe bele tud tanulni. Szerencsére a Visual Studio rendelkezik egy nagyon fejlett debugger eszközzel, illetve magába a .NET-ben is vannak tÃpusok és funkciók, amelyek nagymértékben megkönnyÃtik a hibakeresést és a program menet közbeni elemzését.
C# és .NET debuggolásra két fő eszközt szokás használni: Visual Studio-t és Visual Studio Code-ot. A könyv ezen részében a Visual Studio-ról lesz alaposabban szó.
Futtatás előtt
A legalapabb funkcióról, a breakpoint elhelyezésérÅ‘l és a programunk debug módban indÃtásáról az elsÅ‘ programunk futtatása során már volt szó, de mi van akkor, ha nem debug módban indÃtottuk el a programunkat, de szeretnénk megvizsgálni a belsÅ‘ állapotát?
Természetesen erre is lehetőségünk van, mégpedig úgy, hogy a Debug menüből az Attach to Process… menüpontot választjuk.
Az ennek hatására megjelenő ablakban ki tudunk választani egy folyamatot, amihez csatlakozni szeretnénk. Ennek a folyamatnak nem feltétlen kell futnia a saját gépünkön. Csatlakozhatunk egy távoli géphez is vagy akár egy Docker konténerhez is.
Bármelyik esetet is válasszuk fontos, hogy ha egy olyan folyamathoz csatlakozunk, amely nem Debug üzemmódba lett fordÃtva, akkor a folyamat komplikálódhat. Mégpedig azért, mert a Debug build-ek során generálódnak PDB fájlok, amik a generált bináris kódot és a hozzájuk tartozó forráskódot kapcsolják össze. PDB és/vagy forráskód hiányában azonban elképzelhetÅ‘, hogy natÃv, gépi kódú Assembly utasÃtásokat kapunk. .NET programok esetén a debugger megpróbálja visszafejteni nekünk a kódot, de ez idÅ‘igényes tud lenni nagy programok esetén és a visszafejtett kód sosem tükrözi 100%-ban az eredeti kódot.
Ugyan ebben a menüben a másik fontos funkció a Reattach to Process, amikor a debugger a korábban az Attach to Process során kiválasztott programhoz csatlakozik újra.
A folyamathoz csatlakozás után ugyanúgy elhelyezhetünk break pointokat. Ezek feltételekkel is elláthatóak. A feltételes breakpoint csak akkor aktiválódik, ha a hozzá társÃtott kifejezés értéke igaz lesz. A kifejezés bármilyen C# kifejezés lehet, egészen addig, amÃg bool tÃpusra kiértékelhetÅ‘.
Feltételeket a törésponthoz a jobb kattintás után megjelenÅ‘ Conditions… menüpont segÃtségével társÃthatunk.
A megjelenÅ‘ szerkesztÅ‘ben megadhatunk egy feltételt. Ez különösen hasznos tud lenni, ha ciklusokat debuggolunk. Akciókat is hozzárendelhetünk a feltételhez, ami mondjuk a debug kimenetre Ãr ki nekünk adatokat, illetve a töréspontok függhetnek akár egymástól is.
Futtatás közben
A töréspontok után az egyik leghasznosabb funkciója a debuggernek az Immediate Window. Ezt debuggolás közben tudjuk aktiválni a Debug menü Windows menüjében.
Az Immediate Window-ban kódrészleteket értékelhetünk ki, akár úgy, hogy a futó programunk változóira hivatkozunk.
Ez nagy segÃtség tud lenni a breakpointok finomÃtásában, hogy mire is van szükségünk. Ha nem tudjuk, hogy hol keressük pontosan a problémát, akkor elÅ‘fordul, hogy teleszórjuk töréspontokkal a szoftvert. Ezek központi menedzselésére a Breakpoints nézet szolgál, ami szintén a korábban emlÃtett Windows menüpont alatt található.
Ebben a nézetben egy helyen tudjuk ki és bekapcsolni, valamint törölni az aktÃv töréspontokat anélkül, hogy a töréspontot tartalmazó fájlra navigálnánk.
Metódus hÃvások debuggolása közben két hasznos nézet a Locals és a Call Stack. A Locals ablakban a helyi változókat és azok értékeit láthatjuk, mÃg a Call Stack a hÃvási láncot mutatja meg, vagyis hogy melyik metódus is hÃvta meg az aktuális metódusunkat.
A Call Stack kivételek okainak felderÃtésekor nagyon hasznos. Ha egy kivétel történik a kódunkban, akkor a Call Stack-ben visszalépkedve kivizsgálhatjuk, hogy pontosan mi is volt a kiváltó oka a kivételnek. Ehhez persze be kell kapcsolva legyen, hogy kivételek keletkezésekor a debugger megálljon a kivétel helyén.
Alapértelmezetten a leggyakoribb C# kivételek vannak bekapcsolva, de ez nem minden esetben lesz elég. Éppen ezért szintén a Debug Windows menüjében találunk egy Exception Settings menüpontot, amiben részletesen személyre szabhatjuk, hogy milyen kivételek kezelése járjon a debugger megállÃtásával.
Debugger vezérlés a kódból
A .NET beépÃtetten tartalmaz tÃpusokat a debuggerrel való interakcióhoz. Ezek a Debugger és a Debug tÃpus. A Debugger a programhoz csatlakoztatott debugger vezérlésére szolgál, mÃg a Debug osztály a debugger konzol kimenetére Ãrást teszi lehetÅ‘vé. Mindkét tÃpus a System.Diagnostics névtérben található.
A Debug osztály rendelkezik a Console osztály esetén megismert Write és WriteLine statikus metódusokkal, amelyek nevükben egyezőek, de szignatúrájukban eltérőek:
static void Write (string? Message);
static void Write (object? Value);
static void Write (object? value, string? category);
static void Write (string? message, string? Category);
static void WriteLine (object? value);
static void WriteLine (string? Message);
static void WriteLine (object? value, string? category);
static void WriteLine (string? message, string? category);
static void WriteLine (string format, params object?[] args);
Az egy paraméteres változatokkal egy tetszÅ‘leges szöveget vagy objektumot tutudunk kiÃratni. A kétparaméteres változatok esetén a category paraméter segÃtségével egy olvashatóságot segÃtÅ‘ paramétert tudunk megadni. A WriteLine esetén ugyanúgy, mint a konzol esetén van lehetÅ‘ségünk formázott kimenetet Ãrni. Ezek a kimenetek a debugger kimenetén jelennek meg, amit futás közben szintén a Debug menü Windows almenüjében tudunk aktiválni Output néven.
Ezen felül a Write és a WriteLine is rendelkezik egy If taggal bÅ‘vÃtett változattal.
static void WriteIf(bool condition, object? value);
static void WriteIf(bool condition, string? message);
static void WriteIf(bool condition, string? message, string? category);
static void WriteIf(bool condition, object? value, string? Category);
static void WriteLineIf(bool condition, object? value);
static void WriteLineIf(bool condition, string? message);
static void WriteLineIf(bool condition, object? value, string? category);
static void WriteLineIf(bool condition, string? message, string? category);
Ezen metódusok, csak akkor Ãrnak a kimenetre, ha az elsÅ‘ paraméterként megadott kifejezés igaz értékű lesz.
Ezen felül az osztály még egy hasznos metódusa az Assert. Ez az alábbi szignatúrákkal rendelkezik:
static void Assert(bool condition);
static void Assert(bool condition, string? message);
static void Assert(bool condition, string? message, string? detailMessage);
Ez a metódus, amennyiben az elsÅ‘ paraméterként megadott kifejezés hamis, egy üzenet ablakot jelenÃt meg a megadott üzenettel, amit részletezhetünk a három paraméteres változatában. Ez különösen hasznos, mivel Ãgy nem kell figyelnünk a debugger kimenetét.
A Write, WriteLine, WriteIf és WriteLineIf metódusok hÃvása nem fut hibára abban az esetben, ha RELEASE konfigurációnk van, vagy nincs éppen Debugger hozzácsatlakoztatva a programunkhoz. Ugyan ez igaz az Assert metódusokra is. Ezek is csak DEBUG konfigurációban aktiválódnak, ha van debugger hozzácsatlakoztatva a programunkhoz.
A Debugger osztállyal pedig magát a programhoz csatlakoztatott debuggert tudjuk vezérelni. Ezen osztály legfontosabb részei:
static bool IsAttached { get; }
Ezen tulajdonság segÃtségével lekérdezhetjük, hogy van-e debugger a programhoz rendelve, vagy sem.
static void Break();
Programozott breakpoint kiváltásra szolgál. Ha a program végrehajtása ebbe a metódusba kerül, akkor az megáll ugyanúgy, mint egy kézzel elhelyezett breakpoint esetén
static bool Launch();
A Debugger programozott indÃtásra szolgál.
Ezen metódusok különösen hasznosak, például akkor, ha az alkalmazásunk általános kivételkezelőjébe beesik valami hiba:
using System.Diagnostics;
namespace Hibakezeles
{
internal static class Program
{
private static void Main(string[] args)
{
try
{
//alkalmazás logika
}
catch (Exception ex)
{
//kivétel kiÃrása a debuggerre
Debug.WriteLine(ex);
#if DEBUG
//DEBUG build esetén ha a debugger csatlakoztatva van
//akkor break, ellenkezÅ‘ esetben elindÃtjuk.
if (Debugger.IsAttached)
Debugger.Break();
else
Debugger.Launch();
#endif
}
}
}
}
Ha történik egy általános hiba, akkor a példában szereplÅ‘ kódrészlet azt a debugger kimenetére ki is Ãrja, illetve ha csatlakoztatva van, akkor megállÃtja a végrehajtást, vagy elindÃtja a debugger alkalmazást, hogy a hiba körülményeirÅ‘l minél több információt megszerezzünk. Ezt természetesen egy #if DEBUG szimbólummal körbezárt blokkban érdemes tenni, mivel a RELASE konfiguráció nem hibakeresésre lett kitalálva.