Самоучитель Rust: узнайте больше о языке программирования Rust

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

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

Обзор языка программирования Rust

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

Использование Rust в вашей собственной системе

Поскольку Rust является бесплатным программным обеспечением с открытым исходным кодом (FOSS), любой может загрузить инструментарий Rust и использовать его на своей собственной системе. В отличие от Python или JavaScript, Rust не является интерпретируемым языком. Вместо интерпретатора используется компилятор, как в C, C++ и Java. На практике это означает, что выполнение кода состоит из двух этапов:

  1. Компиляция исходного кода. В результате создается двоичный исполняемый файл.
  2. Выполнение полученного двоичного файла.

В простейшем случае оба этапа управляются через командную строку.

Совет

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

Rust можно использовать как для создания библиотек, так и исполняемых двоичных файлов. Если скомпилированный код является непосредственно исполняемой программой, в исходном коде должна быть определена функция main(). Как и в C/C++, она служит точкой входа в выполнение кода.

Установите Rust для учебника на локальную систему

Чтобы использовать Rust, вам сначала придется установить его локально. Если вы используете macOS, вы можете воспользоваться менеджером пакетов Homebrew. Homebrew также работает в Linux. Откройте командную строку («Terminal.App» на Mac), скопируйте в терминал следующую строку кода и выполните команду:

brew install rust
Примечание

Чтобы установить Rust на Windows или другую систему без Homebrew, вы можете использовать официальный инструмент Rustup.

Чтобы проверить, успешно ли прошла установка Rust, откройте новое окно в командной строке и выполните следующий код:

rustc --version

Если Rust правильно установлен в вашей системе, появится версия компилятора Rust. Если вместо этого появится сообщение об ошибке, при необходимости перезапустите установку.

Компиляция кода Rust

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

cd "$HOME/Desktop/"
mkdir rust-tutorial && cd rust-tutorial

Затем мы создадим файл исходного кода Rust для простого примера «Hello, World!».

cat << EOF > ./rust-tutorial.rs
fn main() {
    println!("Hello, World!");
}
EOF
Примечание

Файлы исходного кода Rust заканчиваются ярлыком .rs.

Далее мы скомпилируем исходный код Rust и выполним полученный двоичный файл.

# compile Rust source code
rustc rust-tutorial.rs
# execute resulting binary file 
./rust-tutorial
Совет

Используйте команду rustc rust-tutorial.rs && ./rust-tutorial, чтобы объединить эти два шага. Таким образом, вы можете перекомпилировать и запустить свою программу в командной строке, нажав стрелку вверх и клавишу Enter.

Управление пакетами Rust с помощью Cargo

В дополнение к собственно языку Rust существует ряд внешних пакетов. Эти так называемые crates можно получить в реестре пакетов Rust. Для этого используется инструмент Cargo, установленный вместе с Rust. Команда Cargo используется в командной строке и позволяет устанавливать пакеты и создавать новые пакеты. Проверить, правильно ли установлен Cargo, можно следующим образом:

cargo --version

Изучение основ Rust

Чтобы изучить Rust, мы рекомендуем вам самостоятельно протестировать примеры кода. Для этого вы можете использовать предварительно созданный файл rust-tutorial.rs. Скопируйте пример кода в этот файл, скомпилируйте его и выполните полученный двоичный файл. Чтобы это сработало, код примера должен быть вставлен внутрь функции main().

На Rust Playground вы также можете использовать Rust непосредственно в браузере и протестировать Rust таким образом.

Утверждения и блоки кода

Утверждения — это основные строительные блоки кода в Rust. Утверждение заканчивается точкой с запятой (;) и, в отличие от выражения, не возвращает значение. Несколько утверждений могут быть сгруппированы в блок. Блоки разграничиваются фигурными скобками «{}», как в C/C++ и Java.

Комментарии в Rust

Комментарии являются важной особенностью любого языка программирования. Они используются как для документирования кода, так и для планирования перед написанием кода. В Rust используется тот же синтаксис комментариев, что и в C, C++, Java и JavaScript: Любой текст после двойной косой черты интерпретируется как комментарий и игнорируется компилятором:

