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

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

На протяжении всего курса мы рассмотрим следующие темы:

  1. Введение в ржавчину
  2. Переменные и типы данных
  3. Вывод значений в консоль
  4. Поток управления
  5. Функции и модули
  6. Собственность и заимствование
  7. Структуры и перечисления
  8. Черты и дженерики
  9. Параллелизм и параллелизм
  10. Файловый ввод-вывод и обработка ошибок
  11. Внешние зависимости

Поскольку пост наполнен небольшими примерами, я не сделал код доступным на GitHub. Однако большинство фрагментов кода будут работать без каких-либо дополнительных изменений, просто скопировав и вставив их в файл main.rs (подробнее об этом позже).

Давайте начнем!

Введение в ржавчину

Этот раздел познакомит вас с Rust и его основными понятиями. Вы узнаете, как установить Rust, написать свою первую программу на Rust и использовать менеджер пакетов Rust.

Установка ржавчины

Прежде чем вы сможете начать программировать на Rust, вы должны установить компилятор Rust на свой компьютер. Для этого выполните следующие действия:

  1. Перейдите на сайт Rust по адресу https://www.rust-lang.org/.
  2. Нажмите на кнопку «Установить» в правом верхнем углу страницы.
  3. Отсюда, в зависимости от вашей ОС, я предполагаю, что вы работаете в Linux, так что это просто запуск данной инструкции в терминале:
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

Теперь вы можете выбрать любой вариант, который вы выберете. Для стандартной установки выберите «1) Продолжить установку (по умолчанию)».

После завершения процесса вам нужно будет сделать Rust видимым для ваших терминалов, т. е. открыть .bashrc в выбранном вами текстовом редакторе (например, gedit ~ /.bashrc») и внизу добавьте «source «$HOME/.cargo/env»». Сохраните изменения, откройте новый терминал, и все готово!

Написание вашей первой программы

Установив Rust, пришло время написать свою первую программу! Вот простое «Привет, мир!»:

fn main() {
    println!("Hello, world!");
}

Чтобы запустить эту программу, сохраните ее в файле с именем main.rs, а затем введите в терминале следующую команду:

$ rustc main.rs
$ ./main

Это скомпилирует и запустит ваш код, напечатав «Hello, world!» к терминалу.

Использование диспетчера пакетов

Менеджер пакетов Rust называется Cargo. Он используется для управления проектами, зависимостями и создания артефактов. Вот как создать новый проект Rust, используя Cargo:

$ cargo new rs_sandbox
$ cd myproject

Это создаст новый проект в каталоге с именем rs_sandbox. Чтобы собрать и запустить проект, перейдите в каталог (cd rs_sandbox) и введите следующие команды:

$ cargo build
$ cargo run

Переменные и типы данных

В этом разделе будут рассмотрены основные типы данных и переменные. Я покажу вам, как объявлять и инициализировать переменные и как использовать встроенные типы данных Rust.

Объявление переменных

В Rust переменные объявляются с использованием ключевого слова let. Тип переменной будет автоматически выведен из значения.

По умолчанию переменные являются неизменяемыми (const). Это означает, что как только вы присвоите значение переменной, вы не сможете его изменить. Чтобы сделать переменную изменяемой, вам нужно использовать ключевое слово mut. Вот пример:

fn main() {
    // Declare inmutable variable
    let a = 5;
    
    // Declare mutable variable
    let mut b = 6;

    // Update the value of the mutable variable
    b = 10;
}

Типы данных

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

  • Целые числа: в Rust есть несколько типов целых чисел, в том числе i8, i16, i32, i64 и i128, которые являются целыми числами со знаком, и u8, u16, u32, u64 и u128, которые являются целыми числами без знака. Вы можете применить тип при объявлении переменной.
  • Числа с плавающей запятой: в Rust есть два типа чисел с плавающей запятой: f32 и f64.
  • Булевы значения: в Rust есть логический тип, который называется bool. Он имеет два возможных значения: true и false.
  • Символы. В Rust есть тип символов, который называется char. Он представляет собой один символ Юникода.
  • Строки. В Rust есть строковый тип, который называется String. Это расширяемая строка, размещаемая в куче.
  • Массивы: в Rust они имеют фиксированный размер и могут содержать элементы только одного типа.
  • Кортежи: в Rust они могут содержать элементы разных типов и иметь фиксированную длину.
