Глава 3: Основы синтаксиса

Содержание: Переменные и изменяемость Типы данных: скалярные и составные Операторы: арифметические, логические, побитовые Выражения и точка с запятой Литералы и их кастомизация Разница между стеком и кучей Комментарии и форматирование кода Заглушки (placeholders) или типовые параметры Упражнение: Написать программу с переменными и выводом

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


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

Переменные в Rust объявляются с помощью ключевого слова let. По умолчанию они неизменяемы (immutable), что повышает безопасность и предсказуемость кода.

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

Простое объявление переменной задаёт её имя и начальное значение. Тип часто выводится автоматически.

let x = 42; // Переменная x с значением 42
            //тип i32 (по умолчанию для целых чисел)

Если попытаться изменить неизменяемую переменную, компилятор выдаст ошибку:

let x = 42;
x = 50; // Ошибка: нельзя присвоить новое значение неизменяемой переменной

Изменяемость с mut

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

let mut y = 100; // y объявлена изменяемой с начальным значением 100
y = 200; // Теперь y равно 200, тип остаётся i32

Теневое объявление (Shadowing)

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

// Объявляем переменную z и присваиваем ей значение 10 (тип i32 по умолчанию)
let z = 10;

// Создаём новую переменную z
// используя предыдущее значение z и добавляя 1 (z теперь 11, тип i32)
let z = z + 1;

// Создаём ещё одну новую переменную z
// теперь это строка (тип &str), предыдущее z "затеняется"
let z = "строка";

Подробное объяснение shadowing:

  1. let z = 10; — создаётся переменная z со значением 10, тип i32.
  2. let z = z + 1; — новая переменная z вычисляется как 10 + 1, становится равной 11, старая z (10) недоступна.
  3. let z = "строка"; — новая z теперь строка (&str), предыдущее значение (11) затенено, тип полностью изменён.

Зачем использовать shadowing?

Отличие от mut: При mut модифицируется одна переменная, а shadowing создаёт новую, позволяя менять тип.

Совет: Используйте mut только когда нужно менять значение, а shadowing — для новых значений или типов.


Типы данных: скалярные и составные

Rust различает скалярные (одиночные значения) и составные (группы значений) типы.

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

Целые числа: Имеют знаковые (i) и беззнаковые (u) варианты с разной разрядностью:
i8-i128, u8-u128 (по умолчанию i32)

let a: i32 = -42; // Знаковое 32-битное число
let b: u8 = 255;  // Беззнаковое 8-битное число (0..255)

Числа с плавающей точкой: Дробные числа с типами f32 и f64 (по умолчанию f64)

let c: f64 = 3.14; // 64-битное число с плавающей точкой

Булевы: Логические значения true или false.

let is_active: bool = true; // Булева переменная

Символы: Одиночные Unicode-символы, записываются в одинарных кавычках (Unicode, 4 байта).

let letter: char = 'Z';  // Символ ASCII
let emoji: char = '😊';  // Unicode-символ

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

Кортежи: Фиксированная последовательность разнотипных элементов, доступ по индексу.

let tup: (i32, f64, char) = (500, 6.4, 'x'); // Кортеж из трёх элементов
let x = tup.0; // Доступ к первому элементу (500)

Массивы: Фиксированная последовательность однотипных элементов.

let arr: [i32; 3] = [1, 2, 3]; // Массив из трёх i32
let first = arr[0]; // Доступ к первому элементу (1)

Совет: Используйте кортежи для разнотипных данных, массивы — для однотипных.

Пример

fn main() {
    let num: i32 = 100;
    let float = 2.718;
    let array = [1, 2, 3];
    println!("num = {}, float = {}, array = {:?}", num, float, array);
}

Вывод: num = 100, float = 2.718, array = [1, 2, 3]


Операторы: арифметические, логические, побитовые

Операторы позволяют выполнять вычисления и сравнения.

Арифметические

let sum = 5 + 10; // Сложение: 15
let div = 10 / 3; // Целочисленное деление: 3
let rem = 10 % 3; // Остаток от деления: 1

