Rust — компилируемый язык программирования общего назначения, спонсируемый Mozilla Research,
поддерживающий функциональное программирование, модель акторов и процедурное программирование.
Объектно-ориентированное программирование как таковое языком не поддерживается,
но язык позволяет реализовать большинство понятий ООП при помощи других абстракций, например, типажей.
Rust — это системный язык программирования, который предназначен для решения трёх основных задачах:
безопасность, скорость и параллелизм.
Он сопоставим по скорости и возможностям с C++, однако, даёт большую безопасность при работе с памятью,
что обеспечивается механизмами ограничения.
Более подробнее читайте на
википедии
Для того, чтобы установить раст, нужно сходить на официальный сайт и выбрать инсталлятор в виде архива по адресу
https://www.rust-lang.org/en-US/downloads.html
После распаковки архива раст нужно установить с помощью файла
install.sh
После чего раст готов к работе.
Hello World
Для начала напишем первую программу - Hello World. Создадим каталог с таким названием. В нем создадим подкаталог src.
В подкаталог положим файл
main.rs
следующего содержания
fn main() {
println!("Привет, мир!");
}
Зайдем в подкаталог src и запустим из командной строки две команды
$ rustc main.rs
$ ./main
Начало каждой программы на расте начинается с функции main. Каждое выражение в программе на расте заканчивается точкой с запятой.
Раст - компилятор, и перед выполнением любая программа должна быть вначале скомпилирована.
rustc подходит для небольших программ. В случае роста программ нам потребуется специальный инструмент, который называется
Cargo
Cargo — это система сборки и пакетный менеджер для Rust.
Cargo делает 3 вещи :
1 сборка кода
2 загрузка библиотек, от которых зависит ваш код,
3 сборка этих библиотек.
По умолчанию при инсталяции раста cargo устанавливается вместе с ним.
Теперь соберем наш проект Hello world с помощью cargo.
Внутри каталога Hello world создаим конфигурационный файл
Cargo.toml
следующего содержания
[package]
name = "hello_world"
version = "0.0.1"
authors = ["iakovlev.org" ]
Осталось запустить команду
Cargo run
Сборку можно найти в подкаталоге ./target.
Для сборки релиза можно запустить команду
Cargo build --release
Всю только что описанную процедуру по созданию проекта можно выполнить с помощью одной единственной
команды
cargo new hello_world --bin
При этом будет созданы каталоги и все необходимые файлы для генерация исполняемого файла.
Угадай число
Следующая программа будет более осмысленной - мы сделаем реализацию игры Угадай Число.
Для начала сгенерируем шаблон для проекта:
cargo new game --bin
Функция main будет иметь вид
use std::io;
fn main() {
println!("Угадайте число!");
println!("Пожалуйста, введите предположение.");
let mut guess = String::new();
io::stdin().read_line(&mut guess).expect("Не удалось прочитать строку");
println!("Ваша попытка: {}", guess);
}
Вначале мы импортируем стандартную библиотеку ввода io.
Выражение let используется для создания переменной.
Модификатор mut разрешает менять переменную.
String - строковый тип переменной utf8 произвольной длины.
Далее идет стандартный обработчик ввода с терминала в виде функции read_line, которой в качестве аргумента
передается ссылка на переменную.
Метод expect проверяет, правильный ли тип мы ввели, и в случае ошибки вызовет панику и останов программы.
Теперь можно запустить программу
cargo run
Теперь нам нужно расширить функционал этой программы. Добавим в нее генератор случайного числа.
Для этого в файле Cargo.toml добавим пару строк, в которых будет ссылка на стандартную библиотеку rand
Если вы запустили эту команду первый раз, вам может потребоваться время на ее выполнение.
При этом cargo скачает с интернета две библиотеки. Их версии прописываются в специальном файле
Cargo.lock
И каждый раз, когда мы будем собирать проект, будут использоваться те номера версий, которые прописаны
именно в Cargo.lock. Если же мы хотим использовать более новые версии, нужно использовать префикс
cargo update
Теперь используем rand в новой версии программы.
Окончательный вариант программы с циклической проверкой будет выглядеть так:
extern crate rand;
use std::io;
use std::cmp::Ordering;
use rand::Rng;
fn main() {
println!("Угадайте число!");
let secret_number = rand::thread_rng().gen_range(1, 101);
loop {
println!("Пожалуйста, введите предположение.");
let mut guess = String::new();
io::stdin().read_line(&mut guess)
.expect("Не удалось прочитать строку");
let guess: u32 = match guess.trim().parse() {
Ok(num) => num,
Err(_) => continue,
};
println!("Ваша попытка: {}", guess);
match guess.cmp(&secret_number) {
Ordering::Less => println!("Слишком маленькое!"),
Ordering::Greater => println!("Слишком большое!"),
Ordering::Equal => {
println!("Вы выиграли!");
break;
}
}
}
}
Здесь метод
guess.cmp
может использоваться для сравнения обьектов. Результатом сравнения будет обьект типа
Ordering
который фактически является перечислением - enum.
В качестве селектора мы используем оператор
match
Мы переопределяем тип переменной
guess
Обедающие философы
В следующей задаче мы будем использовать многопоточность.
В древние времена богатые филантропы пригласили погостить пятерых
философов.
Им выделили каждому по комнате, в которой они могли
заниматься своей профессиональной деятельностью — мышлением.
Также была общая столовая, где стоял большой круглый стол, а вокруг него пять стульев.
Каждый стул имел табличку с именем философа, который должен был сидеть на нем.
Слева от каждого философа лежала вилка, а в центре стола стояла большая миска
со спагетти, которая постоянно пополнялась.
Как подобает философам, они большую часть своего времени проводили в раздумьях.
Но однажды они почувствовали голод и отправились в столовую.
Каждый сел на свой стул, взял по вилке и воткнул её в миску со спагетти.
Но сущность запутанных спагетти такова, что необходима вторая вилка,
чтобы отправлять спагетти в рот. То есть философу требовалась ещё и вилка справа
от него.
Вилка может быть использована только одним философом одновременно.
Если другой философ захочет её взять, то ему придётся ждать когда она освободится.
Нужно придумать сценарий, при котором все философы cмогут поесть.
Для начала создадим новый проект командой
cargo new phil --bin
В файле main.rs создадим структуру для философов и напишем первую версию программы
Мы обьявляем структуру struct и делаем для нее реализацию impl, внутри реализации
с помощью статической функции new мы создаем новый эксземляр структуры Philosopher.
Функция new - это вариант для другой реализации, которая могла бы выглядеть так,
если бы мы не использовали new:
let p1 = Philosopher { name: "Джудит Батлер".to_string() };
что выглядит не совсем изящно.
Далее нужно написать метод для окончания приема пищи и реализовать цикл по приему пищи.
Вторая версия программы
Вектор является расширенной версией массива.
В цикле for мы перебираем вектор, получая ссылку на очередного философа на каждой итерации.
В расте методы ожидаемо получают обязательный параметр
self
Мы сразу видим , что new - это статический метод, а eat() - метод.
В следующей - третьей - версии программы философы будут есть одновременно
use std::thread;
use std::time::Duration;
struct Philosopher {
name: String,
}
impl Philosopher {
fn new(name: &str) -> Philosopher {
Philosopher {
name: name.to_string(),
}
}
fn eat(&self) {
println!("{} начала есть.", self.name);
thread::sleep(Duration::from_millis(1000));
println!("{} закончила есть.", self.name);
}
}
fn main() {
let philosophers = vec![
Philosopher::new("Джудит Батлер"),
Philosopher::new("Рая Дунаевская"),
Philosopher::new("Зарубина Наталья"),
Philosopher::new("Эмма Гольдман"),
Philosopher::new("Анна Шмидт"),
];
let handles: Vec<_> = philosophers.into_iter().map(|p| {
thread::spawn(move || {
p.eat();
})
}).collect();
for h in handles {
h.join().unwrap();
}
}
Прием пищи длится условно 1 секунду
thread::sleep(Duration::from_millis(1000));
Мы создаем вектор для того, чтобы создать несколько потоков, для каждого потока будет создан дескриптор.
let handles: Vec<_> =
Итерация по вектору выполняется с помощью стандартного метода
philosophers.into_iter()
Итератор передает элемент вектора в поток. У итератора вызывается метод
map
который принимает замыкание в качестве аргумента и вызывает это
замыкание для каждого из элементов итератора
thread::spawn(move || {
p.eat();
})
Функция
thread::spawn
берет в качестве аргумента замыкание и исполняет это замыкание в отдельном потоке.
При этом переменная, называемая
p
блокируется с помощью ключевого слова
move
При этом в потоке у переменной p вызывается метод eat()
Метод
}).collect();
создает коллекцию, состоящую из дескрипторов потоков. Далее мы блокируем функцию main до тех пор,
пока не выполним все потоки
for h in handles {
h.join().unwrap();
}
В последней - четвертой - версии программы - мы добавим вилки :-)
use std::sync::Mutex;
struct Table {
forks: Vec<Mutex <()>>,
}
Структура Table содержит вектор Мьютексов.
Мьютекс — способ управления доступом к данным для параллельно выполняющихся потоков:
только один поток может получить доступ к глобальным данным в конкретный момент времени.
Мы используем пустой кортеж () внутри мьютекса.
В структуру философов мы добавляем два поля - левую и правую вилку.
Мы получаем доступ к списку вилок через структуру Table, используем идентификаторы вилок в виде
self.left и self.right.
Мы получаем мьютекс, который регулирует доступ к вилке, и вызываем для него метод
lock
блокируя одновременный доступ к вилке. Метод
unwrap()
дает нам возможность не задумываться о проблемах, связанных с блокировками и падением программы.
Использование символа подчеркивания в переменных
_left и _right
делается для локальных переменных с ограниченной областью видимости.
В функции main() мы создаем экземпляр структуры Table и оборачиваем его в атомарный счетчик ссылок
(atomic reference count), который передается в новый поток, при этом счетчик увеличивается,
а по завершении работы потока счетчик уменьшается:
Arc<T>
Внутри цикла
map() / collect()
мы вызываем метод
table.clone()
который клонирует счетчик, увеличивает его на единицу, а потом инкрементирует.
В программе мы дважды используем переменную table для создания разных обьектов.
Создание библиотеки
Для этого сначала создадим новый проект, выполнив команду
cargo new embed
В файл lib.rs вставим следующий библиотечный код
use std::thread;
#[no_mangle]
pub extern fn process() {
let handles: Vec<_> = (0..10).map(|_| {
thread::spawn(|| {
let mut x = 0;
for _ in 0..5_000_000 {
x += 1
}
x
})
}).collect();
for h in handles {
println!("Поток завершился со счётом={}",
h.join().map_err(|_| "Не удалось соединиться с потоком!").unwrap());
}
}
В коде создаются 10 потоков, которые собираются в вектор handles. Внутри каждого потока выполняется
по 5 миллионов итераций в цикле. Для того, чтобы эту библиотеку можно было использовать,
нужно сделать следующее - добавить в начало кода новый атрибут и сделать функцию публичной для доступа
#[no_mangle]
pub extern fn process() {
Далее в файле Cargo.toml нужно нужно добавить строчки
[lib]
name = "embed"
crate-type = ["dylib"]
Собираем библиотеку
cargo build --release
Библиотеку можно найти в файле target/release/libembed.so.
Файл libembed.so является динамической библиотекой - shared object.
Теперь можно написать код, например, на питоне , и вызвать из питона эту библиотеку
from ctypes import cdll
lib = cdll.LoadLibrary("target/release/libembed.so")
lib.process()
print("сделано!")