Представьте: вы пишете функцию, указываете входные параметры и ожидаете, что все будет работать как надо. Но на деле при вызове функции ошибочно передается текст вместо числа и программа уже ломается в руках у пользователя.
На простых примерах показали, как типы данных помогают писать быстрый и надежный код, будь то небольшая утилита или высоконагруженный сервис.
Нужно ли присваивать переменной в Rust тип данных
Непредсказуемое поведение — это частая проблема языков программирования. Rust решает ее с помощью статической типизации: компьютер должен заранее знать тип каждой переменной еще до запуска программы. В отличие от других языков, здесь нельзя оставить определение — все проверяется строго и сразу.
При этом язык обладает мощной системой вывода типов. Компилятор самостоятельно определяет тип переменной и анализирует контекст использования. Благодаря этому разработчики часто могут явно не указывать тип, что делает код более лаконичным без потери безопасности типов.
let count = 42; // компилятор выводит i32
let temperature = 36.6; // выводится f64
let is_active = true; // тип bool
let name = "Алексей"; // тип &str
let items = vec![1, 2, 3]; // Vec<i32>
В примерах выше не указана аннотация типа, но компилятор определит нужный тип данных на основе литерала и последующего использования переменной. Но бывают ситуации, когда вывод типов оказывается невозможным или неоднозначным. В таких случаях компилятор требует явного указания типа.
Классический пример — метод parse(), который преобразует строку в число. Поскольку один и тот же текст можно интерпретировать как разные числовые форматы, компилятору нужна подсказка от разработчика.
fn main() {
let input = "42";
// Ошибка компиляции: cannot infer type
// let number = input.parse();
// Корректные варианты с явной аннотацией типа
let number_i32: i32 = input.parse().unwrap();
let number_u64: u64 = input.parse().unwrap();
let number_f64: f64 = input.parse().unwrap();
println!("i32: {}, u64: {}, f64: {}", number_i32, number_u64, number_f64);
}
Без аннотации — i32, u64 или f64 — метод parse()не поймет, в какое именно число (целое или с плавающей точкой) конвертировать строку. Компилятор выдает ошибку вида «type annotations needed» или «cannot infer type for type parameter».
Та же ситуация возникает при работе с коллекциями, замыканиями и некоторыми обобщенными функциями, когда контекст не дает достаточно информации для однозначного вывода типа. В таких случаях явная типизация становится обязательной.
В целом механизм вывода типов в Rust избавляет от лишнего написания boilerplate-кода, не жертвуя строгими проверками при сборке. Разработчик может быть уверен, что ошибки несоответствия типов будут выявлены еще до запуска программы.
Как указывать тип данных в Rust
В языке Rust существует несколько способов явно сообщить компилятору, какой тип должен иметь значение переменной или литерала, то есть какой тип данных мы используем. Самый наглядный и привычный вариант — это классическая аннотация типа через двоеточие.
let age: u32 = 29;
let height: f64 = 1.78;
let is_admin: bool = true;
let title: &str = "Senior Rust Developer";
let coordinates: (f32, f32) = (45.32, 37.61);
После имени переменной ставится двоеточие, затем следует желаемый тип. Это основной способ, который используют везде, где автоматика не справляется или где нужно помочь коллегам быстрее понять ваш код.
Еще один вариант — суффиксы прямо в литералах (числах или символах), когда значение присваивается сразу и нет необходимости вводить отдельную аннотацию.
let small_number = 255u8; // тип u8
let big_unsigned = 18_446_744_073_709_551_615u64; // u64
let negative = -32768i16; // i16
let precise = 3.141592653589793_f64; // f64
let byte_value = b'x'; // u8 (байт-литерал)
let wide_char = '⚓'; // char
Если суффикс не указан, компилятор по умолчанию выбирает i32 для целых чисел без знака минус и f64 для чисел с плавающей точкой.
В Rust для читаемости часто используют символ подчеркивания «_» как визуальный разделитель разрядов (например, 1_000_000). Компилятор их полностью игнорирует — это сделано исключительно для того, чтобы разработчику было проще читать большие значения.
let one_billion = 1_000_000_000; // 10⁹
let max_u64_value = 18_446_744_073_709_551_615u64;
let credit_card = 1234_5678_9012_3456u64;
let hex_color = 0xFF_00_7F; // RGB
let binary_pattern = 0b1010_1100_1111_0000u16;
let scientific = 1.299_792_458e8_f64; // скорость света м/с
Основные правила использования подчеркивания: не в начале, не в конце и не рядом с точкой в числах с плавающей запятой. Компилятор воспринимает все варианты ниже как идентичные значения:
let a = 1_000_000;
let b = 1000000;
let c = 1_00_00_00;
Все три записи равны миллиону и имеют тип i32. Комбинация этих трех приемов (аннотация через двоеточие, суффиксы литералов и визуальные разделители) позволяет писать код, который одновременно безопасен с точки зрения типов, удобен для чтения и минимизирует количество лишних символов в большинстве повседневных ситуаций.
Числовые
Числа в Rust делятся на две большие категории: целочисленные и числа с плавающей точкой. Все числовые типы являются скалярными и имеют фиксированный размер в памяти, что гарантирует предсказуемое поведение и отсутствие скрытых аллокаций.
Целочисленные типы делятся на знаковые и беззнаковые. Знаковые типы способны хранить как положительные, так и отрицательные значения, беззнаковые — только неотрицательные.
Целочисленные типы:
- знаковые — i8, i16, i32, i64, i128, isize;
- беззнаковые — u8, u16, u32, u64, u128, usize.
Типы с суффиксом isize и usize являются «машинно-зависимыми»: их размер совпадает с разрядностью указателя на текущей архитектуре. На большинстве современных систем это 64 бита (usize = 8 байт), но на 32-битных платформах — 32 бита. Именно поэтому usize чаще всего используется для индексов массивов, длин векторов и адресов в памяти.
Если тип не аннотирован и у литерала нет суффикса, Rust по умолчанию выберет i32:
let default_int = 42; // i32
let explicit_u8 = 255u8; // u8
let big_signed = 9223372036854775807i64; // i64
let pointer_size = 0usize; // usize
Важная особенность операций в Rust — строгая защита от переполнения. В режиме отладки (debug) при попытке выйти за границы диапазона программа завершается паникой. Это помогает отловить ошибки до того, как они попадут в продакшен.
let mut x: u8 = 255;
x = x + 1; // в debug → panic: attempt to add with overflow
В режиме оптимизированной сборки переполнение вызывает не панику, а циклическое обертывание. Значение просто начинается сначала.
// cargo run --release
let mut x: u8 = 255;
x = x.wrapping_add(1); // x становится 0
Для явного управления поведением при переполнении в любом режиме сборки существуют методы семейства wrapping_*, checked_*, overflowing_* и saturating_*.
let a: u8 = 200;
let b: u8 = 100;
let wrapped = a.wrapping_add(b); // 44
let checked = a.checked_add(b); // None
let overflow = a.overflowing_add(b); // (44, true)
let saturated = a.saturating_add(b); // 255
Современные процессоры (особенно x86-64 и ARM64) выполняют операции с f64 практически с той же скоростью, что и с f32. При этом f64 обеспечивает высокую точность и гораздо больший диапазон значений.
Поэтому в большинстве библиотек и приложений на Rust предпочтение отдается именно f64, если нет жестких требований к экономии памяти или совместимости с внешними форматами: графикой OpenGL, шейдерами, старыми протоколами.
let single = 3.14f32;// f32
let double = 3.141592653589793;// f64 (по умолчанию)
let pi = 3.14159265358979323846_f64;
let tau = 2.0 * pi;// точное удвоение
let approx = 22.0 / 7.0_f32;// менее точное приближение π
С дробными числами f32 и f64 Rust работает по стандарту IEEE-754, включая специальные значения: бесконечность (±Inf), не-число (NaN) и денормализованные числа.
Нечисловые
Кроме чисел, в Rust есть основные типы, которые встречаются почти в любом коде: логический тип (bool), одиночные символы и два способа работы со строками.
Тип char предназначен для хранения одного Unicode-скаляра. В отличие от других языков, где символ занимает один байт и ограничен ASCII, в Rust char всегда занимает четыре байта. Это позволяет хранить любой допустимый кодпоинт Unicode — от латиницы и кириллицы до эмодзи и иероглифов.
let letter: char = 'A';
let cyrillic: char = 'Я';
let emoji: char = '🚀';
let zero_width: char = '\u{200D}';// zero width joiner
Литералы типа char записываются в одинарных кавычках. Компилятор проверяет, что внутри находится ровно один валидный Unicode-скаляр. Попытка записать несколько символов или некорректную escape-последовательность приводит к ошибке компиляции.
Логический тип bool имеет всего два возможных значения: true и false. В памяти занимает один байт, хотя теоретически достаточно было бы одного бита. Такой размер выбран для совместимости с большинством аппаратных архитектур и удобства выравнивания в структурах.
let is_production: bool = false;
let has_permission = true;
let is_empty = items.is_empty();// метод, возвращающий bool
Логические значения широко используются в условных конструкциях, циклах и для представления состояния программы.
Для работы с текстом в Rust существуют два основных типа:
- неизменяемая ссылка на строку (&str);
- изменяемая строка (String).
Тип &str представляет собой строковый срез — указатель на последовательность байтов в кодировке UTF-8 плюс информацию о длине. Содержимое &str всегда неизменяемо, а само значение может указывать либо на статическую память (строковые литералы), либо на часть другой строки, либо на содержимое String.
let greeting: &str = "Привет, Rust!";
let hello = "Hello";// тип выводится как &str
let substring = &greeting[0..6];// &str, указывающий на "Привет"
const VERSION: &str = "1.75.0";// &'static str
Строковые литералы автоматически имеют тип &’static str — они хранятся в сегменте только для чтения исполняемого файла и живут все время выполнения программы.
Тип String — это изменяемый, владеющий строковый тип, хранящийся в куче (heap) в кодировке UTF-8. Он состоит из трех полей: указатель на данные, текущая длина и зарезервированная емкость. String гарантирует, что содержимое всегда валидно в UTF-8.
let mut name = String::new();// пустая строка
name.push_str("Александр");
name.push(' ');// добавляем один символ
name += "Сергеевич";// оператор += работает
let full_name = format!("{} {}", name, "Иванов");
let json = String::from(r#"{"active": true}"#);
let capacity_example = String::with_capacity(1024);
String реализует множество полезных методов: push, push_str, truncate, clear, replace, to_lowercase, trim и другие. При необходимости из String легко получить &str через .as_str() или через автоматическое приведение.
fn print_info(text: &str) {
println!("Длина: {}, емкость: {}", text.len(), text.as_bytes().len());
}
let s = String::from("Rust крут!");
print_info(&s);// &String автоматически преобразуется в &str
print_info("статический");// работает напрямую
Разделение типов String и &str позволяет языку эффективно управлять памятью, избегать ненужного копирования и сохранять удобство работы со строками. В большинстве функций библиотеки предпочтительно принимать &str, чтобы работать как со статическими строками, так и со строками из String без лишних аллокаций.
Ссылки и указатели
В Rust прямой доступ к произвольным адресам памяти через сырые указатели возможен, но используется крайне редко и только в unsafe-коде. Основной и рекомендуемый способ работы с памятью другого объекта — это безопасные ссылки.
Ссылки бывают двух видов: неизменяемые (&T) и изменяемые (&mut T). Они представляют собой указатели, которые гарантируют соблюдение правил владения и заимствования на этапе компиляции.
let value = 42;
let ref_to_value: &i32 = &value;// неизменяемая ссылка
let mut mutable = String::from("hello");
let ref_mut: &mut String = &mut mutable;// изменяемая ссылка
Ссылки всегда указывают на существующий объект в памяти и не могут быть нулевыми. Компилятор отслеживает время жизни каждой ссылки, что исключает возможность использования указателя после освобождения данных (dangling pointer).
Сырые указатели (*const T и *mut T) похожи на указатели в C/C++, но их использование требует блока unsafe.
let x = 100;
let raw_ptr: *const i32 = &x as *const i32;
unsafe {
println!("{}", *raw_ptr);// разыменование сырого указателя
}
Сырые указатели применяются при написании FFI (взаимодействие с C-библиотеками), низкоуровневых аллокаторов, драйверов устройств и в редких случаях оптимизации производительности.
Любой указатель (как безопасная ссылка, так и сырой) в конечном итоге хранит адрес байта в памяти. Однако современные процессоры читают данные гораздо эффективнее, если адрес начала объекта кратен его размеру в байтах. Это свойство называется выравниванием.
Например:
- значение типа i32 (4 байта) предпочтительно размещать по адресам, кратным 4;
- значение типа i64 (8 байт) — по адресам, кратным 8;
- значение типа f64 — тоже по адресам, кратным 8.
Если данные расположены не по естественному выравниванию, процессор вынужден выполнять несколько операций на чтение/запись, а затем дополнительные операции сдвига и маскирования. В худшем случае это может замедлить выполнение в несколько раз.
#[repr(align(16))]
struct AlignedData {
data: [u8; 64],
}
struct NormalStruct {
a: u8,// смещение 0
b: u32,// смещение 4 (автоматическое выравнивание)
c: u16,// смещение 8
}
Атрибут #[repr(align(N))] позволяет принудительно задать выравнивание структуры. Без него компилятор сам расставляет padding-байты (байты выравнивания), чтобы каждое поле начиналось с нужного адреса.
Безопасные ссылки &T и &mut T скрывают всю сложность управления памятью, обеспечивая при этом нулевую стоимость в рантайме (они компилируются в обычные указатели процессора) и полную защиту от наиболее распространенных ошибок работы с памятью.
Кортежи и массивы в Rust
Это простейшие составные типы. В обоих случаях длина фиксирована и известна заранее, еще при сборке программы. Однако они по-разному устроены и решают разные задачи. Кортеж — это набор значений, которые могут быть разных типов. Его размер строго определен в момент создания.
Кортежи удобно использовать для временной группировки разнородных данных, возврата нескольких значений из функции или в качестве ключей в хеш-таблицах (при условии, что все типы реализуют необходимые трейты).
let person = ("Алексей", 34, true, 1.78_f64);
let coordinates: (f32, f32, f32) = (10.5, -3.2, 7.8);
let http_response = (200, "OK", 1452usize);
Доступ к элементам кортежа осуществляется через точку и числовой индекс, начинающийся с нуля.
let name = person.0; // "Алексей"
let age = person.1; // 34
let is_active = person.2; // true
Более элегантный способ — деструктуризация, которая позволяет сразу привязать имена к каждому элементу.
let (username, level, admin, height) = person;
let (x, y, z) = coordinates;
println!("x = {}, y = {}, z = {}", x, y, z);
Существует специальный кортеж нулевой длины unit-тип, записываемый как (). Он обозначает «отсутствие значения» и используется как возвращаемый тип функций, которые ничего не возвращают (аналог void в других языках). Выражение () также является единственным значением этого типа.
fn do_something() -> () {
println!("Выполнено");
// неявно возвращается ()
}
let result: () = do_something();
Массив в Rust — это непрерывный блок памяти фиксированной длины, все элементы которого имеют один и тот же тип. Массивы размещаются в стеке, что делает их очень быстрыми и предсказуемыми по потреблению памяти.
let numbers: [i32; 5] = [1, 2, 3, 4, 5];
let zeros = [0u8; 1024];// 1024 нуля типа u8
let repeated = ["error"; 3];// ["error", "error", "error"]
let matrix: [[f64; 3]; 4] = [
[1.0, 0.0, 0.0],
[0.0, 1.0, 0.0],
[0.0, 0.0, 1.0],
[0.0, 0.0, 0.0],
];
Доступ к элементам массива осуществляется через квадратные скобки и индекс.
let first = numbers[0]; // 1
let third = numbers[2]; // 3
let last = numbers[4]; // 5
Одна из ключевых особенностей массивов в Rust — встроенная защита от выхода за границы во время выполнения. При попытке доступа по недопустимому индексу программа немедленно завершается с паникой.
let arr = [10, 20, 30];
// Нормально
println!("{}", arr[1]); // 20
// Паника в runtime
// println!("{}", arr[10]); // thread 'main' panicked at 'index out of bounds: the len is 3 but the index is 10'
Такая проверка выполняется при каждом обращении по индексу, за исключением случаев, когда компилятор может доказать безопасность на этапе компиляции. Это радикально снижает вероятность ошибок переполнения буфера и некорректного доступа к памяти, одной из самых опасных уязвимостей в низкоуровневом программировании.
Если нужна динамическая длина, вместо массива обычно используют вектор Vec<T>. Но когда размер известен заранее и не меняется, массив лучше: у него нет лишних затрат на память, он гарантированно размещается в стеке.
Кортежи и массивы вместе покрывают большинство сценариев, где требуется хранить небольшое фиксированное количество значений, обеспечивая при этом максимальную производительность и безопасность типов.
Векторы
Вектор Vec — самая популярная динамическая коллекция в Rust. Главное отличие от массива в том, что вектор умеет расти и сжиматься прямо во время работы программы. Все данные он хранит в куче единым блоком, а сам вектор отслеживает, сколько места уже занято, а сколько зарезервировано на будущее.
// Создание вектора
let mut v1 = Vec::new();// пустой
let mut v2 = vec![1, 2, 3, 4];// с начальными значениями
let v3: Vec = vec![0; 1024];// 1024 нуля
// Доступ к элементам
let second = v2[1];// 2
v2[10];// паника при выходе за границы
// Безопасный доступ
if let Some(val) = v2.get(5) {
println!("Элемент: {}", val);
}
Вектор автоматически расширяется при необходимости (удваивая capacity), поддерживает reserve() для предварительного выделения памяти и легко преобразуется в срез &[T] или &mut [T]. Благодаря этому Vec используется практически во всех случаях, когда размер последовательности заранее неизвестен или должен изменяться динамически.
Хэш-таблица
Хэш-таблица HashMap<K, V> — основной тип в стандартной библиотеке Rust для хранения пар «ключ — значение». Она обеспечивает очень быстрое (в среднем O(1)) получение, добавление и удаление элементов по ключу и широко применяется при обработке конфигураций, кэшировании, подсчете частот, группировке данных и в тех ситуациях, где требуется быстрый поиск по произвольному ключу.
Ключи должны реализовывать трейты Hash и Eq, а значения могут быть любого типа. Наиболее распространенные типы ключей — String, &str, i32, u64, usize и составные типы (кортежи, структуры). Для использования типов f32 или f64 в качестве ключей требуется преобразовывать их в битовое представление.
use std::collections::HashMap;
fn main() {
// Создание и заполнение
let mut scores = HashMap::new();
scores.insert(String::from("Алексей"), 95);
scores.insert("Мария".to_string(), 88);
// Доступ по ключу
if let Some(score) = scores.get("Алексей") {
println!("Оценка Алексея: {}", score);// 95
}
// Проверка наличия ключа
println!("Есть ли Ольга? {}", scores.contains_key("Ольга"));// false
// Перебор всех пар
for (name, score) in &scores {
println!("{} → {}", name, score);
}
scores.remove("Алексей");// Удаление
}
HashMap автоматически расширяется при необходимости, использует алгоритм SipHash по умолчанию (защищен от атак по времени выполнения), а при большом количестве элементов показывает отличную производительность. Для случаев, когда ключи — строки, часто удобнее использовать &str в качестве ключа при чтении (благодаря Borrow), а String — при владении.
Для сохранения порядка вставки элементов, вместо HashMap используют BTreeMap (упорядоченный по ключам) или indexmap / hashbrown с сохранением порядка. Но в большинстве повседневных задач именно HashMap<K, V> остается стандартным и наиболее эффективным выбором для ассоциативного массива в Rust.
Как указать другие системы измерения, кроме десятичной
Числовые литералы можно записывать не только в привычной десятичной системе, но и в шестнадцатеричной, восьмеричной и двоичной. Для этого используются специальные префиксы, которые сразу указывают компилятору основание системы счисления.
let decimal = 255; // десятичная (по умолчанию)
let hex = 0xFF; // 255 в шестнадцатеричной
let octal = 0o377; // 255 в восьмеричной
let binary = 0b1111_1111; // 255 в двоичной
let large_hex = 0xFFFF_FFFF_FFFF_FFFFu64;// максимальное u64
let color = 0xFF5733; // типичный RGB-цвет в hex
let mask = 0b1010_1100; // битовая маска
Все перечисленные литералы имеют один и тот же тип (по умолчанию i32, если не указан суффикс) и одинаковое значение в памяти. Префиксы лишь меняют способ записи, но не влияют на внутреннее представление числа.
Для записи одиночного байта (u8) удобно использовать байт-литерал с префиксом b. Он записывается как ASCII-символ в одинарных кавычках с буквой b перед кавычками.
let byte_a: u8 = b'A'; // 65
let byte_space: u8 = b' '; // 32
let byte_newline: u8 = b'\n'; // 10
// ошибка компиляции — 'Я' не помещается в один байт
let byte_russian: u8 = b'Я';
Байт-литералы ограничены значениями, которые укладываются в один байт (0–255), поэтому допустимы только ASCII-символы и escape-последовательности (\n, \t, \r, \xHH и т.д.).
Чтобы указать нужную систему счисления, достаточно просто использовать соответствующий префикс перед числом:
- без префикса — десятичная;
- 0x — шестнадцатеричная;
- 0o — восьмеричная;
- 0b — двоичная.
Компилятор сам видит эти префиксы, так что лишних пояснений в коде не нужно. С ними гораздо проще работать на низком уровне: с теми же масками, цветовыми кодами, сетевыми протоколами, низкоуровневыми флагами и дампами памяти.
Чем может помочь Selectel
Если вам необходимо перенести критичные части инфраструктуры, микросервисы для обработки данных, системы мониторинга, прокси-серверы, балансировщики нагрузки, разработанные на Rust — наша команда Selectel поможет сделать это быстро и безопасно.
Выделенные серверы (Bare Metal) — идеальный выбор для Rust-приложений, где критично отсутствие оверхеда виртуализации. Вы получаете прямой доступ к ресурсам процессора и памяти, что позволяет реализовать весь потенциал языка в задачах с низкими задержками (low-latency).
Managed Kubernetes — если ваша архитектура на Rust состоит из десятков микросервисов. Мы берем на себя управление мастер-нодами и обновление кластера, чтобы вы могли сосредоточиться на коде, а не на эксплуатации.
Облачные базы данных возьмут на себя рутину по администрированию, резервному копированию и обновлению PostgreSQL, MySQL или Redis, позволяя вашей команде сосредоточиться на логике приложений.
Балансировщики нагрузки — обеспечат отказоустойчивость ваших систем, эффективно распределяя трафик между Rust-воркерами.
Наша команда бесплатно проконсультирует вас и поможет составить оптимальный план миграции, чтобы перенос критичных частей инфраструктуры прошел быстро и без простоев.