Логически, но незаконно неинициализированная переменная в Rust - PullRequest
9 голосов
/ 06 августа 2020

Я относительно новичок в Rust. Этот вопрос оказался довольно длинным, поэтому я начну с подведения итогов: Какое решение вы предпочитаете? Есть ли у вас какие-либо идеи или замечания?

Мой код не компилируется, потому что строки 6 (prev = curr) и 12 (bar(...)) используют переменные, которые, по мнению компилятора, возможно, не инициализированы. Как программист, я знаю, что нет причин для беспокойства, потому что строки 6 и 12 не выполняются во время первой итерации.

let mut curr: Enum;
let mut prev: Enum;

for i in 0..10 {
    if i > 0 {
        prev = curr;
    }

    curr = foo();

    if i > 0 {
        bar(&curr, &prev);
    }
}

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

1) Инициализировать и перестать слишком много думать

Я мог бы просто инициализировать переменные произвольными значениями . Риск состоит в том, что сопровождающий может ошибочно подумать, что эти первоначальные, надеюсь, неиспользуемые значения имеют какое-то важное значение. Строки 1-2 станут:

let mut curr: Enum = Enum::RED; // Just an arbitrary value!
let mut prev: Enum = Enum::BLUE; // Just an arbitrary value!

2) Добавьте None значение к Enum

Строки 1-2 станут

let mut curr: Enum = Enum::None;
let mut prev: Enum = Enum::None;

Причина, по которой мне не нравится это решение, состоит в том, что теперь я добавил новое возможное и ненатуральное значение в свое перечисление. Для моей собственной безопасности мне придется добавить проверки утверждений и сопоставления к foo() и bar(), а также к любой другой функции, которая использует Enum.

3) Option<Enum>

Я думаю, что это решение является наиболее очевидным, но оно делает код длиннее и труднее для понимания.

Строки 1-2 станут

let mut curr: Option<Enum> = None;
let mut prev: Option<Enum> = None;

На этот раз None принадлежит Option, а не Enum. Оператор innocent prev = curr станет

prev = match curr {
    Some(_) => curr,
    None => panic!("Ah!")
};

, а наивный вызов bar() превратится в

match prev {
    Some(_) => bar(&curr, &prev),
    None => panic!("Oy!")
};

Вопросы: Какое решение вы предпочитаете ? Есть ли у вас идеи или замечания?

Ответы [ 4 ]

12 голосов
/ 06 августа 2020

По возможности избегайте использования слишком большого количества изменяемых привязок переменных в Rust, особенно в циклах. Управление состоянием в al oop с использованием изменяющихся переменных может затруднить понимание кода и привести к ошибкам. Обычно это красный флаг, чтобы видеть переменную счетчика в for l oop, которую очень часто можно заменить итератором по значениям.

В вашем случае вы можете создать итератор для значения, полученные с помощью foo, и передать их парами в bar, используя tuple_windows из ящика itertools:

use itertools::Itertools as _; // 0.9.0
use std::iter::repeat_with;

for (prev, curr) in repeat_with(foo).tuple_windows().take(9) {
    bar(&curr, &prev);
}

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

Этот стиль очень лаконичный и выразительный. Например, в приведенном выше фрагменте подчеркивается, что bar будет вызываться 9 раз. Вы также можете немного изменить его, если хотите подчеркнуть, что foo будет вызываться 10 раз:

for (prev, curr) in repeat_with(foo).take(10).tuple_windows() {
    bar(&curr, &prev);
}
7 голосов
/ 06 августа 2020

Фактически, logi c, который вы выражаете, называется «складкой», и вы можете использовать складку вместо al oop.

Что можно сделать, это:

(0..9).map(|_|foo()) //an iterator that outputs foo() 9 times, not 10
   .fold(foo(), |prev, curr| {bar(&curr, &prev); curr});

Деловой конец - это, конечно, строка с .fold в ней. Что делает свертка, так это то, что она принимает один аргумент в качестве начального значения, вызывает бинарную функцию для этого начального значения и текущего значения, создаваемого итератором, а затем использует результат в качестве нового начального значения для следующей итерации. Я просто использовал здесь функцию, которая вызывается для побочного эффекта, как и у вас, и в качестве результирующего значения используйте значение curr, которое, таким образом, используется в качестве начального значения следующей итерации и служит prev.

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

https://doc.rust-lang.org/std/iter/trait.Iterator.html#method .fold

3 голосов
/ 06 августа 2020

Я также очень новичок в Rust, но я считаю третий вариант наиболее ясным. Я добавлю, что unwrap() или expect() можно использовать вместо выражений соответствия с тем же значением («Я знаю, что это занято»):

let mut curr: Option<Enum> = None;
let mut prev: Option<Enum> = None;

for i in 0..10 {
    if i > 0 {
        prev = curr;
    }

    curr = Some(foo());

    if i > 0 {
        bar(curr.as_ref().unwrap(), prev.as_ref().unwrap());
    }
}

См. игровая площадка . В нем все еще есть небольшая дополнительная церемония.

Обычно мне трудно следовать более длинным реальным примерам циклов в этой форме; структура l oop заслоняет желаемый logi c. Если можно структурировать его больше как итерацию, я нахожу его более ясным:

for (prev, curr) in FooGenerator::new().tuple_windows().take(10) {
    bar(&prev, &curr);
}

См. игровую площадку .

1 голос
/ 10 августа 2020

Как бы то ни было, простой способ исправить ваш код - это вытащить первую итерацию из l oop. Из-за операторов if единственное, что делает первая итерация, - это вызов foo(). Кроме того, вы можете переместить присвоение prev в конец l oop, чтобы вы могли охватить curr внутри l oop:

let mut prev = foo();
for _ in 1..10 {
    let curr = foo();
    bar(&curr, &prev);
    prev = curr;
}

Обратите внимание, что я изменил диапазон, чтобы он начинался с 1 вместо 0, поскольку мы вытащили первую итерацию из l oop.

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

...