using 语句的有效使用(MSDN 中没有)

Effective usage of the using-statement (not in MSDN)

我已经阅读了相应的文档页面,但我的问题仍然没有得到解答。 假设我想在 while 循环中使用 disposable Object,如下所示:

StreamReader reader;
while (!ShouldStop)
{
    using (reader = new StreamReader(networkStream))
    {
         // Some code here...    
    }
}

如你所见,我在 using 语句外声明了 StreamReade reader。我通常这样做是因为我认为内存的一个区域被分配给 StreamReader 只有 一次 。当我像这样使用 using 语句时:

while (!ShouldStop)
{
    using (StreamReader reader = new StreamReader(networkStream))
    {
        // Some code here...            
    }
}

我认为 StreamReader 对象的内存分配是连续的,因此 效率和性能要低得多 。 但是我不知道 using 语句的第一次使用是否定期调用实例的 Dispose() 函数。那么 using 语句的第一次用法与第二次用法相同吗?

您不会通过以下方式节省任何内存:

StreamReader reader;
while (!ShouldStop)
{
    using (reader = new StreamReader(networkStream))
    {
         // Some code here...    
    }
}

您已在循环外声明了变量,但每次迭代仍在创建一个新对象。

这两种情况在实例处置方面将具有相同的效果。在每次迭代中,在循环内部,每次都会创建和处理一个对象。

为了更好的理解请看下面的代码

class Program
{
    static void Main(string[] args)
    {
        Disposableclass obj; 

        for (int i = 0; i < 3; i++)
        {
            using (obj = new Disposableclass())
            { 
            }
        }

        /*for (int i = 0; i < 3; i++)
        {
            using (Disposableclass obj2 = new Disposableclass())
            { 
            }
        }*/

        Console.ReadKey();
    }

}

public class Disposableclass : IDisposable
{
    public Disposableclass()
    {
        Console.WriteLine("Constructor called: " + this.GetType().ToString());
    }
    public void Dispose()
    {
        Console.WriteLine("Dispose called: " + this.GetType().ToString());     

   }
}

在这两种情况下,我们将得到相同的输出,如下所示

Constructor called: TestSolution.Disposableclass

Dispose called: TestSolution.Disposableclass

Constructor called: TestSolution.Disposableclass

Dispose called: TestSolution.Disposableclass

Constructor called: TestSolution.Disposableclass

Dispose called: TestSolution.Disposableclass

这两者之间没有区别,因为每当您调用 new 时,都会考虑分配一块内存。 using 语句是这个语法糖(我的意思是 using 语句翻译成这个):

        StreamReader reader = new StreamReader(networkStream)

        try
        {
            //some codes here
        }
        finally
        {
            if (reader!=null)
               ( (IDisposable) reader).Dispose();
        }

所以在这两种情况下,最后都会释放内存,并在需要时再次分配。

I usually do this because I think that then an area of the memory is being allocated for that StreamReader only one time.

你错了。

局部变量在使用期间占用了一定数量的堆栈space,对象在new时占用了一定数量的堆space .这不会改变。

确实,编译器最终会使用您的方法占用更多堆栈 space。只需比较使用每种方法的两种方法的 IL。我们将使用这个 C#:

private static string LastLine1(NetworkStream networkStream)
{
    string last = null;
    StreamReader reader;
    while(!ShouldStop)
    {
        using(reader = new StreamReader(networkStream))
        {
            string line = reader.ReadLine();
            if(line != null)
                last = line;
        }
    }
    return last;
}
private static string LastLine2(NetworkStream networkStream)
{
    string last = null;
    while(!ShouldStop)
    {
        using(StreamReader reader = new StreamReader(networkStream))
        {
            string line = reader.ReadLine();
            if(line != null)
                last = line;
        }
    }
    return last;
}

我们得到这个 CIL:

.method private hidebysig static 
    string LastLine1 (
        class [System]System.Net.Sockets.NetworkStream networkStream
    ) cil managed 
{
    .maxstack 2
    .locals init (
        [0] string,
        [1] class [mscorlib]System.IO.StreamReader,
        [2] string,
        [3] class [mscorlib]System.IO.StreamReader
    )

    IL_0000: ldnull
    IL_0001: stloc.0
    IL_0002: br.s IL_0025
    IL_0004: ldarg.0
    IL_0005: newobj instance void [mscorlib]System.IO.StreamReader::.ctor(class [mscorlib]System.IO.Stream)
    IL_000a: dup
    IL_000b: stloc.1
    IL_000c: stloc.3
    .try
    {
        IL_000d: ldloc.1
        IL_000e: callvirt instance string [mscorlib]System.IO.TextReader::ReadLine()
        IL_0013: stloc.2
        IL_0014: ldloc.2
        IL_0015: brfalse.s IL_0019

        IL_0017: ldloc.2
        IL_0018: stloc.0

        IL_0019: leave.s IL_0025
    }
    finally
    {
        IL_001b: ldloc.3
        IL_001c: brfalse.s IL_0024

        IL_001e: ldloc.3
        IL_001f: callvirt instance void [mscorlib]System.IDisposable::Dispose()

        IL_0024: endfinally
    }
    IL_0025: call bool Demonstrate.Program::get_ShouldStop()
    IL_002a: brfalse.s IL_0004

    IL_002c: ldloc.0
    IL_002d: ret
}

