9

在循环外而不是在循环内声明在循环中使用的变量是否更好?有时我会看到在循环内声明变量的示例。这是否有效地导致程序在每次循环运行时为新变量分配内存?或者.NET 是否足够聪明,可以知道它实际上是同一个变量。

例如,从这个答案中查看下面的代码。

public static void CopyStream(Stream input, Stream output)
{
    byte[] buffer = new byte[32768];
    while (true)
    {
        int read = input.Read (buffer, 0, buffer.Length);
        if (read <= 0)
            return;
        output.Write (buffer, 0, read);
    }
}

这个修改后的版本会更有效吗?

public static void CopyStream(Stream input, Stream output)
{
    int read; //OUTSIDE LOOP
    byte[] buffer = new byte[32768];
    while (true)
    {
        read = input.Read (buffer, 0, buffer.Length);
        if (read <= 0)
            return;
        output.Write (buffer, 0, read);
    }
}
4

6 回答 6

9

不,它不会更有效率。但是,我会以这种方式重写它,无论如何都要在循环之外声明它:

byte[] buffer = new byte[32768];
int read;
while ((read = input.Read(buffer, 0, buffer.Length)) > 0)
{
    output.Write(buffer, 0, read);
}

我通常不喜欢在条件下使用副作用,但实际上该Read方法为您提供了两位数据:您是否已到达流的末尾,以及您已阅读了多少。while 循环现在说,“虽然我们已经设法读取了一些数据......复制它。”

这有点像使用int.TryParse

if (int.TryParse(text, out value))
{
    // Use value
}

您再次使用在条件中调用该方法的副作用。正如我所说,当您处理返回两位数据的方法时,除了这种特殊模式外,我没有这样做的习惯。

同样的事情出现在从 a 读取行TextReader

string line;
while ((line = reader.ReadLine()) != null)
{
    ...
}

回到你原来的问题:如果一个变量要在循环的每次迭代中被初始化并且它只在循环体中使用,我几乎总是在循环中声明它。这里的一个小例外是,如果变量被匿名函数捕获 - 那时它会改变行为,我会选择任何一种形式给我想要的行为......但这几乎总是“内部声明” “无论如何形成。

编辑:说到范围,上面的代码确实使变量的范围比它需要的更大......但我相信它使循环更清晰。如果您愿意,您始终可以通过引入新范围来解决此问题:

{
    int read;
    while (...)
    {
    }
}
于 2010-07-13T21:02:50.550 回答
3

在对您没有帮助的不太可能的环境中,它仍然是一个微优化。清晰度和适当的范围界定等因素比边缘情况重要得多,边缘情况可能几乎没有区别。

您应该在不考虑性能的情况下为变量提供适当的范围。当然,复杂的初始化是另一回事,所以如果某些东西应该只初始化一次但只在循环中使用,你仍然想在外面声明它。

于 2010-07-13T21:13:22.213 回答
2

我将同意大多数其他答案,但需要注意的是。

如果您使用 lambda 表达式,则必须小心捕获变量。

static void Main(string[] args)
{
    var a = Enumerable.Range(1, 3);
    var b = a.GetEnumerator();
    int x;
    while(b.MoveNext())
    {
        x = b.Current;
        Task.Factory.StartNew(() => Console.WriteLine(x));
    }
    Console.ReadLine();
}

会给出结果

3
3
3

在哪里

static void Main(string[] args)
{
    var a = Enumerable.Range(1, 3);
    var b = a.GetEnumerator();
    while(b.MoveNext())
    {
        int x = b.Current;
        Task.Factory.StartNew(() => Console.WriteLine(x));
    }
    Console.ReadLine();
}

会给出结果

1
2
3

或那里的一些命令。这是因为当任务最终启动时,它会检查它对 x 的引用的当前值。在第一个示例中,所有 3 个循环都指向同一个引用,而在第二个示例中,它们都指向不同的引用。

于 2010-07-13T21:42:04.123 回答
2

就像许多像这样的简单优化一样,编译器会为您处理它。如果您尝试这两种方法并在 ildasm 中查看程序集的 IL,您会发现它们都声明了一个 int32 读取变量,尽管它确实对声明进行了重新排序:

  .locals init ([0] int32 read,
           [1] uint8[] buffer,
           [2] bool CS$4$0000)

  .locals init ([0] uint8[] buffer,
           [1] int32 read,
           [2] bool CS$4$0000)
于 2010-07-14T01:56:45.433 回答
1

作为个人习惯,我通常更喜欢后者,因为即使 .NET 足够智能,我以后可能工作的其他环境也可能不够智能。它可能只不过是在循环内编译成额外的代码行来重新初始化变量,但这仍然是开销。

即使在任何给定示例中它们对于所有可衡量的目的都是相同的,我会说后者从长远来看引起问题的机会较小。

于 2010-07-13T21:03:52.020 回答
1

这真的没关系,如果我正在审查那个特定示例的代码,我不会在意任何一种方式。

但是,请注意,如果您最终在闭包中捕获“读取”变量,这两者可能意味着非常不同的事情。

请参阅 Eric Lippert 的这篇出色的帖子,其中出现了有关 foreach 循环的问题 - http://blogs.msdn.com/b/ericlippert/archive/2009/11/12/closing-over-the-loop-variable-considered-有害的.aspx

于 2010-07-13T21:09:28.260 回答