Rust: константы, переменные и изменчивость - о боже!

Перевод | Автор оригинала: Matt Oswalt

В прошлом посте я написал о своем пути от Python к Go в качестве основного языка и о том, как я сейчас изучаю Rust.

Это будет первая из серии статей о Rust, в основном написанных с этой точки зрения. Я понимаю, что не все перейдут на Rust с Go, но это моя точка зрения, и будет невозможно не допустить, чтобы эта перспектива просматривалась и не проводила сравнения между Rust и Go или Python. Я считаю, что лучше всего мы учимся, опираясь на то, что мы знаем, поэтому я буду проводить много таких сравнений для собственного назидания. Смирись с этим. :)

Я буду писать эту серию, просматривая книгу Rust и в основном комментируя ее. Я буду наблюдать за тем, что вижу, и извлекаю уроки, а также делаю некоторые сравнения с концепциями, с которыми я уже знаком. Для этого лучше всего начать с главы 3 «Общие концепции программирования», где я могу провести некоторые сравнения с тем, что мне известно.

Я действительно рекомендую пройти главу 1 на реальном примере hello, world и последующую главу 2, которая представляет собой немного более продвинутый пример hello world, который не погружается очень глубоко, но представляет собой приятный вихревой тур по базовым, базовым концепциям в Rust. Я думаю, они проделали хорошую работу, продемонстрировав вам практический пример, вызывающий интерес, прежде чем погрузиться в более глубокое справочное содержание глав 3+.

Переменные и изменчивость

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

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

// (go)

// This is a constant. If we try to re-assign to this,
// we'll get a compiler error.
const strConst = "Hello!"

// This is a variable. We can re-assign to this to our
// heart's delight.
var strVar = "Hello!"

Так работают многие языки. Однако Rust по умолчанию использует неизменяемость, то есть, если вы не укажете иное, предполагается, что то, что вы создаете, следует рассматривать как константу. Чтобы облегчить это, можно использовать ключевое слово mut, чтобы указать, что переменная должна быть изменяемой:

// (rust)

// This is an integer without the "mut" keyword, and therefore
// defaults to immutable. We will get a compiler error if we try
// to change this.
let intConst = 5;

// The "mut" keyword means we're "opting in" to mutability here,
// so "intVar" will act like a traditional variable. We can change it.
let mut intVar = 5;

Мне действительно нравится, что это заставляет меня тщательно думать каждый раз, когда я что-то выделяю, о том, должно ли то, с чем я работаю, быть изменяемым, или я могу избежать наказания за неизменность. Похоже, что большинство вещей в Rust именно так; немного подумать перед компиляцией означает гораздо большую безопасность во время выполнения. Использование неизменяемого значения по умолчанию может показаться препятствием, но если это устраняет целый класс ошибок, я за это.

Меня действительно беспокоит то, что это означает, что оксюморонический термин «неизменяемая переменная» теперь используется, но я думаю, что переживу это.

Константы мертвы? Подожди, нет.

Я начал писать этот раздел до того, как прокрутил страницу вниз и увидел «Различия между константами и переменными». Оказывается, даже несмотря на то, что мы можем создавать «неизменяемые переменные», в Rust все еще есть концепция «константы», и в этом разделе перечислены несколько ключевых различий между ними.

Если честно, первые три сначала заставили меня фыркнуть. Короче:

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

«Последнее отличие состоит в том, что константы могут быть установлены только для константного выражения, а не для результата вызова функции или любого другого значения, которое может быть вычислено только во время выполнения».

Это говорит мне о том, что, хотя мы можем оптимизировать наш код, установив переменные по умолчанию на «неизменяемые», Rust все равно нужно проделать небольшую дополнительную работу, чтобы сделать эту структуру доступной во время выполнения для заполнения значением, которое может исходить от чего-то вроде функции вызов. Таким образом, хотя мы устраняем большую часть изменчивости, связанной с изменением переменной, все же остается некоторое динамическое поведение во время выполнения.

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

Хотя сначала это сбивает с толку, мне действительно нравится, что теперь у нас есть три уровня изменчивости:

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

Затенение

Когда я прочитал этот раздел, Shadowing немного поразил меня, но на самом деле это довольно просто. И это очень актуально для Rust, поскольку он разработан, чтобы дать нам дополнительную гибкость без слишком большого ущерба для безопасности во время компиляции.

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

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

fn main() {

    // First declaration of x
    let x = 5;

    // At this point, x is 5, so this is like saying
    // x = 5 + 1, which result in 6
    let x = x + 1;

    // At this point, x is 6, so this is like saying
    // x = 6 * 2, which results in 12.
    let x = x * 2;

    // "The value of x is: 12"
    println!("The value of x is: {}", x);
}

Кстати, это еще одна вещь, которую нельзя сделать с константами. Попробуйте скомпилировать эту программу:

const X: i32 = 100;

fn main() {
    let X = 5; // <---- compiler error here
    println!("The value of X is: {}", X);
}

и вы получите:

error[E0005]: refutable pattern in local binding: `std::i32::MIN..=99i32` and `101i32..=std::i32::MAX` not covered
 --> src/main.rs:4:9
  |
4 |     let X = 5;
  |         ^ interpreted as a constant pattern, not new variable

Вывод

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

На данный момент я хочу сделать следующие выводы: