Объявления переменной должны всегда помещаться за пределами цикла?

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

Эта измененная версия больше была бы efficent?

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

9
задан Community 23 May 2017 в 11:54
поделиться

6 ответов

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

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)
{
    ...
}

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

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

{
    int read;
    while (...)
    {
    }
}
9
ответ дан 4 December 2019 в 11:39
поделиться

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

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

3
ответ дан 4 December 2019 в 11:39
поделиться

Как и в случае с множеством простых оптимизаций, подобных этой, компилятор позаботится об этом за вас. Если вы попробуете оба этих варианта и посмотрите на IL сборки в ildasm, то увидите, что они оба объявляют одну переменную int32 read, хотя он и меняет порядок объявлений:

  .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
ответ дан 4 December 2019 в 11:39
поделиться

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

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

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

1
ответ дан 4 December 2019 в 11:39
поделиться

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

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

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

2
ответ дан 4 December 2019 в 11:39
поделиться

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

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

1
ответ дан 4 December 2019 в 11:39
поделиться
Другие вопросы по тегам:

Похожие вопросы: