C# Garbage Collection, Dispose and Native Resources Explained
Understand when .NET garbage collection is enough, when to use Dispose and IDisposable, how resource ownership works, and where finalizers, SafeHandle, P/Invoke and LibraryImport fit.
C# developers often learn that .NET has a garbage collector, so memory is cleaned up automatically. That is true, but it does not mean every resource can simply be left for the garbage collector.
Understanding that difference makes using, IDisposable, finalizers and native interoperability much easier to reason about.
1. Garbage collection and disposal solve different problems
var customer = new Customer();Because Customer is a class, .NET allocates a managed object. When the application no longer has any reachable references to it, the garbage collector can eventually reclaim its memory. You normally do not clean up that managed memory yourself.
var file = File.OpenRead("customers.csv");A FileStream is also a managed object, but it represents access to an operating-system resource: an open file handle. While that handle remains open, the operating system considers the file to be in use. Other code may be prevented from modifying, moving or deleting it.
using var file = File.OpenRead("customers.csv");The using declaration ensures that Dispose() is called when execution leaves the current scope, including when an exception occurs. Garbage collection says that unreachable managed memory can eventually be reclaimed. Disposal says that the application has finished with a resource and should release it now.
Disposable resources commonly include files and streams, network sockets, database connections and commands, operating-system handles, some cryptographic objects, and wrappers around native resources.
Code-review questionThink of a hotel room: garbage collection is the staff eventually clearing abandoned belongings; disposal is returning the room key when you check out.
2. Deterministic cleanup with using
Disposal is called deterministic cleanup because your code determines when the resource is released. A using statement makes that lifetime explicit.
using (var file = File.OpenRead("customers.csv"))
{
// Read the file.
}The file is disposed when execution leaves the block. A using declaration is more compact and disposes the resource at the end of its containing scope.
using var file = File.OpenRead("customers.csv");
// Read the file.Both forms provide reliable cleanup without requiring you to call Dispose() manually on every possible execution path.
3. When should your class implement IDisposable?
Most of the time, you consume types that already implement IDisposable. Your own class may need to implement it when the class acquires and owns a disposable resource.
public sealed class ReportWriter : IDisposable
{
private readonly StreamWriter _writer;
public ReportWriter(string fileName)
{
_writer = new StreamWriter(fileName);
}
public void WriteLine(string text)
{
_writer.WriteLine(text);
}
public void Dispose()
{
_writer.Dispose();
}
}ReportWriter creates and owns a StreamWriter. Its Dispose() method passes the cleanup responsibility to that writer.
using var report = new ReportWriter("sales.txt");
report.WriteLine("Monthly sales report");When report leaves scope, its Dispose() method disposes the underlying writer. The design principle is ownership: if your class creates and owns a disposable resource, it should normally ensure that the resource is disposed.
Code-review questionWho owns this resource, and who is responsible for releasing it?
Ownership should be unambiguous. If another part of the application supplies a resource, your class should not automatically assume it owns that resource. Disposing something owned elsewhere can break code that still expects to use it.
4. Why not wait for garbage collection?
Garbage collection is driven mainly by managed-memory pressure, not by how urgently an external resource needs to be released. An application could stop using a file or socket while still having plenty of managed memory, giving the runtime no immediate reason to collect.
var file = File.OpenRead("customers.csv");
// Use the file and simply forget about it.The managed object may eventually become eligible for collection, but eventually is not a suitable resource-lifetime policy. Use using when the resource belongs to a clear operation or scope.
5. What is a finalizer?
~NativeResource()
{
// Last-resort unmanaged cleanup.
}A finalizer gives the runtime an opportunity to perform special cleanup before an object's memory is reclaimed. Your application does not control exactly when it runs, so finalization is not the normal cleanup mechanism.
Dispose() means release the resource now because the program knows it has finished with it. A finalizer is a last-resort safety net if deterministic cleanup did not happen. Finalizable objects also require extra garbage-collector work and may survive longer than ordinary objects.
For everyday application code, prefer using and IDisposable. When directly owning an operating-system handle, modern .NET generally favours a safe wrapper such as SafeHandle, reducing the delicate cleanup code that application developers must maintain.
6. Crossing the managed boundary
C# application
↓
.NET runtime
↓
Managed objects and garbage collectionMost C# code runs inside the managed .NET environment. Sometimes an application must call code outside it, such as an operating-system API, an existing C or C++ library, a hardware vendor SDK, legacy software or an older COM component.
C# application
↓
Native boundary
↓
Windows API, C/C++ DLL or vendor libraryThe garbage collector understands managed objects, but it does not automatically understand every pointer, buffer or handle created by an external native library. Crossing this boundary introduces additional questions about data representation, ownership and cleanup.
7. Calling native code with P/Invoke
Platform Invocation Services, usually shortened to P/Invoke, allow managed C# code to call functions exported by native libraries.
[DllImport("kernel32.dll")]
private static extern IntPtr GetCurrentProcess();In plain English, the declaration says that the function is implemented outside the C# application and should be found in the named native library. Modern .NET also provides source-generated interoperability through LibraryImport.
[LibraryImport("kernel32.dll")]
private static partial IntPtr GetCurrentProcess();LibraryImport can generate much of the required interop code at compile time, but neither approach removes the need for an accurate declaration. Parameter types, return types, character encoding and ownership rules must match the native API. An incorrect signature can cause corrupted data, resource leaks or process instability rather than a friendly C# exception.
8. Performance is only part of the decision
Native libraries may provide specialised or highly optimised functionality, but crossing the managed/native boundary is not automatically faster. Interoperability can introduce data conversion, buffer copies, memory pinning, native-handle tracking, cleanup work and more difficult diagnostics.
Use native interoperability when it provides a necessary capability or a measured benefit, not simply because native code sounds faster. In enterprise applications it is often a practical choice because a valuable native component already exists and replacing it would be expensive, risky or unnecessary.
A practical C# code-review checklist
- ✓Managed memory: Is this an ordinary managed object with no special resource ownership?
- ✓External resource: Does the object represent a file, stream, socket, database connection, handle or native resource?
- ✓Ownership: Which component creates or accepts responsibility for releasing the resource?
- ✓Lifetime: Can a using statement or declaration make the intended scope clear?
- ✓Exceptions: Will cleanup still happen when the operation fails?
- ✓Native boundary: Have the signature, ownership rules and data conversions been verified?
- ✓Finalization: Is a finalizer genuinely necessary, or would IDisposable and SafeHandle be safer?
The resource-lifetime model to remember
Ordinary managed object
↓
GC reclaims managed memory
Disposable resource
↓
Dispose it when finished
Your class owns that resource
↓
Implement IDisposable
Directly owned unmanaged handle
↓
Prefer a safe wrapper such as SafeHandle
C# calls a native library
↓
Use P/Invoke or LibraryImport carefullyOnce that distinction is clear, using, IDisposable, finalizers, SafeHandle and native interoperability stop looking like unrelated features. They become parts of one consistent resource-lifetime model.