C# 8 使用声明作用域混淆

14

使用新的C# 8 Using声明语法,第二个连续using语句的包含范围是什么?

简短回答

在C# 8之前,如果有一系列的using语句,例如:

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

会扩展成类似以下内容(来源):

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();
}

我知道还有另外两种可能的扩展,但它们大致都是这样的。

在升级到 C# 8 后,Visual studio 提供了一个代码清理建议,但我不确定它是否是等效的建议。

它将上述连续的 using 语句转换为:

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

对我来说,这将第二个作用域更改为与第一个相同的作用域。在这种情况下,它可能会巧合地以正确的顺序处理流,但我不确定是否要依赖这种幸运的巧合。

明确一下VS要求我做什么:首先转换内部(因为内部仍包含在外部的范围内是有意义的)。然后转换外部(因为在方法的作用域中仍有意义)。这两个清理的结合是我感到好奇的地方。

我也认识到我的想法可能有些错误(甚至非常错误),但根据我今天的理解,这似乎不正确。我的评估中缺少什么?我错了吗?

唯一能想到的就是,在声明语句后面的所有内容中插入了某种隐式作用域。


4
这是一个细节问题,但值得一提。在C#中,“作用域”被定义为程序文本区域,在该区域内可以使用未经限定的简单名称来引用某个实体。您正在使用“作用域”一词来表示“局部变量的生存期”。这两者之间存在联系,因为“局部变量声明空间”和“那些变量的名称的作用域”是程序文本中相同的区域。但请记住,C#允许延长缩短局部变量的生存期,以使其生存期不同于控制流在作用域中的时间。 - Eric Lippert
2
我的观点是,将“使用”视为“在变量的作用域离开时进行处理”可能会令人感到困惑。正如您所指出的,“using”实际上是一个try-finally块,在控制进入finally块时进行处理。而且,这与持有对被处理资源的引用的变量的生命周期是独立的。该变量的生命周期可以被延长! - Eric Lippert
@EricLippert,“C# 8有什么新特性”文档在此中将using声明描述为“告诉编译器正在声明的变量应在封闭作用域结束时被处理”。这是否与您的观点相冲突? - Justin Blakley
啊,回顾我的评论,我发现我试图更加精确而不必要地使事情变得更加混乱;我的错。即使变量的生命周期被延长,当控件离开作用域时,Dispose 的调用也会发生,这才是我应该强调的。你在问题中表达的关注是关于处理发生的时间,而不是变量的生命周期。如果那让你感到困惑,我很抱歉。 - Eric Lippert
没问题,我现在意识到我在第四段有点埋了主题,我的担忧是关于Dispose而不是作用域本身。 - Justin Blakley
3个回答

18
在这种情况下,它可能会巧合地以正确的顺序处理流,但我不确定我想要依赖这种幸运的巧合。根据规范提案,这些using本地变量将按照声明的相反顺序进行处理。因此,是的,他们已经考虑到了这一点,并按预期顺序进行处置,就像在之前链接的使用语句中一样。

2
为了阐明Daminen的回答,当你有一个类似于以下代码的方法时:
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将其转换为;
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();
        }
    }
}

这与嵌套使用语句相同,您可以从这里检查:https://sharplab.io/#v2:CYLg1APgAgTAjAWAFBQMwAJboMLoN7LpGYZQAs6AsgBQCU+hxTUADOgG4CGATugGZx0AXnQA7AKYB3THAB0ASQDysyuIC2Ae24BPAMoAXbuM5rqogK4AbSwBpD58bQDcTRkyKsOPfjGFipMgrKqpo6BkYmZla29o5Obu6eXLx8GCIS0lBySirqWnqGxqYW1nbcDs4JAL7IVUA===

1

我希望能够看到使用该函数的真实功能。编译器不会轻易更改作用域、分配或释放的顺序。如果您有一个像这样的方法:

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

那么 Dispose() 命令的顺序就不重要了,因此编译器可以安排任何它认为合适的事情。可能还有其他情况下顺序很重要,编译器应该足够聪明以识别出来。我不会将其归类为“巧合”,而更多地是“良好的 AST 分析”。


更新了问题以反映更合适的示例。 - Justin Blakley

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接