Skip to main content
CodeOath
← All posts

C#64 min total · 17 parts

C# Fundamentals: Value Types, Reference Types, Boxing, and the Type System

Part 16 of 17 · ~2 min

IDisposable and using with Value vs. Reference Types

Talking to the physical barcode scanner means opening a real connection — a class-based resource that needs to be closed reliably, even when something goes wrong mid-scan:

class ScannerConnection : IDisposable
{
    public void Dispose() { /* releases the underlying port */ }
}

using (var scanner = new ScannerConnection())
{
    // ... read scans ...
} // Dispose() runs here no matter what, even if an exception was thrown above

// Modern "using declaration" — disposes at the end of the enclosing scope rather than a nested block
using var scanner2 = new ScannerConnection();

Strip away the syntax and using is nothing more than a try/finally block written more compactly, and neither half of that pattern cares whether the resource involved is a class or a struct. The framework itself puts a struct behind IDisposable in more than one spot — the enumerator type sitting behind a foreach over List<T> is one real, shipping example, chosen specifically so walking a list doesn't cost a heap allocation just to track position. The warehouse system's own scan buffer pool leans on the identical trade, and shows exactly where that trade turns risky:

struct RentedScanBuffer : IDisposable
{
    private readonly byte[] _buffer;
    private bool _returned;

    public RentedScanBuffer(int size) { _buffer = ArrayPool<byte>.Shared.Rent(size); _returned = false; }

    public void Dispose()
    {
        if (!_returned) { ArrayPool<byte>.Shared.Return(_buffer); _returned = true; }
    }
}

Because it's a struct, every copy of RentedScanBuffer carries its own _returned flag. Pass one into a helper method by value — the ordinary, unremarkable way a struct gets passed — and that helper's Dispose(), if it ever calls one, operates on its own copy's flag. The original, sitting in the caller's using block, still thinks the buffer hasn't been returned yet and dutifully returns it a second time when the block exits — a double-return into the pool that can hand the same backing array out to two unrelated callers at once. Which is exactly why you don't see struct-based IDisposable types very often outside of narrow, performance-critical corners of a codebase, built by someone who sat down and reasoned through every place a copy of the struct could get made. The instant one slips into a code path its author didn't reason through — a helper method that quietly receives it as an ordinary value-type argument, say — the "dispose it exactly once" promise the whole pattern depends on quietly stops holding.