// This is a comment
// A comment
// that is
// spread across
// several lines.

Переменные и константы

В Rust мы используем ключевое слово «let» для объявления переменной. Существующая переменная может быть объявлена в Rust еще раз, и тогда она «затмевает» существующую переменную. В отличие от многих других языков, значение переменной не может быть легко изменено:

// Declare “age” variable and set value to “42” 
let age = 42;
// Value of the variable “age” cannot be changed 
age = 49; // compiler error
// with a renewed “let” the variable can be overwritten
let age = 49;

Чтобы обозначить значение переменной, которое можно изменить на более поздней стадии, Rust предлагает ключевое слово «mut». Значение переменной, объявленной с помощью «mut», может быть изменено:

let mut weight = 78;
weight = 75;

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

const VERSION: &str = "1.46.0";

Значение константы не может быть изменено — константа также не может быть объявлена как «mut». Кроме того, константа не может быть повторно объявлена:

// Defining constants
const MAX_NUM: u8 = 255;
MAX_NUM = 0; // compiler error, since the value of a constant cannot be changed
const MAX_NUM = 0; // compiler error, since constants cannot be re-declared

Концепция владения в Rust

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

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

let name = String::from("Peter Smith");
let _name = name;
println!("{}, world!", name); // compiler error, since the “name” value is passed on to “_name”.

При определении функций необходимо соблюдать особую осторожность: Если переменная передается в функцию, владелец значения меняется. Однако: переменная не может быть повторно использована после вызова функции. Но в Rust есть трюк, который можно использовать для этого: Вместо того чтобы передавать в функцию само значение, ссылка объявляется с помощью символа амперсанда (&). Это позволяет «заимствовать» значение переменной. Вот пример:

let name = String::from("Peter Smith");
// if the type of “name” parameter is defined as “String” instead of “&String” 
// the variable “name” can no longer be used after the function call
fn hallo(name: &String) {
  println!("Hello, {}", name);
}
// the function statement must also be marked with “&” 
// to mark it as a reference
hello(&name);
// without using the reference, this line leads to a compilation error
println!("Hello, {}", name);

Управляющие структуры

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

Rust имеет в своем репозитории управляющие структуры большинства языков программирования. Сюда входят циклические конструкции «for» и «while» и ветвление через «if» и «else». Rust также имеет некоторые специальные возможности. Конструкция «match» позволяет присваивать шаблоны, а оператор «loop» создает бесконечный цикл. Чтобы сделать последний практичным, используется оператор «break».

Циклы

Повторное выполнение блока кода с помощью циклов также известно как «итерация». Часто итерации выполняются через элементы контейнера. Как и Python, Rust знаком с понятием «итератор». Итератор абстрагирует последовательный доступ к элементам контейнера. Давайте рассмотрим пример:

// List with names
let names = ["Jim", "Jack", "John"];
// “for” loop with iterator in the list
for name in namen.iter() {
    println!("Hello, {}", name);
}

Итак, что если вы хотите написать цикл «for» в стиле C/C++ или Java? Для этого вам нужно указать начальное и конечное число, и в цикле перебирать все значения между ними. Для таких ситуаций в Rust, как и в Python, существует так называемый объект «range». Он, в свою очередь, создает итератор, над которым работает ключевое слово «for»:

// output the numbers 1 to 10
// “for” loop with “range” iterator
// attention: the range does not contain the end number
for number in 1..11 {
  println!("number: {}", number);
}
// alternative (including) range notation
for number in 1..=10 {
  println!("number: {}", number);
}

Цикл «while» работает в Rust так же, как и в других языках программирования. Задается условие, и тело цикла выполняется до тех пор, пока условие истинно:

// the numbers “1” to “10” are output via “while” loop
let mut number = 1;
while (number <= 10) {
  println!(number: {}, number);
  number += 1;
}

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

