C#
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Object-Oriented C#
-
Section 4: Working with Data
-
Section 5: Error Handling
-
Section 6: Delegates and Events
-
Section 7: Async Programming
-
Section 8: More Language Features
-
Section 9: File I/O and Serialization
-
Section 10: Networking in .NET
-
Section 11: The .NET Ecosystem
-
Section 12: Memory and Performance
-
Section 13: Concurrency Beyond Async
-
Section 14: Reflection and Attributes
-
Section 15: Testing and Best Practices
-
Section 16: Design Patterns in C#
-
Section 17: Standard Library Deep Dive
-
Section 18: Data Structures and Algorithms in C#
-
Section 19: GUI and Desktop Development Overview
-
Section 20: Practical Projects
-
Section 21: More Practice Exercises
-
Section 22: More Standard Library and Text Processing
-
Section 23: More Design Patterns
-
Section 24: More Projects
-
Section 25: Interview Practice
-
Section 26: C# Keywords Reference (Modifiers)
-
Section 27: C# Keywords Reference (Statements)
-
Section 28: C# Keywords Reference (Operators)
-
Section 29: BCL: System.Collections.Generic
-
Section 30: BCL: System.Linq
-
Section 31: BCL: System.Threading
-
Section 32: BCL: System.IO
-
Section 33: BCL: System.Text and System.Text.Json
-
Section 34: BCL: System.Net.Http
-
Section 35: C# Language Specification Topics
-
Section 36: More Practice Exercises
-
Section 37: More Async Patterns
-
Section 38: More BCL: System.Reflection and System.Diagnostics
-
Section 39: Nullable Reference Types In Depth
-
Section 40: C# Records and Pattern Matching In Depth
-
Section 41: Dependency Injection Deep Dive
-
Section 42: More Interview and Whiteboard Practice
85: IDisposable and the using Statement
Imagine you're renting a high-end power tool from a local hardware store—say, a heavy-duty industrial pressure washer. You sign a contract, take the tool home, and get your driveway sparkling. But here's the catch: the store only has three of these. If you just leave the pressure washer in your garage and forget about it, the store can't rent it to anyone else, and you're still being charged by the day. Eventually, the store runs out of tools entirely, even though nobody is actually using them. To fix this, you have to explicitly bring the tool back to the store to "close out" your rental.
In C#, the Garbage Collector (GC) is like a cleaning crew that comes through your house every once in a while to throw away trash. It's great for memory, but it doesn't know how to "return the rental tool." Things like file handles, database connections, or network sockets are "unmanaged resources." The GC knows how to reclaim the memory the object took up, but it doesn't know that it needs to tell the Operating System, "Hey, I'm done with this file; you can let another program open it now."
Why the Garbage Collector isn't enough
I've seen plenty of juniors assume that because C# has automatic memory management, they never have to worry about cleanup. That's a dangerous assumption. If you open a StreamWriter to a log file in a loop and never close it, you'll eventually hit a IOException because the OS thinks the file is still being used by your program, even if you aren't actively writing to it.
This is where IDisposable comes in. It's essentially a contract that says, "This object holds onto something that needs to be explicitly released." When a class implements IDisposable, it promises to provide a Dispose() method where you can put your cleanup logic.
// A simplified look at how a resource-heavy class is structured
public class ResourceHolder : IDisposable
{
private bool _disposed = false;
public void DoWork()
{
if (_disposed) throw new ObjectDisposedException("ResourceHolder");
Console.WriteLine("Doing some heavy lifting with a file or socket...");
}
public void Dispose()
{
// This is where the "return the tool to the store" logic goes
Console.WriteLine("Cleaning up unmanaged resources...");
_disposed = true;
// Tell the GC we don't need to run the finalizer for this object
GC.SuppressFinalize(this);
}
}
The "Auto-Return" of the using Statement
You could manually call .Dispose() in a finally block, but that's verbose and easy to forget. I'll be honest: I've spent way too many hours debugging memory leaks because someone forgot to call dispose in an edge-case catch block. C# gives us the using statement to solve this. It's essentially syntactic sugar that guarantees Dispose() is called the moment the code exits the block, regardless of whether it finished successfully or threw an exception.
Here is the modern, concise way to do it (the using declaration), which I prefer for its cleanliness:
public void WriteLog(string message)
{
// The 'using' keyword here means: "Call Dispose() when this variable goes out of scope"
using var writer = new StreamWriter("log.txt", append: true);
writer.WriteLine($"{DateTime.Now}: {message}");
// No need to call writer.Dispose() or writer.Close().
// It happens automatically right here at the closing brace.
}
If you're working in an older codebase, you'll see the using (...) { } block version. They do exactly the same thing; the block version just lets you control the exact moment the resource is released if you have a very long method and don't want to hold the resource until the very end.
When should you actually implement IDisposable?
You don't need this for every class. In fact, adding it where it isn't needed just creates noise. You only implement IDisposable if your class:
- Owns an object that itself implements
IDisposable(like aHttpClientorSqlConnection). - Directly interacts with unmanaged handles (like Win32 API calls or raw pointers in
unsafecode).
If your class just holds a list of strings and a couple of integers, leave it alone. Let the Garbage Collector do its thing.
📋 Practical Task
Implementing a Disposable Database Connection Simulator
Since we aren't connecting to a live server in this exercise, you're going to build a simulator. Your goal is to create a system that mimics a database connection that must be closed to prevent "connection leaks."
Requirements:
- Create a class called
DbConnectionSimulatorthat implementsIDisposable. - Add a private boolean field
_isOpen. Set it totruein the constructor. - Create a method
ExecuteQuery(string query). This method should check if_isOpenis true; if it is, print "Executing query: [query]". If it's false, throw anObjectDisposedException. - Implement the
Dispose()method to set_isOpentofalseand print "Connection closed safely." - In your
Mainmethod, use ausingdeclaration to instantiate the simulator and run a query. - As a test, try to call
ExecuteQueryafter theusingscope has ended to verify that theObjectDisposedExceptionis thrown.
There are no comments for now.