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.