// endless loop with “while”
while true {
  // …
}
// endless loop with “while”
loop {
  // …
}

В обоих случаях для прерывания цикла можно использовать ключевое слово «break».

Ветвление

Ветвление с помощью «if» и «else» работает в Rust так же, как и в аналогичных языках программирования.

const limit: u8 = 42;
let number = 43;
if number < limit {
  println!("under the limit.");
}
else if number == limit {
  println!("right at the limit…");
}
else {
  println!("above the limit!");
}

Более интересным является ключевое слово «match» в Rust. Оно выполняет ту же функцию, что и оператор «switch» в других языках. Для примера посмотрите на функцию card_symbol() в разделе «Составные типы данных» (см. ниже).

Функции, процедуры и методы

В большинстве языков программирования функции являются основным строительным блоком модульного программирования. Функции определяются в Rust с помощью ключевого слова «fn». Строгого различия между смежными понятиями функции и процедуры не существует. Оба понятия определяются практически одинаково.

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

fn procedure() {
  println!("this procedure doesn’t return a value.");
}
// negate a number
// return time after the “->”-Operator
fn negates(integer: i8) -> i8 {
  return integer * -1;
}

Помимо функций и процедур, Rust также знает методы, известные из объектно-ориентированного программирования. Метод — это функция, связанная со структурой данных. Как и в Python, методы определяются в Rust с первым параметром «self». Метод вызывается по обычной схеме object.method(). Здесь приведен пример метода surface(), привязанного к структуре данных «struct»:

// ‚struct‘-Definition
struct rectangle {
    width: u32,
    height: u32,
}
// ‚struct‘-Implementation
impl rectangle {
    fn surface(&self) -> u32 {
        return self.width * self.height;
    }
}
let rectangle = rectangle {
    width: 30,
    height: 50,
};
println!("the surface of the rectangle equals {}.", rectangle.surface());

Типы данных и структуры данных

Rust — статически типизированный язык. В отличие от динамически типизированных языков Python, Ruby, PHP или JavaScript, Rust требует, чтобы тип каждой переменной был известен во время компиляции.

Элементарные типы данных

Как и большинство высших языков программирования, Rust знает некоторые элементарные типы данных (называемые «примитивами»). Экземпляры элементарных типов данных размещаются в стековом хранилище, которое отличается особой производительностью. Кроме того, значения элементарных типов данных могут быть определены с помощью «литерального» синтаксиса. Это означает, что значения могут быть легко записаны.

Тип данных Пояснение Аннотации типов
Целое число Целое число i8, u8 и т.д.
Плавающая точка Значение с плавающей точкой f64, f32
Булево Истинное значение bool
Символ Одиночная буква Юникода char
Строка Строка символов Юникода str

Хотя Rust является статически типизированным языком, тип значения не всегда должен быть объявлен явно. Во многих случаях тип может быть выведен компилятором из контекста («Type inference»). В качестве альтернативы тип явно указывается в аннотации типа. В некоторых случаях последнее является обязательным:

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

Приведем несколько наглядных примеров инстанцирования элементарных типов данных с помощью литерального синтаксиса:

// here, the compiler automatically recognizes the type of variable
let cents = 42;
// type annotation: positive number (‘u8’ = "unsigned, 8 bits")
let age: u8 = -15; // compiler error, since a negative value was provided
// floating point value
let angle = 38.5;
// equivalent to
let angle: f64 = 38.5;
// floating point value
let user_registered = true;
// equivalent to
let user_registered: bool = true;
// letter needs single doors
let letter = 'a';
// static string, needs double quotes
let name = "Walther";
// with explicit type
let name: &'static str = "Walther";
// alternatively as a dynamic “string” with “string::from()”
let name: string = string::from("Walther");

Комбинированные типы данных

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

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

Тип данных Объяснение Тип элементов Буквальный синтаксис
Массив Список из нескольких значений Одинаковый тип [a1, a2, a3]
Кортеж Набор из нескольких значений Любой тип (t1, t2)
Структура Группировка нескольких именованных значений Любой тип
Enum Листинг Любой тип