fn main() {
    // Integer
    let c: i64 = 5;
    
    // Floating point
    let d: f64 = 3.14159;
    
    // Boolean
    let e: bool = true;
    
    // Character
    let f: char = 'a';
    
    // String
    let g: String = String::from("hello");
    
    // Array
    let h: [i32; 5] = [1, 2, 3, 4, 5];
    
    // Tuples
    let i: (i32, f64, u8) = (500, 6.4, 1);
}

Вывод значений на консоль

Прежде чем двигаться дальше, я кратко расскажу о процессе печати на экране, так как это всегда полезный инструмент. Макрос println! — это широко используемый макрос в Rust, который позволяет вам печатать текст в консоли. Это похоже на функцию printf в C и C++ и функцию print в Python.

Макрос println! работает, беря строку формата и набор аргументов, которые затем интерполируются в строку формата. Строка формата использует заполнители, такие как {} или {:<width>}, чтобы указать, куда должны быть вставлены аргументы.

Вот пример, демонстрирующий использование:

fn main() {
    let x = 10;
    let y = 20;
    println!("The value of x is {} and the value of y is {}", x, y);
    // Output: The value of x is 10 and the value of y is 20
}

Его также можно использовать для печати форматированного текста с различным выравниванием, числовыми базами и т. д. Вот несколько примеров:

fn main() {
    let x = 42;
    println!("Binary: {:b}", x);
    println!("Octal: {:o}", x);
    println!("Hexadecimal: {:x}", x);
    println!("Padded with zeroes: {:08}", x);
    println!("Right-aligned: {:>8}", x);
    println!("Left-aligned: {:<8}", x);

    // Output 
    // Binary: 101010
    // Octal: 52
    // Hexadecimal: 2a
    // Padded with zeroes: 00000042
    // Right-aligned:       42
    // Left-aligned: 42
}

Если мы хотим напечатать вектор, мы можем использовать заполнитель {:?}, который говорит Rust использовать трейт Debug (подробнее об этом позже) для печати вектора. Это удобный способ вывода содержимого вектора или любого другого типа, реализующего трейт Debug.

fn main() {
    let vec = vec![1, 2, 3, 4, 5];
    println!("The vector is {:?}", vec);
    // Output: The vector is [1, 2, 3, 4, 5]
}

Поток управления

В этом разделе я расскажу о конструкциях потока управления, изучая, как использовать выражения if, loop, while, for и match. Как и прежде, мы пройдемся по выражениям и продемонстрируем их все в конце раздела.

  • Если выражения: в Rust if — это выражение, а не оператор. Это означает, что он оценивает значение, которое может быть присвоено переменной.
  • Цикл: используется для создания бесконечного цикла.
  • Пока: создает цикл, который продолжается до тех пор, пока выполняется условие.
  • For: используется для перебора набора значений.
  • Сопоставление: используется для сравнения значения с набором шаблонов и выполнения кода на основе совпадения.
fn main() {
    // Declare variables for demonstration
    let mut x = 5;
    let arr = [1, 2, 3, 4, 5];
    
    // If expression 
    let y = if x == 5 {
        10
    } else {
        15
    };

    // Loop expression
    loop {
        println!("Hello, world!");
        break;    // If you remove this line, the loop will run forever!
    }

    // While expression    
    while x < 10 {
        println!("{}", x);
        x += 1;
    }

    // For expression
    for x in arr.iter() {
        println!("{}", x);
    }

    // Match expression
    // will print "something else" as x != 1, 2, 3
    match x {
        1 => println!("one"),
        2 => println!("two"),
        3 => println!("three"),
        _ => println!("something else"),
    }
}

Функции и модули

В этом разделе я расскажу о функциях и модулях. Мы увидим, как объявлять и вызывать функции и как организовывать код в модули.

Функции объявляются с помощью ключевого слова fn. В скобках мы можем указать аргументы с их типом и ожидаемое возвращаемое значение после них. Вот пример:

fn add(x: i32, y: i32) -> i32 {
    x + y
}

fn main() {
    let result = add(5, 10);
    println!("Result = {}", result);
}

Это объявляет новую функцию с именем add, которая принимает два аргумента i32 и возвращает значение i32. Обратите внимание, что после «x + y нет точки с запятой. Это означает, что функция вернет результат, созданный этой строкой. В качестве альтернативы вы могли написать «return x + y;».

Функция вызывается с аргументами 5 и 10 и присваивает результат переменной result.

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

mod my_module {
    pub fn add(x: i32, y: i32) -> i32 {
        x + y
    }
}

fn main() {
    let result_mod = my_module::add(5, 10);
}