.method private hidebysig static 
    string LastLine2 (
        class [System]System.Net.Sockets.NetworkStream networkStream
    ) cil managed 
{
    .maxstack 1
    .locals init (
        [0] string,
        [1] class [mscorlib]System.IO.StreamReader,
        [2] string
    )

    IL_0000: ldnull
    IL_0001: stloc.0
    IL_0002: br.s IL_0023
    IL_0004: ldarg.0
    IL_0005: newobj instance void [mscorlib]System.IO.StreamReader::.ctor(class [mscorlib]System.IO.Stream)
    IL_000a: stloc.1
    .try
    {
        IL_000b: ldloc.1
        IL_000c: callvirt instance string [mscorlib]System.IO.TextReader::ReadLine()
        IL_0011: stloc.2
        IL_0012: ldloc.2
        IL_0013: brfalse.s IL_0017

        IL_0015: ldloc.2
        IL_0016: stloc.0

        IL_0017: leave.s IL_0023
    }
    finally
    {
        IL_0019: ldloc.1
        IL_001a: brfalse.s IL_0022

        IL_001c: ldloc.1
        IL_001d: callvirt instance void [mscorlib]System.IDisposable::Dispose()

        IL_0022: endfinally
    }

    IL_0023: call bool Demonstrate.Program::get_ShouldStop()
    IL_0028: brfalse.s IL_0004

    IL_002a: ldloc.0
    IL_002b: ret
}

(严格来说,两者确实应该产生相同的代码,但事实是它们没有,你的方法是稍微长一点,使用堆栈space)。

因为 C# 编译器无法优化你在使用之外的 reader,实际上是你的方法导致额外的堆栈 space 被另一个副本占用reader.

如果您不熟悉 CIL,请比较 ILSpy 如何尝试将它们再次反编译回 C#:

private static string LastLine1(NetworkStream networkStream)
{
    string result = null;
    while (!Program.ShouldStop)
    {
        StreamReader streamReader2;
        StreamReader streamReader = streamReader2 = new StreamReader(networkStream);
        try
        {
            string text = streamReader.ReadLine();
            if (text != null)
            {
                result = text;
            }
        }
        finally
        {
            if (streamReader2 != null)
            {
                ((IDisposable)streamReader2).Dispose();
            }
        }
    }
    return result;
}

private static string LastLine2(NetworkStream networkStream)
{
    string result = null;
    while (!Program.ShouldStop)
    {
        using (StreamReader streamReader = new StreamReader(networkStream))
        {
            string text = streamReader.ReadLine();
            if (text != null)
            {
                result = text;
            }
        }
    }
    return result;
}

(当将其转换为实际上是 运行 的机器代码时,您也可能减少了空检查被优化的机会)。

it is much more less efficient & perfomant. However I dont know if the first usage of the using-statement calls the instance's Dispose()-function regularly.

using 无论哪种方式调用 Dispose() 都很好,但是您会稍微浪费一些,因此效率和性能会稍差一些。可能可以忽略不计,但你避免的方法肯定不是你声称的"much more less efficient & performant"。


总的来说,要缩小范围。主要原因是不再在范围内的变量是一个您不能再做错事甚至不必考虑的变量,因此您将拥有更清晰的代码,错误更少并且更容易找到错误。第二个原因是,在某些情况下,例如这种情况,更大的范围会导致代码浪费更多。

现在,将 assignments 放在循环之外确实可以提高性能。如果您的代码适用于:

using(var reader = new StreamReader(networkStream))
  while(!ShouldStop)
  {
    // do stuff
  }

然后这将节省堆搅动,最重要的是 减少,因此如果它可行,那将是一个改进。

然而,声明不会做任何事情,因此将它们放在循环之外无济于事,有时甚至会造成轻微阻碍。

前面关于内存分配的回答都是正确的。

当你在循环外声明 'reader' 变量时你做了什么:你保留了内存,但只是为了引用,本质上是一个指针,而不是为了对象本身;对象本身通过 new() 语句在堆上分配,并在 using 块末尾调用 Dispose() 时被释放。接下来的 new() 分配一个新对象。

危险在于:如果您要在 'using' 块之后使用 reader 对象引用,您将得到一个 ObjectDisposedException。

在您的上下文中,一直处置和重新创建 reader 对象没有意义。您可能应该将 using 语句放在 while 循环的外部。

但这会引出一个更广泛的问题,即您到底要达到什么目的。你为什么要检查 ShouldStop?你希望你的阅读循环能够被取消吗?通过另一个线程?确保线程间通信正确实现,并且当有人要取消你时,你不会处于等待读取数据的阻塞状态。另一方面,您不希望 运行 过于频繁地执行循环,否则会造成不必要的负载。