В этой лекции мы познакомимся с основами синтаксиса 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
Теневое объявление позволяет создавать новую переменную с тем же именем, затеняя предыдущую. Это не модификация, а создание новой сущности, что даёт гибкость в работе с типами и значениями.
// Объявляем переменную z и присваиваем ей значение 10 (тип i32 по умолчанию)
let z = 10;
// Создаём новую переменную z
// используя предыдущее значение z и добавляя 1 (z теперь 11, тип i32)
let z = z + 1;
// Создаём ещё одну новую переменную z
// теперь это строка (тип &str), предыдущее z "затеняется"
let z = "строка";
Подробное объяснение shadowing:
let z = 10;
— создаётся переменная z
со значением 10
, тип i32
.let z = z + 1;
— новая переменная z
вычисляется как 10 + 1
, становится равной 11
, старая z
(10) недоступна.let z = "строка";
— новая z
теперь строка (&str
), предыдущее значение (11) затенено, тип полностью изменён.Зачем использовать shadowing?
mut
.Отличие от 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] — литерал массива
Даже такие структуры могут быть литералами, если их содержимое — фиксированные значения.
Литералы называют фиксированными, потому что их значение известно на этапе компиляции и не зависит от вычислений или ввода данных. Например:
42
всегда будет числом 42;"Hello"
всегда будет строкой "Hello".В отличие от переменных или результатов функций, которые могут изменяться или вычисляться во время выполнения программы, литералы неизменны и "вшиты" в код.
Литералы — это основа для работы с данными в коде. Они используются для инициализации переменных, передачи аргументов в функции или задания констант. Понимание их "фиксированности" помогает писать более предсказуемый и безопасный код, что особенно важно в Rust с его акцентом на безопасность памяти.
Если коротко: литералы — это то, что вы видите в коде "как есть", без вычислений и подвохов.
Макрос println!
в Rust используется для вывода текста в консоль, и он часто работает с литералами. Вот пример:
fn main() {
println!("Hello, world!"); // "Hello, world!" — строковый литерал
}
Что здесь происходит?
"Hello, world!"
— это строковый литерал. Это фиксированное значение, которое вы прямо записали в коде. Оно неизменяемо, не имеет имени и существует как "сырой" текст, который компилятор понимает сразу.println!
— это макрос, который берёт этот литерал и выводит его на экран.Литерал здесь — это именно "Hello, world!", а не переменная или результат вычисления. Он "зашит" в код и не меняется.
Добавим переменную для сравнения:
fn main() {
let greeting = "Hello, world!"; // "Hello, world!" — литерал
// greeting — неизменяемая переменная
println!("{}", greeting);
}
"Hello, world!"
— это по-прежнему литерал, фиксированное значение.greeting
— это неизменяемая переменная, которая привязана к этому литералу. Она даёт имя значению, чтобы можно было использовать его позже.println!("{}", greeting)
— выводит значение, на которое ссылается greeting, а не сам литерал напрямую.Литералы с плейсхолдерами в println!
. Можно использовать несколько литералов в одном вызове println!:
fn main() {
println!("Number: {}, Text: {}", 42, "Rust"); // 42 и "Rust" — литералы
}
42
— целочисленный литерал."Rust"
— строковый литерал."Number: {}, Text: {}"
— тоже строковый литерал, который служит шаблоном для вывода.Здесь все значения — фиксированные, записаны прямо в коде и передаются в макрос как есть.
Быстрая память фиксированного размера, данные копируются.
i8-i128
, u8-u128
.f32
, f64
.bool
.char
.&T
, &mut T
.(i32, char)
.[i32; 5]
.()
.Динамическая память, данные перемещаются.
String
(данные в куче, метаданные на стеке).Vec
.HashMap
, HashSet
.Box
.Rc
, Arc
.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
}
В Rust буквы вроде T
, N
, F
и подобные не обозначают конкретные типы данных, а используются как заглушки (placeholders) в обобщённом программировании (generics). Они называются типовыми параметрами и позволяют писать код, который работает с разными типами, не привязываясь к чему-то конкретному. Давай разберём это попроще, а потом я расскажу про основные типы данных в Rust.
T
, N
, F
итп?Эти буквы — просто имена, которые программист выбирает для обозначения "какого-то типа".
Если коротко: T
, N
, F
итп — это не типы, а "заменители", а настоящие типы в Rust — это числа, строки, векторы и так далее, из которых ты строишь программу.
T
, N
, F
— самые известные, есть и другие, которые часто используют:
T
— от "Type" (тип). Самый универсальный вариант, когда тип может быть любым.
Vec<T>
— вектор любого типа.N
— от "Number" (число). Обычно для числовых типов.
fn add<N>(a: N, b: N) -> N
— функция сложения для чисел.F
— от "Function" (функция). Часто для замыканий или функций.
fn apply<F>(f: F, x: i32)
— применение функции F
к числу.K
— от "Key" (ключ). Используется в структурах вроде HashMap
.
HashMap<K, V>
— ключ K
, значение V
.V
— от "Value" (значение). Пара к K
.
struct Pair<K, V> { key: K, value: V }
.E
— от "Error" (ошибка). Часто в Result
.
Result<T, E>
— успех T
или ошибка E
.U
, S
— дополнительные типы, если нужно больше одного. Просто следующие буквы после T
.
fn convert<T, U>(x: T) -> U
— преобразование из T
в U
.Да, абсолютно! Вместо T
можно написать Item
, MyType
, Foo
— что угодно. Например:
fn process<Item>(data: Item) {
println!("{:?}", data);
}
Работает так же, как с T
. Но короткие буквы вроде T
популярны, потому что они экономят место и сразу понятны тем, кто знаком с обобщениями.
Если код сложный и нужно больше ясности:
Key
и Value
вместо K
и V
в словаре.Element
вместо T
в списке.Callback
вместо F
для функции.Пример:
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
для читаемого кода.
cargo new basics
cd basics
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]); // Выводим третий элемент
}
cargo run
Вывод:
x = 10
y = 23
Кортеж: (10, 3.14, R)
Третий элемент массива: 3
cargo fmt // Форматирование кода
cargo clippy // Проверка стиля и ошибок
Совет: Экспериментируйте с типами и значениями в упражнении.
Вы изучили основы синтаксиса Rust: переменные, типы, операторы, выражения и многое другое.
Практикуйтесь и задавайте вопросы!