1. How do you properly release resources in C#?
Let's imagine your OS as a strict librarian. You took a book (opened a file/stream), you're reading it, and then... you forgot to return it! The librarian gets mad: "What do you mean — you still have the book?!" It's the same with streams. An open stream takes up resources: a file descriptor, a chunk of memory, and it also locks the file for other apps.
If you don't close the stream, the result can be anything from "something doesn't work" to "everything broke, nobody can write to this file". And if your program has a bunch of unclosed streams — the system can start "leaking" resources and just stop working.
What's the danger?
- File doesn't close, commands don't reach the disk (for example, when writing — data might stay in the buffer).
- File is locked for other processes — your coworkers and other programs get mad.
- Descriptor limit: on Windows/Linux, processes have limits on open files/streams.
The IDisposable interface
Any class that works with unmanaged resources (streams, files, databases, sockets) must implement the IDisposable interface.
public interface IDisposable
{
void Dispose();
}
Inside the Dispose() method, you usually release all those poor resources: the file finally closes, connections are cut, memory is freed.
Streams are objects that hold on to all sorts of important resources: files, connections, memory. If you don't close them, the file might stay locked (your Word will say "file is open in another app!"), and the system — out of memory. So it's super important to release the stream when you're done.
In .NET, streams implement the IDisposable interface. That means: you gotta close them by calling Dispose() (or just wrap them in a using block).
2. Ways to close a stream: from risky to reliable
Option 1. "Manual" closing: don't do this!
This is the old-school way. And yeah, I showed it in previous examples, but in modern development, almost nobody does it like this anymore :P
var stream = new FileStream("file.txt", FileMode.Open);
// Working with the stream
stream.Close(); // or stream.Dispose()
The problem: if something goes wrong (an error/exception) between opening and closing, the file stays open and locked. It's like you ran out of the library with the book because you smelled pizza...
Option 2. Using try...finally
FileStream stream = null;
try
{
stream = new FileStream("file.txt", FileMode.Open);
// Working with the stream
}
finally
{
if (stream != null)
stream.Dispose();
}
This way is reliable: finally will always run, even if there's an error. But let's be honest, it's a pain to write this every time.
Option 3. Clean and safe: the using statement
Classic syntax (using ( ... ) { ... })
using (var stream = new FileStream("file.txt", FileMode.Open))
{
// Working with the stream
}
// Here stream.Dispose() is called automatically!
The key idea — everything inside the using block works with the stream, and when the block ends — the file closes even if something goes wrong (like an exception is thrown).
Option 4. Modern syntax
Modern syntax (using var)
using var stream = new FileStream("file.txt", FileMode.Open);
// Working with the stream
// ... Dispose will be called automatically when the variable goes out of scope
Awesome! No need for extra indents or curly braces.
How does it work "under the hood"?
The using statement gets turned by the compiler into that same try...finally — but for you. Joke: "using writes clean code for you — maybe soon it'll start drinking coffee and scrolling Stack Overflow?"
Difference between classic and modern using
| Classic using-block | using var (declaration) | |
|---|---|---|
| Look | |
|
| Scope | Inside the curly braces of the block | Until the end of the current block (method, loop, etc.) |
| Brevity | A bit more verbose | Concise, less indentation |
| Since | C# 1.0 | C# 8.0 and up |
3. What are using-declarations?
5 years ago, the world saw a new, concise way to work with IDisposable objects.
using-declaration — that's when instead of a block, you declare a variable with the using keyword, and it'll be automatically released at the end of the current block (like a method or loop), not at the end of some extra curly braces.
using var stream = new FileStream("file.txt", FileMode.Open);
// Working with the stream
Console.WriteLine(stream.Length);
// The file is still open here!
// ... end of the method
// stream.Dispose() will be called here automatically
Main differences from classic using:
- No need for curly braces, no nested code block is created.
- The variable is available until the end of the whole block where it's declared (usually — a method, sometimes — a loop, class, if declared at class level).
- The resource is released only when the execution block ends.
Why is this awesome?
- Less nesting — code is way shorter and more readable.
- Easier to work with multiple resources — declare several using variables in a row, and everything gets released when the method ends.
- Less chance to mess up — you won't miss the spot where you should've called Dispose().
4. Comparing: classic vs. modern using
Let's check out a code comparison.
Classic way
using (var reader = new StreamReader("input.txt"))
{
using (var writer = new StreamWriter("output.txt"))
{
string line;
while ((line = reader.ReadLine()) != null)
{
writer.WriteLine(line.ToUpper());
}
}
} // Here both files will be closed
Modern way (C# 8+)
using var reader = new StreamReader("input.txt");
using var writer = new StreamWriter("output.txt");
string line;
while ((line = reader.ReadLine()) != null)
{
writer.WriteLine(line.ToUpper());
}
// Both files will be closed here, when exiting the method
Looks simpler, right? Especially if you have even more nesting — the modern way makes life a lot easier.
5. When and where does Dispose() run?
Here's where students often mess up: they think Dispose gets called right after the line where you use the variable — but that's not true!
Check out this example:
void MyMethod()
{
using var fileStream = new FileStream("data.bin", FileMode.Open);
// ... lots of code, maybe even loops and nested calls
// fileStream is still open!
// You can still access fileStream here
}
// Here, at the } of the method, fileStream.Dispose() is called
Important point: If you declare a using variable inside a loop, Dispose will be called after each iteration.
foreach (var path in filePaths)
{
using var reader = new StreamReader(path);
// working with reader
} // reader.Dispose() will be called after each iteration (closes the file)
6. Mistakes when porting old code
Sometimes you port old code or copy an example with classic using, but the variable needs a longer "lifetime" than the scope of the braces. Then the classic way won't work, but using-declarations are perfect.
But there are gotchas. For example, if you have two resources in a loop, but one of them needs to "live" longer than the other — declare them in the right order:
using var resource1 = ...;
for (int i = 0; i < 10; i++)
{
using var resource2 = ...;
// resource2 lives for one iteration
// resource1 — the whole function
}
7. Practice
Let's keep building our learning app — a little coffee order simulator you've been improving since last time. Now let's make it save the order history to a text file and read it at startup.
Step 1: Save an order to a file
using var writer = new StreamWriter("orders.txt", append: true);
writer.WriteLine("Coffee: Latte; Milk: Oat; Size: Large");
// The second parameter of the StreamWriter constructor append: true means we want to add to the file, not overwrite it.
Step 2: Read order history
using var reader = new StreamReader("orders.txt");
string? line;
while ((line = reader.ReadLine()) != null)
{
Console.WriteLine($"Order: {line}");
}
And as soon as the program finishes this method, the files will close automatically.
8. Best practices for working with using-declarations
1. Always use using for objects that implement IDisposable
In .NET, most classes for working with files, streams, resources implement this interface. That's the signal: release me with using!
2. Remember about scope: don't declare using-var where the variable might "get in the way"
If you only need the variable for a couple of lines — use it where you need it, and not earlier.
3. Don't forget about release order
If you declare several using variables in a row — Dispose will be called in reverse order:
using var first = new Resource("First");
using var second = new Resource("Second");
// ... work
// Dispose for second first, then for first
This sometimes matters, like if one resource depends on another (for example, a write stream should be released before the file).
4. Don't use using-declaration outside a method
using-declarations are not allowed at the class level (like for fields). They only work inside methods, constructors, etc.
5. Combine with error handling
Remember, even with using not all exceptions are convenient — it's smart to add try-catch if you need to control read/write errors.
GO TO FULL VERSION