Every way to release resources
using statements, using declarations, await using, finally, IDisposable, IAsyncDisposable and finalizers. Giving back files, connections and locks on time.
Memory is the garbage collector's job. Everything else (open files, network connections, database connections, timers, event subscriptions) is yours, and the rule is: whoever creates it disposes it, as soon as they're done.
- You create something disposable and use it in this method →
a using declaration,
using var writer = new StreamWriter(path);. It's disposed at the end of the enclosing block, even if an exception is thrown. - It should close before the block ends (release the file so
another step can read it) → a using statement with braces,
using (var writer = ...) { ... }. - You're in async code and the type has
DisposeAsync→await using, so the final flush doesn't block a thread. - Your class keeps a disposable in a field → your class owns it,
so implement
IDisposable(orIAsyncDisposable) and dispose it there. Don't dispose what a caller handed you. - Cleanup that isn't a Dispose (reset a flag, stop a
stopwatch) →
try/finally.
You'll see finalizers (~OrderLog()) and the Dispose(bool)
pattern in older code. New classes that only own managed objects
don't need them.
Why it matters
Formal. An object on the managed heap may be collected once no chain
of references leads to it from a root (a live local variable or
parameter, a static field, a CPU register, a GC handle). When the
collector runs is up to the runtime, mostly driven by allocation. The
.NET GC is generational: new objects start in generation 0; objects that
survive a collection are typically promoted to generation 1 and then 2,
which are collected less often. The GC reclaims memory only; files,
sockets and other resources are released by Dispose, not by
collection.
In plain English. .NET cleans up objects for you, but only the ones your program can no longer reach by following references from anything it's still using. It does the cleanup when it decides to, not the moment you stop using something. New objects are checked often; objects that keep surviving get checked less, because long-lived objects tend to stay alive. The cleanup is for memory only. Open files still need closing yourself.
Example. After customers.Clear(), every customer object that
nothing else refers to becomes collectable. An object still referenced
from a static list or an event subscription never is, however long ago
you last used it.
Why this rule exists. Freeing exactly what can't be reached is always safe, and checking young objects most often finds most garbage cheaply.
The GC frees memory for objects nothing can reach, when it decides to. It never closes your file on time.
Formal. StreamWriter and FileStream hold written data in an
in-memory buffer and write it to the operating system when the buffer
fills, on Flush(), or on Dispose(). Dispose also releases the file
handle. The runtime does not flush a writer that was never disposed
when the process exits, so its buffered text is lost. A using
statement or declaration calls Dispose when its scope ends, even when
an exception is thrown.
In plain English. A writer collects your text in a holding area and
sends it to the file in batches. Closing the writer sends the last
batch and lets go of the file. Forget to close it, and the last batch
never arrives. The file can be left empty. Writing using in front of
the writer makes C# close it for you at the end of the block, even if
something goes wrong.
Example. var w = new StreamWriter("log.txt"); w.WriteLine("hi");
with no Dispose can leave log.txt at 0 bytes.
using var w = new StreamWriter("log.txt"); writes the line when w
goes out of scope.
Why this rule exists. Writing to disk one character at a time would be very slow; buffering is fast, and disposal is how the program says "I'm done, finish up."
A writer holds text in a buffer. Until it's disposed, the file may be missing its last lines.
Letting C# call Dispose
Section
finally, and the using shorthand
using is a try/finally the compiler writes for you.
Section
Closing files with using
The first real use, and how to pick between the statement and the declaration by where the scope should end.
Reverse-order disposal of stacked resources, and the modern sealed-class Dispose.
Section
await using and DisposeAsync
The async twin, and the CS8417/CS8418 errors when you pick the wrong one.
Who disposes what
Services the container builds are disposed when their scope ends, so you don't dispose them yourself.
A new HttpClient per call can run out of sockets. Let IHttpClientFactory manage them.