Должны ли объявления переменных всегда размещаться вне цикла? - PullRequest
9 голосов
/ 14 июля 2010

Лучше ли объявлять переменную, используемую в цикле, вне цикла, а не внутри? Иногда я вижу примеры, где переменная объявляется внутри цикла. Эффективно ли это заставляет программу выделять память для новой переменной каждый раз при запуске цикла? Или .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);
    }
}

Ответы [ 6 ]

9 голосов
/ 14 июля 2010

Нет, это не будет более эффективным. Тем не менее, я бы переписал его так, чтобы в любом случае объявить его вне цикла:

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
}

Опять вы используете побочный эффект вызова метода в условии. Как я уже сказал, у меня нет привычки делать это за исключением для этого конкретного шаблона, когда вы имеете дело с методом, возвращающим два бита данных.

То же самое происходит при чтении строк из TextReader:

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

Возвращаясь к исходному вопросу: если переменная будет инициализироваться на каждой итерации цикла и , она будет использоваться только в теле цикла, я почти всегда объявляю ее в петля. Единственное незначительное исключение здесь - это если переменная захватывается анонимной функцией - в этот момент это изменит поведение, и я выберу любую форму, которая дала мне желаемое поведение ... но это почти всегда "объявление внутри" "в любом случае.

РЕДАКТИРОВАТЬ: Когда дело доходит до области видимости, приведенный выше код действительно оставляет переменную в большем объеме, чем нужно ... но я считаю, что это делает цикл более четким. Вы всегда можете решить эту проблему, введя новую область видимости, если хотите:

{
    int read;
    while (...)
    {
    }
}
3 голосов
/ 14 июля 2010

В маловероятной среде, которая вам не поможет, это все равно будет микрооптимизацией.Такие факторы, как ясность и правильная область видимости, гораздо важнее, чем крайний случай, когда это может просто не иметь значения.

Вы должны дать своим переменным надлежащий охват, не задумываясь о производительности.Конечно, сложные инициализации - это другой зверь, поэтому, если что-то нужно инициализировать только один раз, но использовать только в цикле, вы все равно захотите объявить это снаружи.

2 голосов
/ 14 июля 2010

Как и в случае с множеством простых оптимизаций, подобных этой, компилятор позаботится об этом за вас. Если вы попробуете оба из них и посмотрите на IL сборок в ildasm, вы увидите, что они оба объявляют одну переменную чтения 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)
2 голосов
/ 14 июля 2010

Я согласен с большинством этих других ответов с оговоркой.

Если вы используете выражения lambada, вы должны быть осторожны с захватом переменных.

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 цикла указывали на одну и ту же ссылку, а во втором примере все они указывали на разные ссылки.

1 голос
/ 14 июля 2010

Это действительно не имеет значения, и если бы я просматривал код для этого конкретного примера, мне было бы все равно.

Однако имейте в виду, что эти два значения могут означать очень разные вещи, если вы в конечном итоге захватите переменную read в замыкании.

См. Этот замечательный пост от Эрика Липперта, где эта проблема возникает в отношении циклов foreach - http://blogs.msdn.com/b/ericlippert/archive/2009/11/12/closing-over-the-loop-variable-considered-harmful.aspx

1 голос
/ 14 июля 2010

Я обычно предпочитал последнее как личную привычку, потому что, даже если .NET достаточно умен, другие среды, в которых я мог бы работать позже, могут быть недостаточно умными.Это может быть не что иное, как компиляция до дополнительной строки кода внутри цикла для повторной инициализации переменной, но это все еще накладные расходы.

Даже если они идентичны для всех измеримых целей в любом данном примере,Я бы сказал, что у последнего меньше шансов вызвать проблемы в долгосрочной перспективе.

...