Давайте сначала рассмотрим структуру данных «struct». Здесь мы определяем человека с тремя именованными полями:

struct Person = {
  first name: String,
  surname: String,
  age: u8,
}

Чтобы представить конкретного человека, мы инстанцируем «struct»:

let player = Person {
  first name: String::from("Peter"),
  surname: String::from("Smith"),
  age: 42,
};
// access field of a “struct” instance
println!("Age of player: {}", player.age);

Перечисление» (сокращение от «перечисления») определяет возможные варианты свойства. Мы проиллюстрируем этот принцип ниже на примере четырех цветов игральных карт:

enum cardcolor {
  Cross,
  Spade,
  Heart,
  Diamond,
}
// the color of a playing card
let color = cardcolor::cross;

Rust также знает ключевое слово «match» для «сопоставления шаблонов». По функциональности оно сравнимо с оператором «switch» в других языках. Вот пример:

// determine the symbol belonging to a card color
fn card_symbol(color: cardcolor) -> &'static str {
  match color {
    cardcolor::cross => "♣︎",
    cardcolor::spade => "♠︎",
    cardcolor::heart => "♥︎",
    cardcolor::diamond => "♦︎",
  }
}
println!("Symbol: {}", card_symbol(cardcolor::cross)); // gives you the symbol ♣︎ 

Кортеж — это набор из нескольких значений, которые могут быть разных типов. Одиночные значения кортежа могут быть присвоены нескольким переменным путем деконструкции. Если одно из значений не нужно, то подчеркивание (_) используется как символ-заместитель — как это типично для Haskell, Python и JavaScript. Вот пример:

 

// define playing card as tuple 
let playing card: (cardcolor, u8) = (cardcolor::heart, 7);
// the values of a tuple are assigned to multiple variables
let (color, value) = playing card;
// if you only need the value
let (_, value) = playing card;

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

let name = ("Peter", "Smith");
let first name = name.0;
let surname = name.1;

Изучение высших конструкций программирования в Rust

Динамические структуры данных

Общим для уже представленных составных типов данных является то, что их экземпляры размещаются в стеке. Стандартная библиотека Rust также содержит ряд часто используемых динамических структур данных. Экземпляры этих структур данных размещаются в куче. Это означает, что размер экземпляров может быть изменен впоследствии. Ниже приведен краткий обзор часто используемых динамических структур данных:

Тип данных Объяснение
Вектор Динамический список из нескольких значений одного типа
Строка Динамическая последовательность букв Unicode
HashMap Динамическое присвоение пар ключ-значение

Вот пример динамически растущего вектора:

// declare vector with “mut” as changeable
let mut name = Vec::new();
// assign values to the vector
name.push("Jim");
name.push("Jack");
name.push("John");

Объектно-ориентированное программирование (ООП) в Rust

В отличие от таких языков, как C++ и Java, Rust не понимает концепции классов. Тем не менее, методология ООП может быть запрограммирована следующим образом. Ее основу составляют уже введенные типы данных. Особенно тип «struct» может быть использован для определения структуры объектов.

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

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

Метапрограммирование

Как и многие другие языки программирования, Rust позволяет писать код для метапрограммирования. Это код, который генерирует дальнейший код. В Rust это, с одной стороны, «макросы», которые вы можете знать из C/C++. Макросы заканчиваются восклицательным знаком (!); макрос «println!» для вывода текста в командную строку уже несколько раз упоминался в этой статье.

С другой стороны, Rust также знает «дженерики». Они позволяют писать код, абстрагированный от нескольких типов. Дженерики похожи на шаблоны в C++ или одноименные дженерики в Java. В Rust часто используется дженерик «Option<T>», который абстрагирует двойственность «None»/»Some(T)» для любого типа «T».

Резюме

Rust имеет потенциал заменить старые любимые C и C++ в качестве основного языка системного программирования.

Оцените статью
cdelat.ru
Добавить комментарий