Resource Management
Memory, file handles, and other resources — what the runtime cleans up for you, and what you must clean up yourself
Every program uses resources — memory, file handles, network sockets, database connections, locks. Most of them are finite: the operating system gives you a budget, and if you don't return what you've borrowed, eventually the program (or the whole machine) runs out.
C# splits resources into two camps:
- Memory, which is reclaimed automatically by the garbage collector (GC).
- Everything else — usually called unmanaged resources — which the runtime can help you release but can't fully manage on its own.
Understanding this split is the foundation of clean, leak-free C#.
Memory: handled (mostly) for you
In low-level languages like C, you call malloc and free by
hand. Forget the free and you leak memory; double-free and you
corrupt memory.
In C#, when you write new Person(...), the runtime allocates
memory on the heap. When nothing points to that object anymore,
the GC eventually reclaims the space:
Two important consequences:
- You don't have to (and shouldn't) try to free memory yourself.
- GC runs whenever it decides to, not at a precise moment after the object becomes unreachable. So for memory, "when" is fuzzy.
That fuzziness is fine for memory. It's a problem for the second category.
Unmanaged resources: not the GC's job
Consider opening a file. Behind the scenes, the OS gives you a file handle — a small numeric ticket the kernel tracks. The OS only allows a limited number per process.
If you open a file, never close it, and let the variable go out of scope, the GC will eventually reclaim the .NET object — but it might be seconds or minutes later. Meanwhile, the file handle is still held by the OS. Do this in a loop and you'll hit "too many open files" long before memory becomes a concern.
So C# offers a way to say "I'm done with this resource — release
it now, deterministically": the IDisposable pattern.
IDisposable and using
Any class that owns an unmanaged resource implements
IDisposable with a Dispose() method. You can call Dispose()
yourself, but the language has a better tool: the using
statement, which guarantees Dispose() runs even if an exception
is thrown.
Run that. You'll see "closed A" before "done" — proof that
Dispose() ran deterministically at the end of the block.
The shorter "using declaration"
Modern C# offers an even tidier form: a using declaration
attached to a local variable. The resource is disposed at the end
of the enclosing scope.
Why using and not just Dispose()?
You could call Dispose() manually:
But using is exactly that try/finally, written for you and
impossible to forget. Reach for it every time.
The two-tier picture
Beginners often misremember this and think the GC handles
everything. It doesn't. It handles memory. The rest is your job —
made easy by IDisposable and using.
Finalizers (briefly)
A class can also have a finalizer (~ClassName()) that the GC
calls before reclaiming the object — a last-ditch chance to
release unmanaged resources if the user forgot to call Dispose().
You will almost never write a finalizer in everyday code. They run
at unpredictable times and add complexity. Use IDisposable and
make your callers use using.
Practice (conceptual)
We're running C# inside a browser via WebAssembly here, so we don't have a real filesystem to open. But you can practice the shape of resource management against a tiny mock:
The using ensures the log is flushed and closed even if Job()
threw. That's the whole game.
A guideline that pays for itself forever
If a class implements
IDisposable, treat every instance as requiringusing— unless the documentation explicitly says otherwise.
It's a one-line rule that prevents an enormous category of production bugs.
Test your understanding
What does the garbage collector reclaim automatically?
Memory, file handles, and network sockets
Memory only — other resources still need to be released explicitly, typically via IDisposable / using
Only unmanaged memory
Variables on the stack
Why is the using statement preferred over calling Dispose() manually?
It's faster
It doesn't actually call Dispose()
It guarantees Dispose() runs at the end of the block, even if an exception is thrown — so you can't forget cleanup
It hides what's happening