Логические

let t = true;
let f = false;
let result = t && !f; // Логическое И с отрицанием: true

Побитовые

let a = 2;  // 0010 в двоичной системе
let b = 3;  // 0011 в двоичной системе
let c = a & b; // Побитовое И: 0010 (2)
let d = a << 1; // Сдвиг влево: 0100 (4)

Совет: Убедитесь, что типы операндов совместимы с операцией.


Выражения и точка с запятой

Выражения возвращают значение, операторы завершаются ; и не возвращают ничего полезного.

let x = {     // Блок — это выражение
    let y = 5; // Оператор внутри блока
    y + 1      // Последнее выражение без ; возвращает 6
}; // x теперь равно 6

Функция тоже использует выражения для возврата:

fn add(a: i32) -> i32 {
    a + 1 // Возвращаемое значение без ;
}

Совет: Отсутствие ; в конце выражения означает возврат значения.


Литералы и их кастомизация

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

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

Числовые литералы

let dec = 98_765;  // Десятичное число с разделителем
let hex = 0xff;    // Шестнадцатеричное: 255
let oct = 0o77;    // Восьмеричное: 63
let bin = 0b1111;  // Двоичное: 15
let x = 42u8;      // Явный тип u8
let y = 3.14_f32;  // Явный тип f32
let a = 3.14;      // 3.14 — литерал с плавающей точкой типа f64 (по умолчанию)
let b = 2.0e-3;    // 0.002 в экспоненциальной записи
let x = 42;        // 42 — это целочисленный литерал типа i32 (по умолчанию)
let y = 1_000_000; // 1_000_000 — тоже литерал, разделители для читаемости
let z = 0xFF;      // Шестнадцатеричный литерал (255 в десятичной системе)

Здесь 42, 1_000_000 и 0xFF — фиксированные значения, которые вы буквально вписали в код.

Компилятор Rust автоматически определяет тип литерала (например, i32 для целых чисел или f64 для дробных), но вы можете явно указать тип с помощью суффиксов

Строковые литералы

let s = "Hello, Rust!"; // Обычная строка, "Hello, Rust!" — строковый литерал
let raw = r#"Сырой текст с "кавычками""#; // Сырая строка без экранирования

Символьные литералы

let c = 'a';          // Простой символ, 'a' — символьный литерал типа char
let unicode = '\u{1F600}'; // Unicode-символ: 😀

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

Булевы литералы:

let t = true; // true — булевый литерал
let f = false; // false — булевый литерал

Значения true и false фиксированы и встроены в язык.

Составные литералы:

let tuple = (1, "test"); // (1, "test") — литерал кортежа
let array = [1, 2, 3]; // [1, 2, 3] — литерал массива

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

Литералы называют фиксированными, потому что их значение известно на этапе компиляции и не зависит от вычислений или ввода данных. Например:

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

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

Если коротко: литералы — это то, что вы видите в коде "как есть", без вычислений и подвохов.

Макрос println! в Rust используется для вывода текста в консоль, и он часто работает с литералами. Вот пример:

fn main() {
    println!("Hello, world!"); // "Hello, world!" — строковый литерал
}

Что здесь происходит?

Литерал здесь — это именно "Hello, world!", а не переменная или результат вычисления. Он "зашит" в код и не меняется.

Добавим переменную для сравнения:

fn main() {
    let greeting = "Hello, world!"; // "Hello, world!" — литерал
                                    // greeting — неизменяемая переменная
    println!("{}", greeting);
}

Литералы с плейсхолдерами в println!. Можно использовать несколько литералов в одном вызове println!:

fn main() {
    println!("Number: {}, Text: {}", 42, "Rust"); // 42 и "Rust" — литералы
}

Здесь все значения — фиксированные, записаны прямо в коде и передаются в макрос как есть.


Разница между стеком и кучей

Стек (Stack)

Быстрая память фиксированного размера, данные копируются.

Куча (Heap)

Динамическая память, данные перемещаются.

