January 23rd, 2026
intriguinglikeheart6 reactions

C++ has scope_exit for running code at scope exit. C# says “We have scope_exit at home.”

The Windows Implementation Library (commonly known as wil) provides a helper called scope_exit which creates and returns an RAII object whose destructor runs the lambda you specify. This helper provides roughly equivalent functionality in C++ to what is called tryfinally in other languages such as C#.

// If there is an active primary Gadget, then do a bunch of stuff,
// but no matter what, make sure it is no longer active when finished.
var gadget = widget.GetActiveGadget(Connection.Primary);
if (gadget != null) {
    try {
        ⟦ lots of code ⟧
    } finally {
        widget.SetActiveGadget(Connection.Primary, null);
    }
}

One thing that is cumbersome about this pattern is that the cleanup code is far away from the place where the cleanup obligation was created, making it harder to confirm during code review that the proper cleanup did occur. Furthermore, you could imagine that somebody makes a change to the GetActiveGadget() call that requires a matching change to the SetActiveGadget(), but since the SetActiveGadget() is so far away, you may not realize that you need to make a matching change 200 lines later.

var gadget = widget.GetActiveGadget(Connection.Secondary);
if (gadget != null) {
    try {
        ⟦ lots of code ⟧
    } finally {
        widget.SetActiveGadget(Connection.Secondary, null);
    }
}

Another thing that is cumbersome about this pattern is that you may create multiple obligations at different points in the code execution, resulting in deep nesting.

var gadget = widget.GetActiveGadget(Connection.Secondary);
if (gadget != null) {
    try {
        ⟦ lots of code ⟧
        if (gadget.IsEnabled()) {
            try {
                ⟦ lots more code ⟧
            } finally {
                gadget.Disable();
            }
        }
    } finally {
        widget.SetActiveGadget(Connection.Secondary, null);
    }
}

Can we get scope_exit ergonomics in C#?

You can do it with the using statement introduced in .NET 8 and a custom class that we may as well call Scope­Exit.

public ref struct ScopeExit
{
    public ScopeExit(Action action)
    {
        this.action = action;
    }

    public void Dispose()
    {
        action.Invoke();
    }

    Action action;
}

Now you can write

var gadget = widget.GetActiveGadget();
if (gadget != null) {
    using var clearActiveGadget = new ScopeExit(() => widget.SetActiveGadget(null));
    ⟦ lots of code ⟧
    if (gadget.IsEnabled()) {
        using var disableGadget = new ScopeExit(() => gadget.Disable());
        ⟦ lots more code ⟧
    }
}

, Although many objects implement IDisposable so that you can clean them up with a using statement, it’s not practical to have a separate method for every possible ad-hoc cleanup that could be needed, and the ScopeExit lets us create bespoke cleanup on demand.¹

I’m adding this to my list of “Crazy C# ideas which never go anywhere,” joining CastTo<T> and As<T> and ThreadSwitcher.

¹ There might be common patterns like “If you Open(), then you probably want to Close()” which could benefit from a disposable-returning method, but you still have to wrap the result.

public ref struct OpenResult
{
    public bool Success { get; init; }

    public OpenResult(Widget widget)
    {
        this.widget = widget;
        Success = widget.Open();
    }

    public void Dispose()
    {
        if (Success) {
            widget.Close();
        }
    }

    private Widget widget;
}

Topics

Author

Raymond has been involved in the evolution of Windows for more than 30 years. In 2003, he began a Web site known as The Old New Thing which has grown in popularity far beyond his wildest imagination, a development which still gives him the heebie-jeebies. The Web site spawned a book, coincidentally also titled The Old New Thing (Addison Wesley 2007). He occasionally appears on the Windows Dev Docs Twitter account to tell stories which convey no useful information.

15 comments

Discussion is closed. Login to edit/delete existing comments.

Sort by :
  • Maik Schott

    Instead of just

    action.Invoke()

    I would call

    Interlocked.Exchange(ref action, null)?.Invoke();

    . This follows the contract of IDisposable.Dispose which states that multiple calls to Dispose should only dispose once.

    Also since C# 13 ref structs allow interfaces. Therefore we can let it explicitly implement IDisposable instead of just duck-typing it.

    • GL

      There's no point in doing interlocked operations on fields of ref structs, because (1) they only live as a thread's local variables, (2) it's undefined behavior to access another thread's local variables (see .NET / unsafe code best practices / 8. cross-thread access to local variables). Consequently, a ref struct can only be operated by one thread, so the interlocked exchange can be optimized to read-and-reset.

      On the interface, did you mean "we can let it (implicitly) implement `IDisposable`"? It's a bad and unnecessary idea to explicitly implement `IDisposable` on ref structs, because (1) it'll be more difficult to call `IDisposable.Dispose`...

      Read more
  • Otul Osan · Edited

    On Windows 9x, what was the use cases for C:\WINDOWS\WINSTART.BAT compared to C:\AUTOEXEC.BAT, and at which phase of the startup process it's launched?

    Could we see an explanation of the whole Windows 9x startup and shutdown process one day? On Wikipedia, The 16-bit modules are loaded doesn't tell what precisely technically happens after WIN.COM is launched, besides loading modules.
    For example I don't know what module switches to VGA mode in the boot process, draws the busy cursor on black background for 2 seconds and then paints the wallpaper.

    Read more
    • Neil Rashbrook

      I see even JavaScript is getting this now, I believe it looks something like this:

      function ScopeExit(callback) {
        return { [Symbol.dispose]: callback };
      }

      Then you can use

      using clearActiveGadget = ScopeExit(() => widget.SetActiveGadget(null));

      (Yes you can do this with classes if you like.)

    • Neil Rashbrook

      I think it was used for TSRs that were only needed in protected mode? The only one I know of was Novell’s BREQUEST for BTRIEVE. Windows BTRIEVE applications want to make calls to BREQUEST. MS-DOS BTRIEVE applications tended to have it statically linked. This is all a very hazy memory but it’s the best I can do, sorry.

    • Dan Bugglin

      AUTOEXEC.BAT was executed by MS-DOS as part of the boot process (the second to last step before launching COMMAND.COM I think). Win 9x shipped on top of MS-DOS 7.x so that meant you got AUTOEXEC.BAT and the full MS-DOS trimmings. Windows NT was built from the ground up to not need MS-DOS so no MS-DOS stuff.

      I checked Windows 98 and Windows 3.1 VMs I have, neither have a WINSTART.BAT, and I have not heard of it before despite using Windows since 3.1. Back in those days you just ran WIN.COM to launch Windows, though sometimes you wanted to switch out...

      Read more
      • Otul Osan · Edited

        If you want to try (Windows 95), WINSTART.BAT is executed at a later stage in the boot process in protected mode, create a file in C:\WINDOWS\WINSTART.BAT and put COMMAND.COM as the content.

  • George Styles

    Ive used this type of approach before when writing my own profiler. Start the method with

    using (new MyProfiler("LoadData")) 

    records the tick time in the constructor, and it automatically calls the dispose method when scope is exitted, at which point we calculate how many ticks and record.