Добро пожаловать! Эта книга обучает основным принципам работы с языком программирования Rust. Rust — это системный язык программирования, внимание которого сосредоточено на трёх задачах: безопасность, скорость и параллелизм. Он решает эти задачи без сборщика мусора, что делает его полезным в ряде случаев, когда использование других языков было бы нецелесообразно: при встраивании в другие языки, при написании программ с особыми пространственными и временными требованиями, при написании низкоуровневого кода, такого как драйверы устройств и операционные системы. Во время компиляции Rust делает ряд проверок безопасности. За счёт этого не возникает накладных расходов во время выполнения приложения и устраняются все гонки данных. Это даёт Rust преимущество над другими языками программирования, имеющими аналогичную направленность. Rust также направлен на достижение «абстракции с нулевой стоимостью». Хотя некоторые из этих абстракций и ведут себя как в языках высокого уровня, но даже тогда Rust по-прежнему обеспечивает точный контроль, как делал бы язык низкого уровня.
Книга «Язык программирования Rust» делится на восемь разделов. Это введение является первым из них. Затем идут:
После прочтения этого введения, в зависимости от ваших предпочтений, вы можете продолжить дальнейшее изучение либо в направлении «Изучение Rust», либо в направлении «Синтаксис и семантика». Если вы предпочитаете изучить язык на примере реального проекта, лучшим выбором будет раздел «Изучение Rust». Раздел «Синтаксис и семантика» подойдёт тем, кто предпочитает тщательно изучить каждое понятие языка отдельно, перед тем как двигаться дальше. Большое количество перекрёстных ссылок соединяет эти части воедино.
Исходные файлы, из которых генерируется оригинал этой книги, могут быть найдены на Github: github.com/rust-lang/rust/tree/master/src/doc/trpl
Исходные файлы перевода этой книги на русский язык также находятся на GitHub: github.com/kgv/rust_book_ru
Чем же Rust может заинтересовать вас? Давайте рассмотрим несколько небольших примеров кода, чтобы продемонстрировать некоторые из его сильных сторон.
Основное понятие, которое делает Rust уникальным, называется «владение». Рассмотрим следующий небольшой пример:
fn main() { let mut x = vec!["Hello", "world"]; }fn main() { let mut x = vec!["Hello", "world"]; }
Эта программа создаёт связанное имя x
. Его значением является Vec<T>
,
«вектор», который мы создаём с помощью макроса, определённого в
стандартной библиотеке. Этот макрос называется vec
, и при его вызове
используется символ !
. Это следует из общего принципа Rust: делать вещи
явными. Макрос может делать значительно более сложные вещи, чем вызовы функций,
и поэтому они визуально отличаются. Символ !
также помогает при разборе, что
облегчает написание инструментов, а это тоже важно.
Мы использовали mut
, чтобы сделать x
изменяемым: связанные имена в Rust по
умолчанию неизменяемы. Дальше в примере мы будем изменять этот вектор.
Стоит также отметить, что здесь нам не нужно указывать тип, несмотря на то, что Rust является статически типизированным. Rust может выводить типы, что позволяет достичь компромисса между мощью статической типизации и многословностью указания типов.
Rust предпочитает выделять память в стеке, а не в куче: x
находится
непосредственно в стеке. Однако тип Vec<T>
выделяет пространство для элементов
вектора в куче. Если вы не знакомы с различиями этих двух видов выделения
памяти, можете пока просто проигнорировать эту информацию или же ознакомиться с
разделом «Стек и Куча». Как системный язык программирования, Rust даёт
вам возможность контролировать выделение памяти. Но не будем забегать вперёд, мы
только начинаем изучение языка.
Ранее мы упоминали, что «владение» — это то, что делает Rust уникальным. В
терминологии Rust, x
«владеет» вектором. Это означает, что как только x
выходит из области видимости, выделенная для вектора память будет освобождена.
Когда это будет происходить, определяется средствами компилятора Rust, а не
через механизмы наподобие сборщика мусора. Другими словами, в Rust вы не
вызываете функции вроде malloc
и free
собственноручно: компилятор статически
определяет, когда нужно выделить или освободить память, и вставляет эти вызовы
самостоятельно. Человек может совершить ошибку при использовании этих вызовов, а
компилятор — никогда.
Давайте добавим ещё одну строку в наш пример:
fn main() { let mut x = vec!["Hello", "world"]; let y = &x[0]; }fn main() { let mut x = vec!["Hello", "world"]; let y = &x[0]; }
Мы создаём ещё одно имя, y
. В этом случае, y
является «ссылкой» на первый
элемент вектора. Ссылки в Rust похожи на указатели в других языках, но с
дополнительными проверками безопасности на этапе компиляции. Ссылки
взаимодействуют с системой прав владения при помощи
«заимствования». Ссылки заимствуют то, на что они указывают, а не
получают права владения им. Разница в том, что при заимствовании ссылка не
освобождает основную память, когда выходит за пределы области видимости. Если бы
это было не так, то память освобождалась бы два раза — плохо!
Давайте добавим третью строку. На первый взгляд в коде нет ничего такого, но он вызывает ошибку компиляции:
fn main() { let mut x = vec!["Hello", "world"]; let y = &x[0]; x.push("foo"); }fn main() { let mut x = vec!["Hello", "world"]; let y = &x[0]; x.push("foo"); }
push
— это метод, который добавляет ещё один элемент в конец вектора. Когда мы
пытаемся скомпилировать эту программу, то получаем ошибку:
error: cannot borrow `x` as mutable because it is also borrowed as immutable
x.push("foo");
^
note: previous borrow of `x` occurs here; the immutable borrow prevents
subsequent moves or mutable borrows of `x` until the borrow ends
let y = &x[0];
^
note: previous borrow ends here
fn main() {
}
^
Вот так! Компилятор Rust в некоторых случаях выдаёт достаточно подробные ошибки,
и это как раз один из таких случаев. Как объясняется в ошибке, несмотря на то,
что мы и сделали наше имя изменяемым, мы всё ещё не можем вызвать метод
push
. Это потому, что у нас уже есть ссылка на элемент вектора, y
. Изменять
вектор, пока существует другая ссылка на него, опасно, потому что можно сделать
ссылку недействительной. В данном конкретном случае, когда мы создаём вектор, у
нас есть выделенное пространство памяти только для двух элементов. Добавление
третьего элемента будет означать выделение новой области памяти для всех этих
элементов, копирование старых значений и обновление внутреннего указателя на эту
память. Всё это работает просто отлично. Проблема заключается в том, что y
не
будет обновлена, из-за чего мы получим «зависший указатель». И это плохо. В этом
случае любое использование y
будет означать ошибку. Компилятор обнаружил
данную проблему.
Так как же нам решить эту проблему? Есть два подхода, которые мы можем использовать. Первый заключается в создании копии вместо ссылки:
fn main() { let mut x = vec!["Hello", "world"]; let y = x[0].clone(); x.push("foo"); }fn main() { let mut x = vec!["Hello", "world"]; let y = x[0].clone(); x.push("foo"); }
По умолчанию, Rust использует семантику перемещения, поэтому, если мы
хотим сделать копию некоторых данных, мы должны вызывать метод clone()
. В этом
примере y
больше не является ссылкой на вектор, хранящийся в x
, но является
копией его первого элемента, "Hello"
. Теперь, когда у нас больше нет ссылки,
метод push()
прекрасно работает.
Если нам всё же нужна ссылка, то следует использовать другой вариант: убедиться, что наша ссылка выходит из области видимости, прежде чем мы попытаемся сделать изменения. Это выглядит примерно так:
fn main() { let mut x = vec!["Hello", "world"]; { let y = &x[0]; } x.push("foo"); }fn main() { let mut x = vec!["Hello", "world"]; { let y = &x[0]; } x.push("foo"); }
Мы создали внутреннюю область видимости с помощью дополнительных фигурных
скобок. y
выйдет за пределы этой области видимости до вызова метода push()
,
и поэтому все будет хорошо.
Концепция владения хороша не только для предотвращения проблемы повисших указателей, но также и для всей совокупности связанных с этим проблем, таких как: недействительность итератора, параллелизм и многое другое.