Пример

fn main() {
    let a = 42;           // Стек
    let b = a;           // Копия
    let s1 = String::from("Rust"); // Куча
    let s2 = s1;         // Перемещение
    println!("a = {}, b = {}, s2 = {}", a, b, s2);
}

Вывод: a = 42, b = 42, s2 = Rust

Полный перечень типов для стека и кучи

Все примитивные типы для стека (i8-u128, f32, f64, bool, char, &T, фиксированные кортежи и массивы)

Сложные типы для кучи (String, Vec, HashMap, Box, Rc/Arc).


Комментарии и форматирование кода

Комментарии

// Однострочный комментарий
/* Многострочный
   комментарий */
/// Документационный комментарий для генерации документации
fn add_one(x: i32) -> i32 {
    x + 1
}

Заглушки (placeholders) или типовые параметры

В Rust буквы вроде T, N, F и подобные не обозначают конкретные типы данных, а используются как заглушки (placeholders) в обобщённом программировании (generics). Они называются типовыми параметрами и позволяют писать код, который работает с разными типами, не привязываясь к чему-то конкретному. Давай разберём это попроще, а потом я расскажу про основные типы данных в Rust.

Что такое T, N, F итп?

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

Если коротко: T, N, F итп — это не типы, а "заменители", а настоящие типы в Rust — это числа, строки, векторы и так далее, из которых ты строишь программу.

Популярные условности

Хотя T, N, F — самые известные, есть и другие, которые часто используют:

Можно ли использовать другие имена?

Да, абсолютно! Вместо T можно написать Item, MyType, Foo — что угодно. Например:

fn process<Item>(data: Item) {
    println!("{:?}", data);
}

Работает так же, как с T. Но короткие буквы вроде T популярны, потому что они экономят место и сразу понятны тем, кто знаком с обобщениями.

Когда используют нестандартные имена?

Если код сложный и нужно больше ясности:

Пример:

struct Container<Element> {
    items: Vec<Element>,
}

Ограничения

Типовые параметры не "живут" сами по себе — их нужно определить в угловых скобках <...> (например, fn foo<T> или struct Bar<T>), а потом использовать в коде. Имена не должны совпадать с ключевыми словами Rust (if, for, struct и т.д.).

Итого

"Заменителей" не три, а бесконечно много — это просто имена, которые ты придумываешь. T, N, F — это традиции, но можно встретить A, B, X, MyCoolType и что угодно ещё. Главное, чтобы код оставался читаемым. Если видишь незнакомую букву в <...>, это почти наверняка типовой параметр!

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

До форматирования:

fn main(){let x=42;println!("{}",x);}

После cargo fmt:

fn main() {
    let x = 42;
    println!("{}", x);
}

Совет: Регулярно используйте cargo fmt для читаемого кода.


Упражнение: Написать программу с переменными и выводом

Шаг 1: Создание проекта

cargo new basics
cd basics

Шаг 2: Код

fn main() {
    let x = 10;              // Неизменяемая переменная
    let mut y = 20;          // Изменяемая переменная
    let tup = (x, 3.14, 'R'); // Корgpu с тремя элементами
    let arr = [1, 2, 3, 4, 5]; // Массив из пяти элементов
    y = y + arr[2];          // Изменяем y, добавляя третий элемент массива (3)
    println!("x = {}", x);   // Выводим x
    println!("y = {}", y);   // Выводим y (23)
    println!("Кортеж: ({}, {}, {})", tup.0, tup.1, tup.2); // Выводим кортеж
    println!("Третий элемент массива: {}", arr[2]); // Выводим третий элемент
}

Шаг 3: Запуск

cargo run

Вывод:

x = 10
y = 23
Кортеж: (10, 3.14, R)
Третий элемент массива: 3

Шаг 4: Проверка

cargo fmt   // Форматирование кода
cargo clippy // Проверка стиля и ошибок

Совет: Экспериментируйте с типами и значениями в упражнении.


Заключение

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

Практикуйтесь и задавайте вопросы!