14

With the new C# 8 Using Declaration Syntax, what is containing scope of a second consecutive using statement?

TL;DR

Previous to C# 8, having a consecutive using statement like:

using(var disposable = new MemoryStream())
{
    using(var secondDisposable = new StreamWriter(disposable))
    {}
}

would expand to something like the following (My Source):

MemoryStream disposable = new MemoryStream();
try {
    {
        StreamWriter secondDisposable = new StreamWriter(disposable);    
        try{
            {}
        }
        finally {
            if(secondDisposable != null) ((IDisposable)secondDisposable).Dispose();
        }
    }
}
finally {
    if(disposable != null) ((IDisposable)disposable).Dispose();
}

I know that there are two other possible expansions but they all are roughly like this

After upgrading to C# 8, Visual studio offered a Code Cleanup suggestion that I'm not certain I believe is an equivalent suggestion.

It converted the above consecutive using statement to:

using var disposable = new MemoryStream();
using var secondDisposable = new StreamWriter(disposable);

To me this changes the second's scope to the same scope as the first. In this case, It would probably coincidentally dispose of the streams in the correct order, but I'm not certain I like to rely on that happy coincidence.

To be clear on what VS asked me to do: I first converted the inner (which made sense because the inner was still contained in the outer's scope). Then I converted the outer (which locally made sense because it was still contained in the method's scope). The combination of these two clean ups is what I'm curious about.

I also recognize that my thinking on this could be slightly (or even dramatically) off, but as I understand it today, this doesn't seem correct. What is missing in my assessment? Am I off base?

The only thing I can think of is that there is some sort of an implicit scope inserted in the expansion for everything following a declaration statement.

Justin Blakley
  • 458
  • 4
  • 16
  • 4
    This is a small point, but worth mentioning. In C#, scope is defined as the region of program text in which a thing may be referred to by its unqualified simple name. You're using scope to mean "the lifetime of a local variable". There is a connection between these two things because a *local variable declaration space* and *the scope of the names of those variables* are the same region of program text. But remember, C# is allowed to *extend* or *shorten* the lifetime of a local variable to make that lifetime *different* than the time when control is in the scope. – Eric Lippert Dec 17 '19 at 16:30
  • 2
    My point is that it can be confusing to think of "using" as "it disposes when control leaves the scope of the variable". As you note, "using" is effectively a try-finally, and it disposes when control enters the finally. And again, this is *independent* of the lifetime of the *variable* that holds the reference to the resource being disposed. That variable's lifetime could be extended! – Eric Lippert Dec 17 '19 at 16:32
  • @EricLippert The "Whats new in C# 8" document [here](https://learn.microsoft.com/en-us/dotnet/csharp/whats-new/csharp-8#using-declarations) describes the using declaration as "...tells the compiler that the variable being declared should be disposed at the end of the enclosing scope". Is that in conflict with your point? – Justin Blakley Dec 18 '19 at 15:27
  • Ah, in reviewing my comments I see I have accidentally made things unnecessarily more confusing by trying to be more precise; my bad. The call to `Dispose` will happen when control leaves the scope, *even if the lifetime of the variable is extended*, and that's what I should have emphasized. Your concern that you expressed in the question is about when the disposal happens, not what the lifetime of the variable is. Sorry if that was confusing. – Eric Lippert Dec 18 '19 at 15:32
  • All good, I realize now that I sort of buried the lead in paragraph four, in that I was concerned about the `Dispose` not the scope itself. – Justin Blakley Dec 18 '19 at 15:47

3 Answers3

18

In this case, It would probably coincidentally dispose of the streams in the correct order, but I'm not certain I like to rely on that happy coincidence.

From the spec proposal:

The using locals will then be disposed in the reverse order in which they are declared.

So, yes, they already thought about it and do the disposal in the expected order, just as chained using statements would before it.

Damien_The_Unbeliever
  • 234,701
  • 27
  • 340
  • 448
2

To Illustrate the Daminen's answer; When you have a method something like;

public void M() 
{
    using var f1 = new System.IO.MemoryStream(null,true);    
    using var f2 = new System.IO.MemoryStream(null,true);
    using var f3 = new System.IO.MemoryStream(null,true);
}

IL converts it into;

public void M()
{
    MemoryStream memoryStream = new MemoryStream(null, true);
    try
    {
        MemoryStream memoryStream2 = new MemoryStream(null, true);
        try
        {
            MemoryStream memoryStream3 = new MemoryStream(null, true);
            try
            {
            }
            finally
            {
                if (memoryStream3 != null)
                {
                    ((IDisposable)memoryStream3).Dispose();
                }
            }
        }
        finally
        {
            if (memoryStream2 != null)
            {
                ((IDisposable)memoryStream2).Dispose();
            }
        }
    }
    finally
    {
        if (memoryStream != null)
        {
            ((IDisposable)memoryStream).Dispose();
        }
    }
}

Which is same as nested using statements you can check from here: https://sharplab.io/#v2:CYLg1APgAgTAjAWAFBQMwAJboMLoN7LpGYZQAs6AsgBQCU+hxTUADOgG4CGATugGZx0AXnQA7AKYB3THAB0ASQDysyuIC2Ae24BPAMoAXbuM5rqogK4AbSwBpD58bQDcTRkyKsOPfjGFipMgrKqpo6BkYmZla29o5Obu6eXLx8GCIS0lBySirqWnqGxqYW1nbcDs4JAL7IVUA===

ilkerkaran
  • 4,214
  • 3
  • 27
  • 42
1

I'd like to see the real function that's using this. The compiler won't change scope or sequence of allocations or disposals willy-nilly. If you have a method like:

void foo()
{
    using(var ms = new MemoryStream())
    {
        using(var ms2 = new MemoryStream())
        {
            /// do something
        }
    }
}

Then the Dispose() order doesn't matter, so it's safe for the compiler to arrange things however it sees fit. There may be other cases where the order is important, and the compiler should be smart enough to recognize that. I wouldn't file that under "coincidence" so much as "good AST analysis."

3Dave
  • 28,657
  • 18
  • 88
  • 151