В этой лекции мы познакомимся с основами синтаксиса 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: переменные, типы, операторы, выражения и многое другое.
Практикуйтесь и задавайте вопросы!