Это объявляет новый модуль с именем my_module, который содержит общедоступную функцию с именем add, которая вызывается из main с аргументами 5 и 10 и присваивает результат переменной result_mod.

Собственность и заимствование

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

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

let x = String::from("hello");

Это объявляет новую переменную с именем x типа String и присваивает ей значение "hello". x является владельцем значения String. Когда x выходит за рамки, значение String автоматически освобождается.

Вы можете передавать ссылки на значения в Rust вместо самих значений. Это называется заимствованием. Вот пример:

fn print_string(s: &String) {
    println!("{}", s);
}

fn main() {
    let x = String::from("hello");
    print_string(&x);
}

Это объявляет новую функцию с именем print_string, которая принимает ссылку на значение String в качестве аргумента, заданного синтаксисом &String. Затем он создает новое значение String с именем x и передает ссылку на него функции print_string, символ & используется для создания ссылки на значение.

Изменчивость и заимствование

Ссылки неизменяемы по умолчанию. Это означает, что вы не можете изменить значение ссылки. Чтобы сделать ссылку изменяемой, вам нужно использовать ключевое слово mut. Вот пример:

fn add_one(x: &mut i32) {
    *x += 1;
}

fn main() {
    let mut x = 5;
    add_one(&mut x);
    println!("x = {}", x);
}

Это объявляет новую функцию с именем add_one, которая принимает изменяемую ссылку на i32 value в качестве аргумента. Затем он создает новую переменную с именем x и присваивает ей значение 5. Наконец, он передает изменяемую ссылку x функции add_one, которая добавляет 1 к значению.

Символ * используется для разыменования ссылки и доступа к базовому значению.

Правила владения и заимствования

В Rust есть набор правил владения и заимствования, которые обеспечивают безопасность памяти и предотвращают распространенные ошибки программирования.

  1. Каждое значение имеет одного владельца.
  2. Когда владелец выходит из области видимости, значение освобождается.
  3. У вас может быть несколько неизменяемых ссылок на значение, но только одна изменяемая ссылка за раз.
  4. Вы не можете одновременно иметь как изменяемые, так и неизменяемые ссылки на одно и то же значение.

Следуя этим правилам, Rust гарантирует, что ваш код безопасен для памяти и свободен от распространенных ошибок программирования.

Структуры и перечисления

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

Структура. – это настраиваемый тип данных, который объединяет значения разных типов в единый блок. Вот пример:

struct Point {
    x: i32,
    y: i32,
}

fn main() {
    let p = Point { x: 5, y: 10 };
    println!("Point p = ({}, {})", p.x, p.y);
}

Это объявляет новую структуру Point, которая имеет два поля, x и y. Затем создается новое значение Point с именем p со значениями 5 и 10 для x и y соответственно. Чтобы получить доступ к значениям структуры, вы можете использовать запись через точку, как показано в операторе println!.

Enum: – это настраиваемый тип данных, позволяющий определить набор именованных значений. В приведенном ниже примере мы объявляем новое перечисление Color с тремя именованными значениями: Red, Green и Blue.

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

enum Color {
    Red,
    Green,
    Blue,
}

fn print_color(c: Color) {
    match c {
        Color::Red => println!("red"),
        Color::Green => println!("green"),
        Color::Blue => println!("blue"),
    }
}

fn main() {
    let c = Color::Red;
    print_color(c);
}

Черты и дженерики

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

Признаки: это набор методов, определяющих набор поведения для типа данных, что-то вроде наследования в C++. Вот пример:

trait Animal {
    fn speak(&self);
}

struct Dog;

impl Animal for Dog {
    fn speak(&self) {
        println!("Woof!");
    }
}

struct Cat;

impl Animal for Cat {
    fn speak(&self) {
        println!("Meow!");
    }
}

fn main() {
    let dog = Dog;
    let cat = Cat;
    
    dog.speak();
    cat.speak();
}

Это объявляет новую черту Animal, которая определяет метод speak. Затем он определяет две структуры, Dog и Cat, и реализует трейт Animal для каждой из них. Наконец, он создает экземпляры структур Dog и Cat и вызывает для них метод speak, который печатает "Woof!" и "Meow!" соответственно.

Общие: вы можете писать универсальный код для различных типов. Это позволяет писать более гибкий и повторно используемый код. Вот пример:

fn add<T: std::ops::Add<Output = T>>(x: T, y: T) -> T {
    x + y
}

fn main() {
    let x = add(5, 10);
    let y = add(1.1879, 7.0987);

    println!("x = {}", x);
    println!("y = {}", y)
}

