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

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

1. Введение в экосистему Rust: установка, Cargo и первая программа

Введение в экосистему Rust: установка, Cargo и первая программа

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

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

Почему Rust?

Прежде чем открывать терминал, давайте ответим на вопрос: зачем изучать Rust? Долгое время в системном программировании существовала дилемма. Вы могли выбрать скорость и контроль над «железом» (C, C++), но при этом рисковали допустить критические ошибки работы с памятью. Либо вы могли выбрать безопасность и удобство (Python, Java, Go), но жертвовали производительностью из-за работы сборщика мусора (Garbage Collector).

Rust решает эту проблему. Он предлагает:

  • Безопасность памяти без сборщика мусора. Ошибки вроде обращения к освобожденной памяти исключены на этапе компиляции.
  • Высокую производительность. Скорость программ на Rust сопоставима с C++.
  • Современные инструменты. Встроенный менеджер пакетов, отличная документация и дружелюбный компилятор.
  • !Иллюстрация баланса между безопасностью и скоростью, который обеспечивает Rust

    Установка инструментов: Rustup

    Первый шаг — установка набора инструментов (toolchain). В мире Rust для этого используется специальная утилита под названием rustup. Это инсталлятор и менеджер версий языка.

    Установка на Linux и macOS

    Если вы используете Unix-подобную систему, откройте терминал и введите следующую команду:

    Скрипт скачает необходимые файлы и начнет установку. Вам будет предложено выбрать параметры установки — в большинстве случаев достаточно нажать Enter для выбора варианта по умолчанию (default).

    Установка на Windows

    Для пользователей Windows процесс также прост:

  • Перейдите на официальную страницу установки: Установка Rust.
  • Скачайте файл rustup-init.exe.
  • Запустите его и следуйте инструкциям в консоли.
  • > Важно: Для работы Rust на Windows вам также понадобятся инструменты сборки C++ (Visual Studio C++ Build Tools). Если они не установлены, rustup предупредит вас об этом и предложит установить их автоматически.

    Проверка установки

    После завершения установки вам, возможно, потребуется перезапустить терминал, чтобы обновились переменные среды (PATH). Чтобы убедиться, что все прошло успешно, введите:

    Вы должны увидеть что-то вроде rustc 1.75.0 (82e1608df 2023-12-21). Это означает, что компилятор Rust (rustc) установлен и готов к работе.

    Первая программа: Hello, World!

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

    Ручной метод

  • Создайте папку для своих проектов и внутри нее создайте файл с именем main.rs. Расширение .rs — стандарт для файлов исходного кода Rust.
  • Откройте файл в любом текстовом редакторе и напишите следующий код:
  • Сохраните файл и откройте терминал в этой папке.
  • Скомпилируйте программу командой:
  • Если вы на Linux или macOS, запустите полученный файл:
  • Если вы на Windows:

    Вы увидите вывод: Hello, world!.

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

    Знакомство с Cargo

    Cargo — это система сборки и менеджер пакетов Rust. Он делает множество полезных вещей:

    * Создает структуру нового проекта. Скачивает и собирает библиотеки (в Rust они называются крейтами или crates*), от которых зависит ваш код. * Запускает тесты. * Собирает документацию.

    !Cargo управляет всеми аспектами жизненного цикла проекта

    Создание проекта через Cargo

    Давайте пересоздадим наш проект, используя Cargo. Удалите предыдущие файлы и введите в терминале:

    Эта команда создаст новую директорию hello_cargo. Давайте заглянем внутрь:

    Cargo создал для нас структуру файлов. Рассмотрим её подробнее:

  • src/: В этой папке хранится исходный код. Cargo ожидает, что ваш код будет жить именно здесь.
  • Cargo.toml: Это манифест проекта. Он содержит метаданные (имя, версия, авторы) и список зависимостей. Формат TOML (Tom's Obvious, Minimal Language) очень прост для чтения.
  • Сборка и запуск

    Теперь перейдите в папку проекта:

    Чтобы собрать и запустить программу одной командой, используйте:

    Вы увидите примерно такой вывод:

    Cargo скомпилировал код, положил исполняемый файл в папку target/debug/ и сразу запустил его.

    Полезные команды Cargo

    * cargo build: Только компилирует проект, но не запускает его. * cargo check: Проверяет код на наличие ошибок, но не создает исполняемый файл. Это работает намного быстрее, чем build, и очень полезно, когда вы пишете код и хотите быстро проверить, нет ли синтаксических ошибок. * cargo build --release: Собирает проект для релиза (продакшена). Компиляция займет больше времени, но программа будет работать значительно быстрее благодаря оптимизациям.

    Анатомия программы на Rust

    Давайте вернемся к коду в файле src/main.rs и разберем его по частям.

    Функция main

    Строка fn main() { объявляет функцию.

    * fn: Ключевое слово для объявления функции. * main: Имя функции. В Rust функция main особенная — это точка входа в программу. Именно с неё начинается выполнение кода. * (): Скобки указывают на параметры функции. В данном случае параметров нет. * { ... }: Фигурные скобки обозначают тело функции — блок кода, который будет выполнен.

    Макрос println!

    Строка println!("Hello, world!"); выполняет вывод текста.

    Обратите внимание на восклицательный знак ! после println. Это очень важная деталь.

    * Если вы видите имя с !, значит, вы вызываете макрос, а не обычную функцию. * Макросы в Rust — это мощный инструмент метапрограммирования, который генерирует код во время компиляции. * В данном случае println! (print line) берет строку и печатает её в консоль, добавляя символ перевода строки в конце.

    Точка с запятой

    Строка заканчивается символом ;. В Rust, как и во многих C-подобных языках, точка с запятой указывает на завершение инструкции (statement). Это говорит компилятору: «Эта команда закончена, переходи к следующей».

    Форматирование кода

    Rust имеет строгие, но полезные стандарты форматирования. Официальный стиль предполагает использование 4 пробелов для отступа (не табуляции).

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

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

    Заключение

    Сегодня мы сделали первый и самый важный шаг. Мы установили Rust с помощью rustup, узнали, что rustc — это компилятор, а cargo — наш верный помощник для управления проектами. Мы также разобрали структуру простейшей программы и узнали отличие макроса от функции.

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

    2. Основы синтаксиса: переменные, типы данных и функции

    Основы синтаксиса: переменные, типы данных и функции

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

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

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

    В большинстве языков программирования переменная — это просто именованная ячейка памяти, значение которой можно менять. В Rust все немного иначе. По умолчанию переменные неизменяемы (immutable). Это один из способов, которым Rust подталкивает вас к написанию безопасного и параллельного кода.

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

    Для создания переменной используется ключевое слово let. Рассмотрим пример:

    Если вы раскомментируете строку x = 6; и попытаетесь скомпилировать код, Rust выдаст ошибку: cannot assign twice to immutable variable x. Компилятор гарантирует, что если вы объявили переменную неизменяемой, она действительно не изменится.

    Изменяемые переменные

    Если вам нужно изменить значение переменной, вы должны явно сообщить об этом, добавив ключевое слово mut (от англ. mutable) перед именем переменной:

    Теперь код скомпилируется и выполнится без ошибок.

    !Визуализация различия между неизменяемыми и изменяемыми переменными

    Константы

    В Rust также есть константы, которые объявляются ключевым словом const. В отличие от переменных:

    * Вы обязаны явно указать тип данных. * Константы всегда неизменяемы (ключевое слово mut использовать нельзя). * Они могут быть объявлены в любой области видимости, в том числе глобальной. * Их значение должно быть известно на этапе компиляции.

    Обратите внимание на использование нижнего подчеркивания _ в числе. Rust позволяет использовать его для улучшения читаемости больших чисел (например, 1_000_000 вместо 1000000).

    Затенение (Shadowing)

    Rust позволяет объявлять новую переменную с тем же именем, что и предыдущая. Это называется затенением. Первая переменная «затеняется» второй.

    В этом примере программа выведет 12. Затенение отличается от использования mut:

  • Мы используем ключевое слово let, фактически создавая новую переменную.
  • Мы можем изменить тип значения, сохранив имя.
  • Пример смены типа:

    С mut такое невозможно, так как тип переменной фиксируется при создании.

    Типы данных

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

    Скалярные типы

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

    #### Целые числа

    Целое число — это число без дробной части. В Rust есть знаковые (signed, i) и беззнаковые (unsigned, u) типы разного размера.

    | Размер | Знаковый | Беззнаковый | | :--- | :--- | :--- | | 8-bit | i8 | u8 | | 16-bit | i16 | u16 | | 32-bit | i32 | u32 | | 64-bit | i64 | u64 | | 128-bit | i128 | u128 | | arch | isize | usize |

    Типы isize и usize зависят от архитектуры компьютера: 64 бита на 64-битных системах и 32 бита на 32-битных.

    Диапазон значений для знаковых чисел рассчитывается по формуле:

    Где — это количество бит, используемых для хранения числа. Например, для i8 () диапазон будет от до .

    Для беззнаковых чисел формула проще:

    Где — количество бит. Для u8 это от до .

    > Примечание: По умолчанию Rust использует i32, так как этот тип, как правило, самый быстрый, даже на 64-битных системах.

    #### Числа с плавающей точкой

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

    #### Логический тип

    Как и в большинстве других языков, логический тип (Boolean) в Rust имеет два возможных значения: true и false. Тип обозначается как bool.

    #### Символьный тип

    Тип char в Rust — это не просто 1 байт (как в C++). Это 4 байта, представляющие скалярное значение Unicode. Это значит, что char может хранить не только ASCII, но и кириллицу, иероглифы и даже эмодзи.

    Обратите внимание, что литералы char пишутся в одинарных кавычках, в то время как строковые литералы — в двойных.

    Составные типы

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

    #### Кортежи (Tuples)

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

    Чтобы получить значения из кортежа, можно использовать деструктуризацию или доступ через точку:

    #### Массивы (Arrays)

    В отличие от кортежа, каждый элемент массива должен иметь один и тот же тип. Массивы в Rust имеют фиксированную длину.

    Массивы полезны, когда вы хотите, чтобы данные размещались в стеке (stack), а не в куче (heap), или когда вы хотите гарантировать наличие фиксированного количества элементов. Доступ к элементам осуществляется по индексу:

    !Сравнение структуры памяти кортежа и массива

    Функции

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

    Стиль именования функций и переменных в Rust — это snake_case (все буквы строчные, слова разделяются подчеркиванием).

    Параметры функций

    В объявлении функции вы можете определить параметры. В Rust вы обязаны указывать тип каждого параметра.

    Инструкции и выражения

    Тело функции состоит из ряда инструкций (statements), необязательно заканчивающихся выражением (expression). Это важное различие в Rust.

    * Инструкции — это действия, которые выполняют что-то, но не возвращают значения (например, let y = 6;). * Выражения вычисляют и возвращают значение (например, 5 + 6).

    Важно: Выражения не заканчиваются точкой с запятой. Если вы добавите точку с запятой в конец выражения, вы превратите его в инструкцию, и оно не вернет значение.

    Функции с возвращаемыми значениями

    Функции могут возвращать значения коду, который их вызвал. Мы не именуем возвращаемые значения, но должны объявить их тип после стрелки ->.

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

    Обратите внимание на функцию five. Там нет return, и нет точки с запятой после 5. Это выражение, и его значение возвращается.

    Если бы мы написали 5;, то получили бы ошибку компиляции, так как функция должна вернуть i32, а инструкция с точкой с запятой возвращает пустой кортеж () (unit type).

    Заключение

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

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

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

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

    В предыдущих статьях мы научились устанавливать Rust, использовать Cargo и разобрались с базовым синтаксисом: переменными, функциями и примитивными типами данных. Теперь пришло время обсудить самую главную особенность Rust. Это то, что делает его уникальным среди всех популярных языков программирования — система владения (ownership).

    Владение — это набор правил, которые компилятор проверяет на этапе сборки программы. Никакой сборщик мусора (как в Java или Python) не замедляет работу вашей программы во время выполнения, и вам не нужно вручную выделять и освобождать память (как в C или C++). Если вы нарушите правила владения, программа просто не скомпилируется.

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

    Стек и Куча (Stack and Heap)

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

    Стек (Stack)

    Стек работает по принципу «последним пришел — первым ушел» (LIFO). Представьте стопку тарелок: вы кладете новую тарелку сверху и берете тоже сверху. Все данные в стеке должны иметь известный, фиксированный размер. Это делает работу со стеком невероятно быстрой: процессору не нужно искать свободное место, он всегда работает с «верхушкой».

    Куча (Heap)

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

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

    !Визуализация различий между организованным Стеком и хаотичной Кучей с указателями.

    Правила владения

    В Rust есть три фундаментальных правила владения. Запомните их, они будут сопровождать вас везде:

  • У каждого значения в Rust есть переменная, которая называется его владельцем (owner).
  • У значения может быть только один владелец в каждый момент времени.
  • Когда владелец выходит из области видимости (scope), значение удаляется (dropped).
  • Давайте разберем эти правила на примерах.

    Область видимости переменной

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

    Здесь все просто. Переменная s ссылается на строковый литерал, который жестко зашит в код программы. Но что, если нам нужно хранить текст, который вводит пользователь? Его размер неизвестен заранее, поэтому мы не можем хранить его в стеке или в коде. Нам нужна куча.

    Для этого в Rust есть тип String. Он выделяет память в куче.

    Тип String можно изменять:

    Почему String можно изменять, а литералы — нет? Разница в том, как они работают с памятью. Литерал быстрый и эффективный, но неизменяемый. String гибкий, но требует выделения памяти в куче.

    Когда переменная s типа String выходит из области видимости, Rust автоматически вызывает специальную функцию drop, которая возвращает память операционной системе. В C++ это называется RAII (Resource Acquisition Is Initialization).

    Перемещение (Move) данных

    Рассмотрим интересный момент взаимодействия переменных.

    Здесь все очевидно: мы берем значение 5, привязываем к x, затем создаем копию этого значения и привязываем к y. Поскольку целые числа имеют фиксированный размер, они хранятся в стеке, и копирование происходит мгновенно.

    А теперь посмотрим на String:

    Кажется, что s2 станет копией s1. Но это не так.

    Строка String состоит из трех частей, хранящихся в стеке:

  • Указатель на память в куче.
  • Длина (сколько байт занимает строка).
  • Вместимость (сколько памяти выделено всего).
  • Сами данные («hello») лежат в куче.

    Когда мы пишем let s2 = s1;, Rust копирует только данные стека (указатель, длину, вместимость). Он не копирует данные в куче.

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

    Но тут возникает проблема. Если s1 и s2 указывают на одну и ту же память, то когда они обе выйдут из области видимости, они обе попытаются освободить одну и ту же память. Это известная ошибка double free (двойное освобождение), которая может привести к повреждению памяти и уязвимостям.

    Чтобы гарантировать безопасность, Rust делает финт ушами: после строки let s2 = s1;, он считает переменную s1 недействительной.

    Это называется перемещением (move). Мы не скопировали s1 в s2, мы переместили владение данными от s1 к s2.

    !Иллюстрация того, как при присваивании s2 = s1 указатель копируется, а s1 становится невалидной.

    Клонирование (Clone)

    Если мы действительно хотим сделать глубокую копию данных из кучи, мы должны использовать метод clone:

    Теперь работают обе переменные, но это стоит дороже по ресурсам.

    Копирование (Copy)

    Почему же код с целыми числами let y = x работал без clone? Потому что типы, размер которых известен во время компиляции (как i32), хранятся целиком в стеке. Их копирование настолько дешево, что нет смысла запрещать доступ к старой переменной.

    Rust использует специальный трейт (интерфейс) Copy для таких типов. Если тип реализует Copy, переменная не перемещается, а копируется. К таким типам относятся: * Все целые числа (u32, i32 и т.д.). * Логический тип (bool). * Числа с плавающей точкой (f64). * Символы (char). * Кортежи, если они содержат только типы Copy (например, (i32, i32)Copy, а (i32, String) — нет).

    Владение и функции

    Передача значения в функцию работает так же, как присваивание переменной. Владение будет передано (move) или скопировано (copy).

    Если бы мы хотели использовать s после вызова функции, нам пришлось бы возвращать её обратно:

    Но это неудобно. Что, если мы хотим просто прочитать данные, не забирая владение? Здесь на сцену выходят ссылки.

    Ссылки и заимствование (Borrowing)

    Заимствование — это создание ссылки на значение без получения владения им. Мы используем символ амперсанда &.

    В функции calculate_length переменная s имеет тип &String. Она ссылается на s1, но не владеет ею. Поэтому, когда s выходит из области видимости, память не освобождается.

    Изменяемые ссылки

    По умолчанию ссылки неизменяемы. Мы не можем изменить то, что позаимствовали. Чтобы изменить значение по ссылке, нужно использовать &mut.

    Однако у изменяемых ссылок есть одно критически важное ограничение:

    > В одной области видимости у вас может быть только одна изменяемая ссылка на конкретные данные.

    Этот код не скомпилируется:

    Это ограничение предотвращает гонки данных (data races). Гонка данных происходит, когда:

  • Два или более указателя обращаются к одним и тем же данным одновременно.
  • По крайней мере один из них используется для записи.
  • Нет механизма синхронизации доступа.
  • Rust предотвращает это на этапе компиляции!

    Также существует правило сочетания изменяемых и неизменяемых ссылок:

    > Вы можете иметь сколько угодно неизменяемых ссылок (&T), ИЛИ ровно одну изменяемую ссылку (&mut T), но не то и другое одновременно.

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

    !Визуальная метафора правил доступа: много читателей ИЛИ один писатель.

    Висячие ссылки (Dangling References)

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

    Rust выдаст ошибку: this function's return type contains a borrowed value, but there is no value for it to be borrowed from. Компилятор спас нас от потенциального падения программы.

    Срезы (Slices)

    Еще один вид ссылок, не владеющих данными — это срезы. Срез позволяет ссылаться на непрерывную последовательность элементов в коллекции, а не на всю коллекцию.

    Срезы имеют тип &str (строковый срез). Именно поэтому строковые литералы имеют тип &str — они являются срезами, указывающими на конкретное место в бинарном файле программы.

    Заключение

    Система владения — это то, что делает Rust надежным.

  • Владение гарантирует очистку памяти.
  • Заимствование позволяет использовать данные без передачи прав владения.
  • Правила ссылок предотвращают гонки данных и ошибки памяти.
  • Поначалу борьба с «borrow checker» (частью компилятора, проверяющей ссылки) может раздражать. Но помните: каждая ошибка, которую он вам показывает — это потенциальный баг, который вы только что избежали, не потратив ночи на отладку.

    В следующей статье мы применим эти знания для создания более сложных структур данных, изучив структуры (structs) и перечисления (enums).

    4. Структурирование данных: структуры, перечисления и сопоставление с образцом

    Структурирование данных: структуры, перечисления и сопоставление с образцом

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

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

    Структуры (Structs)

    Структура — это пользовательский тип данных, который позволяет упаковать несколько связанных значений в единое целое. Если вы пришли из объектно-ориентированных языков, структуры напомнят вам объекты, но без наследования.

    В Rust существует три вида структур:

  • Классические структуры с именованными полями.
  • Кортежные структуры с неименованными полями.
  • Единичные структуры (Unit-like structs) без полей.
  • Определение и создание экземпляров

    Давайте определим структуру User, которая будет хранить информацию о пользователе:

    Чтобы использовать эту структуру, мы создаем её экземпляр:

    Обратите внимание на несколько важных моментов:

    * Порядок полей при создании экземпляра не важен. * Если вы хотите изменять поля структуры после создания, весь экземпляр должен быть объявлен как изменяемый (mut). Rust не позволяет помечать отдельные поля как mut.

    !Структура работает как чертеж, по которому создаются конкретные объекты в памяти.

    Сокращенная инициализация

    Часто имена переменных совпадают с именами полей. Rust предлагает удобный синтаксис для таких случаев:

    Кортежные структуры

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

    Важно понимать, что black и origin — это разные типы, хотя они и выглядят одинаково. Вы не сможете передать Color в функцию, ожидающую Point. Это обеспечивает типобезопасность.

    Методы структур

    Структуры позволяют хранить данные, но как насчет поведения? В Rust методы определяются внутри блока impl (implementation).

    Давайте создадим структуру Rectangle и добавим метод для вычисления площади.

    Параметр &self

    Методы всегда принимают self в качестве первого параметра. Это аналог this в других языках.

    * &self: Метод заимствует экземпляр неизменяемо (читает данные). * &mut self: Метод заимствует экземпляр изменяемо (может менять поля). * self: Метод захватывает владение экземпляром (редкий случай, обычно используется для трансформации объекта в другой тип).

    Ассоциированные функции

    Вы также можете определять функции внутри impl, которые не принимают self. Они часто используются как конструкторы. Например, метод String::from — это ассоциированная функция.

    Перечисления (Enums)

    Если структуры позволяют группировать данные (отношение И: у пользователя есть имя И email), то перечисления позволяют выбирать один вариант из нескольких (отношение ИЛИ: IP-адрес может быть версии 4 ИЛИ версии 6).

    Хранение данных в перечислениях

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

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

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

    Перечисление Option

    В Rust нет значения null. Отсутствие null исключает целый класс ошибок, связанных с попыткой использования пустого значения. Однако концепция отсутствия значения все же полезна.

    Rust реализует её через стандартное перечисление Option<T>:

    * Some(T) означает, что значение есть, и оно типа T. * None означает, что значения нет.

    Поскольку Option<T> и T — это разные типы, компилятор не позволит вам использовать Option<T> так, будто это определенно валидное значение. Вы обязаны проверить наличие значения перед использованием.

    > «Я называю это своей ошибкой на миллиард долларов. Это было изобретение нулевой ссылки (null reference) в 1965 году». — Тони Хоар

    Сопоставление с образцом: match

    Для работы с перечислениями Rust предоставляет конструкцию match. Это похоже на switch в C++ или Java, но гораздо мощнее.

    Представьте монетный сортировщик. Монета падает, и в зависимости от размера попадает в нужное отверстие.

    Извлечение значений

    match может извлекать данные из вариантов перечисления.

    Исчерпывающее сопоставление

    Главное правило match: вы должны обработать все возможные варианты. Если вы забудете обработать None в Option<T>, код не скомпилируется. Это гарантирует, что вы никогда не упустите граничный случай.

    Если вариантов слишком много, можно использовать шаблон _ (подчеркивание), который означает «все остальные случаи»:

    Конструкция if let

    Иногда match бывает слишком громоздким, если нас интересует только один вариант. Для этого существует синтаксический сахар if let.

    Вместо этого:

    Мы можем написать так:

    if let читается как: «Если config_max соответствует шаблону Some(max), то выполни блок кода».

    Заключение

    Сегодня мы сделали огромный шаг вперед. Структуры позволяют нам создавать объекты, перечисления — моделировать вариативность данных, а match дает полный контроль над логикой программы, гарантируя обработку всех сценариев.

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

    5. Обработка ошибок, коллекции и использование типажей

    Обработка ошибок, коллекции и использование типажей

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

    В этой статье мы рассмотрим три критически важные темы:

  • Коллекции: как хранить списки данных и словаря.
  • Обработка ошибок: как Rust помогает превратить сбои в управляемые ситуации.
  • Типажи (Traits): как определять общее поведение для разных типов данных.
  • Эти инструменты превратят ваш код из набора инструкций в гибкую и устойчивую систему.

    Коллекции: Векторы и Хеш-карты

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

    Векторы (Vec<T>)

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

    !Структура вектора: метаданные в стеке указывают на данные в куче

    Создать вектор можно с помощью функции Vec::new или удобного макроса vec!:

    Обратите внимание: чтобы добавлять элементы, вектор должен быть изменяемым (mut).

    #### Доступ к элементам

    Получить данные из вектора можно двумя способами: через индексацию и через метод get.

    В чем разница?

    * Если вы обратитесь к несуществующему индексу через [] (например, &v[100]), программа запаникует и аварийно завершится. * Метод get вернет None, не убивая программу. Это безопасный способ работы с индексами.

    Хеш-карты (HashMap<K, V>)

    Если вектор хранит данные по индексам (0, 1, 2...), то хеш-карта хранит их по ключам. Это аналог словарей в Python или объектов в JavaScript.

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

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

    Rust подходит к ошибкам уникальным образом. Здесь нет исключений (exceptions), как в Java или C++. Вместо этого Rust делит ошибки на две категории:

  • Неисправимые ошибки: Программа попала в состояние, из которого нельзя выйти (например, попытка доступа к памяти за пределами массива).
  • Исправимые ошибки: Что-то пошло не так, но мы можем это обработать (например, файл не найден).
  • Неисправимые ошибки и panic!

    Когда происходит что-то ужасное, Rust вызывает макрос panic!. Он печатает сообщение об ошибке, очищает стек и завершает программу.

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

    Исправимые ошибки и Result

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

    Определение в стандартной библиотеке выглядит так:

    Рассмотрим пример открытия файла:

    Этот код пытается открыть файл. Если файл есть, мы получаем дескриптор файла. Если нет — мы паникуем (хотя могли бы, например, создать новый файл).

    Оператор ?

    Писать match для каждой ошибки утомительно. Rust предлагает элегантное решение — оператор ?. Он ставится после выражения, возвращающего Result.

    * Если значение Ok, то значение внутри распаковывается и код продолжается. * Если значение Err, то ошибка возвращается из текущей функции немедленно (как return Err(...)).

    Этот код делает то же самое, что и куча блоков match, но читается в одну строку.

    Типажи (Traits): Определение общего поведения

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

    Определение типажа

    Представьте, у нас есть разные типы текстов: NewsArticle (новостная статья) и Tweet (твит). Мы хотим, чтобы у них обоих был метод для получения краткой сводки.

    Определим типаж Summary:

    Реализация типажа

    Теперь реализуем этот типаж для наших типов:

    Теперь мы можем вызывать метод summarize на экземплярах Tweet, как если бы это был обычный метод.

    Типажи как параметры

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

    Функция notify теперь может принимать и Tweet, и NewsArticle, и любой другой тип, для которого мы реализуем Summary в будущем. Это и есть полиморфизм в Rust.

    Заключение

    Сегодня мы значительно расширили наш инструментарий:

  • Мы научились хранить динамические данные в векторах и хеш-картах.
  • Мы поняли, что ошибки — это не катастрофа, а просто еще один тип данных (Result), который можно и нужно обрабатывать.
  • Мы узнали о типажах, которые позволяют писать гибкий код, работающий с разными типами данных через единый интерфейс.
  • Эти концепции используются в Rust повсеместно. В следующей статье мы погрузимся в тему тестирования и узнаем, как автоматизировать проверку надежности нашего кода.