Это объявляет новую функцию add, которая принимает два аргумента типа T, где T — параметр универсального типа. Признак std::ops::Add используется, чтобы гарантировать, что оператор + определен для типа T.

Синтаксис Output = T указывает, что оператор + должен возвращать значение типа T.

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

Параллелизм и параллелизм

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

Обсуждения: вы можете создавать обсуждения с помощью модуля std::thread. Вот пример:

use std::thread;

fn main() {
    let handle = thread::spawn(|| {
        println!("Hello from a thread!");
    });

    handle.join().unwrap();
}

Это объявляет новую переменную handle, которой присваивается результат вызова функции thread::spawn с замыканием, выводящим сообщение на консоль. Затем для дескриптора вызывается метод join, чтобы дождаться завершения потока перед выходом из основного потока.

Каналы:используютсядля связи между потоками. Вот пример:

use std::thread;
use std::sync::mpsc;

fn main() {
    let (tx, rx) = mpsc::channel();

    thread::spawn(move || {
        let val = String::from("Hello from a thread!");
        tx.send(val).unwrap();
    });

    let received = rx.recv().unwrap();
    println!("{}", received);
}

Это объявляет новый канал с помощью функции mpsc::channel. Затем он создает новый поток, который отправляет сообщение по каналу, а основной поток ожидает получения сообщения с помощью метода rx.recv().

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

use rayon::prelude::*;

fn main() {
    let arr = vec![1, 2, 3, 4, 5];

    let sum: i32 = arr.par_iter().sum();

    println!("{}", sum);
}

Это объявляет новый вектор и использует метод par_iter из ящика rayon для создания параллельного итератора над вектором. Затем в итераторе вызывается метод sum для параллельного вычисления суммы значений в векторе.

Внешние пакеты

Если бы вы запустили указанную выше ячейку без каких-либо дальнейших изменений, она бы не удалась, потому что для использования крейта rayon вам нужно добавить его в раздел [dependencies] файла Cargo.toml в папке вашего проекта, чтобы он выглядел следующим образом. :

[package]
name = "rs_sandbox"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
rayon = "1.6.1"

При этом крейт (библиотека, зависимость) загружается с Crates.io и добавляется в ваш проект.

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

Файловый ввод-вывод и обработка ошибок

В этом разделе рассматриваются функции файлового ввода-вывода и обработки ошибок.

Чтение файлов. Вы можете использовать модуль std::fs для чтения и записи файлов. Вот пример:

use std::fs::File;
use std::io::prelude::*;

fn main() -> std::io::Result<()> {
    let mut file = File::open("example.txt")?;
    let mut contents = String::new();
    file.read_to_string(&mut contents)?;

    println!("{}", contents);

    Ok(())
}

Это объявляет новую переменную File и использует метод File::open для открытия файла с именем example.txt. Оператор ? используется для распространения любых возникающих ошибок. Затем для файла вызывается метод read_to_string для чтения его содержимого в строковую переменную.

Rust будет искать example.txt в каталоге вашего проекта (где находится файл Cargo.toml).

Запись файлов: также используйте модуль std::fs. Вот пример:

use std::fs::File;
use std::io::prelude::*;

fn main() -> std::io::Result<()> {
    let mut file = File::create("example.txt")?;
    file.write_all(b"Hello, world!")?;

    Ok(())
}

Это объявляет новую переменную File и использует метод File::create для создания нового файла с именем example.txt. Оператор ? используется для распространения любых возникающих ошибок. Затем для файла вызывается метод write_all для записи строки "Hello, world!" в файл.

Обработка ошибок

В Rust ошибки обрабатываются с использованием типа Result. Вот пример:

use std::fs::File;

fn main() {
    let result = File::open("example.txt");

    match result {
        Ok(file) => println!("File opened successfully."),
        Err(error) => println!("Error opening file: {}", error),
    }
}

Это объявляет новую переменную result и использует метод File::open для открытия файла с именем example.txt.

Затем выражение match используется для обработки вариантов Ok и Err типа Result.

  • Если файл успешно открыт, будет выполнена ветвь Ok выражения match, и на консоль будет выведено сообщение.
  • В случае возникновения ошибки будет выполнена ветвь Err выражения match, и сообщение об ошибке будет напечатано на консоли.

Заключение

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

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

В последующих постах я рассмотрю еще три различных варианта использования Rust:

  • Как создавать простые веб-приложения на Rust с помощью пакета Rocket.
  • Как собрать воркер CloudFlare на Rust.
  • Создание контракта Blockchain с Rust.