Перейти к основному содержимому

Базовые типы данных

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

Статическая типизация в Rust

Явное указание типов

Явное указание типов
fn main() {
let x: i32 = 42; // 32-битное целое число
let y: f64 = 3.14159; // 64-битное число с плавающей точкой
let z: bool = true; // Булево значение
let c: char = '🦀'; // Символ Unicode

println!("x: {}, y: {}, z: {}, c: {}", x, y, z, c);
}

Автоматический вывод типов

Вывод типов компилятором
fn main() {
let x = 42; // Компилятор выводит i32
let y = 3.14159; // Компилятор выводит f64
let z = true; // Компилятор выводит bool
let c = '🦀'; // Компилятор выводит char

// Можно проверить тип с помощью std::any::type_name
println!("Тип x: {}", std::any::type_name_of_val(&x));
println!("Тип y: {}", std::any::type_name_of_val(&y));
}
Вывод типов

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

Целые числа

Rust предоставляет несколько типов целых чисел разного размера:

Типы целых чисел

ТипРазмерДиапазон значений
i88 бит-128 до 127
i1616 бит-32,768 до 32,767
i3232 бита-2,147,483,648 до 2,147,483,647
i6464 бита-9,223,372,036,854,775,808 до 9,223,372,036,854,775,807
i128128 бит-(2^127) до 2^127-1
isizeзависит от архитектуры32 или 64 бита
Использование знаковых целых
fn main() {
let small: i8 = -128; // Минимальное значение для i8
let medium: i32 = -2_000_000; // i32 по умолчанию
let large: i64 = -9_223_372_036_854_775_808;

println!("i8: {}", small);
println!("i32: {}", medium);
println!("i64: {}", large);

// Проверка границ типов
println!("Минимальное i32: {}", i32::MIN);
println!("Максимальное i32: {}", i32::MAX);
}

Литералы целых чисел

Различные способы записи чисел
fn main() {
// Десятичные числа
let decimal = 98_222;

// Шестнадцатеричные
let hex = 0xff;

// Восьмеричные
let octal = 0o77;

// Двоичные
let binary = 0b1111_0000;

// Байт (только u8)
let byte = b'A';

println!("Десятичное: {}", decimal);
println!("Шестнадцатеричное: {} ({})", hex, hex);
println!("Восьмеричное: {} ({})", octal, octal);
println!("Двоичное: {} ({})", binary, binary);
println!("Байт: {} (символ: {})", byte, byte as char);

// Суффиксы типов
let typed_num = 42u64; // Явно указываем тип u64
let another_typed = 123i8; // Явно указываем тип i8

println!("u64: {}", typed_num);
println!("i8: {}", another_typed);
}

Переполнение целых чисел

Поведение при переполнении
fn main() {
// В debug режиме - паника при переполнении
// В release режиме - "обёртывание" значения

let mut x: u8 = 255;
println!("x = {}", x);

// В release режиме: 255 + 1 = 0 (переполнение)
// В debug режиме: паника!
// x += 1; // Раскомментируйте для проверки

// Явное обёртывание - безопасно в любом режиме
x = x.wrapping_add(1);
println!("После wrapping_add(1): {}", x); // 0

x = x.wrapping_sub(1);
println!("После wrapping_sub(1): {}", x); // 255

// Проверяющие операции
if let Some(result) = 255u8.checked_add(1) {
println!("Результат: {}", result);
} else {
println!("Переполнение при сложении!");
}

// Насыщающие операции
let saturated = 255u8.saturating_add(10);
println!("Насыщающее сложение: {}", saturated); // 255
}

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

Rust поддерживает два типа чисел с плавающей точкой согласно стандарту IEEE-754:

Типы с плавающей точкой

f32 - одинарная точность

  • Размер: 32 бита
  • Точность: ~6-7 значащих цифр
  • Диапазон: ±1.175494e-38 до ±3.402823e38
Использование f32
fn main() {
let x: f32 = 3.14159;
let y: f32 = 1e6; // Научная нотация

println!("f32: {}", x);
println!("Научная нотация: {}", y);
println!("f32::MAX: {}", f32::MAX);
println!("f32::MIN: {}", f32::MIN);
}

f64 - двойная точность

  • Размер: 64 бита
  • Точность: ~15-17 значащих цифр
  • Диапазон: ±2.225074e-308 до ±1.797693e308
Использование f64
fn main() {
let x = 3.14159265358979323846; // f64 по умолчанию
let y: f64 = 2.71828182845904523536;

println!("π: {}", x);
println!("e: {}", y);
println!("f64::MAX: {}", f64::MAX);
println!("f64::MIN: {}", f64::MIN);
}

Операции с числами с плавающей точкой

Арифметические операции
fn main() {
let x = 10.5;
let y = 3.2;

println!("Сложение: {} + {} = {}", x, y, x + y);
println!("Вычитание: {} - {} = {}", x, y, x - y);
println!("Умножение: {} * {} = {}", x, y, x * y);
println!("Деление: {} / {} = {}", x, y, x / y);
println!("Остаток: {} % {} = {}", x, y, x % y);

// Унарные операции
println!("Отрицание: -{} = {}", x, -x);
println!("Абсолютное значение: |{}| = {}", -x, (-x).abs());
}

Проблемы точности с плавающей точкой

Особенности арифметики с плавающей точкой
fn main() {
// Проблема точности
let x = 0.1 + 0.2;
println!("0.1 + 0.2 = {}", x); // Не равно 0.3!
println!("x == 0.3: {}", x == 0.3); // false

// Правильное сравнение чисел с плавающей точкой
let epsilon = 1e-10;
let difference = (x - 0.3).abs();
println!("Разность: {}", difference);
println!("Приблизительно равно 0.3: {}", difference < epsilon);

// Использование библиотеки для точного сравнения
fn approximately_equal(a: f64, b: f64) -> bool {
(a - b).abs() < f64::EPSILON * 2.0
}

println!("Приблизительно равны: {}", approximately_equal(x, 0.3));

// Демонстрация накопления ошибок
let mut sum = 0.0;
for _ in 0..10 {
sum += 0.1;
}
println!("10 * 0.1 = {}", sum);
println!("sum == 1.0: {}", sum == 1.0); // Может быть false
}

Булевы значения

Тип bool представляет логические значения истина/ложь:

Основы работы с bool

Булевы значения и операции
fn main() {
let is_rust_awesome = true;
let is_difficult: bool = false; // Явное указание типа

println!("Rust потрясающий: {}", is_rust_awesome);
println!("Rust сложный: {}", is_difficult);

// Логические операции
let and_result = is_rust_awesome && is_difficult; // И (AND)
let or_result = is_rust_awesome || is_difficult; // ИЛИ (OR)
let not_result = !is_difficult; // НЕ (NOT)

println!("true && false = {}", and_result); // false
println!("true || false = {}", or_result); // true
println!("!false = {}", not_result); // true

// Короткозамкнутые вычисления
let x = 5;
let result = x > 0 && x < 10; // Второе условие не проверяется, если первое false
println!("5 в диапазоне (0, 10): {}", result);
}

Преобразования и сравнения

Операции сравнения возвращают bool
fn main() {
let a = 5;
let b = 10;

println!("{} == {}: {}", a, b, a == b); // Равенство
println!("{} != {}: {}", a, b, a != b); // Неравенство
println!("{} < {}: {}", a, b, a < b); // Меньше
println!("{} > {}: {}", a, b, a > b); // Больше
println!("{} <= {}: {}", a, b, a <= b); // Меньше или равно
println!("{} >= {}: {}", a, b, a >= b); // Больше или равно

// Сравнение строк
let s1 = "hello";
let s2 = "world";
println!("{} == {}: {}", s1, s2, s1 == s2);
println!("{} < {} (лексикографически): {}", s1, s2, s1 < s2);
}

Символы (char)

Тип char в Rust представляет Unicode-скаляр и занимает 4 байта:

Основы работы с символами

Символы Unicode
fn main() {
let c1 = 'z'; // Латинский символ
let c2 = 'ℤ'; // Математический символ
let c3 = '😻'; // Эмодзи
let c4 = '中'; // Китайский иероглиф
let c5 = '\u{1F980}'; // Краб через Unicode-код

println!("Символы: {} {} {} {} {}", c1, c2, c3, c4, c5);

// Размер char всегда 4 байта
println!("Размер char: {} байт", std::mem::size_of::<char>());

// Получение Unicode-кода символа
println!("Код символа 'A': {}", 'A' as u32);
println!("Код символа '🦀': {}", '🦀' as u32);

// Создание символа из кода
if let Some(symbol) = char::from_u32(65) {
println!("Символ с кодом 65: {}", symbol); // 'A'
}

if let Some(crab) = char::from_u32(129408) {
println!("Символ с кодом 129408: {}", crab); // '🦀'
}
}

Методы работы с символами

Проверка свойств символов
fn main() {
let chars = ['A', 'a', '5', ' ', '\n', '!', '中', '🦀'];

for ch in chars {
println!("\nСимвол: '{}'", ch);
println!(" Буква: {}", ch.is_alphabetic());
println!(" Цифра: {}", ch.is_numeric());
println!(" Алфавитно-цифровой: {}", ch.is_alphanumeric());
println!(" Пробельный: {}", ch.is_whitespace());
println!(" Управляющий: {}", ch.is_control());
println!(" Верхний регистр: {}", ch.is_uppercase());
println!(" Нижний регистр: {}", ch.is_lowercase());
println!(" ASCII: {}", ch.is_ascii());
}
}

Преобразования между типами

Безопасные преобразования

Автоматические и безопасные преобразования
fn main() {
// Расширяющие преобразования (безопасные)
let small: i8 = 42;
let medium: i16 = small as i16; // i8 -> i16
let large: i32 = medium as i32; // i16 -> i32
let huge: i64 = large as i64; // i32 -> i64

println!("i8: {} -> i16: {} -> i32: {} -> i64: {}",
small, medium, large, huge);

// Беззнаковые к знаковым большего размера
let unsigned: u8 = 255;
let signed: i16 = unsigned as i16; // u8 -> i16 безопасно
println!("u8: {} -> i16: {}", unsigned, signed);

// f32 -> f64 безопасно
let float32: f32 = 3.14159;
let float64: f64 = float32 as f64;
println!("f32: {} -> f64: {}", float32, float64);
}

Небезопасные преобразования

Потенциально опасные преобразования
fn main() {
// Сужающие преобразования могут терять данные
let large: i32 = 300;
let small: i8 = large as i8; // Переполнение!
println!("i32: {} -> i8: {}", large, small); // 300 -> 44

// Знаковые в беззнаковые
let negative: i32 = -1;
let unsigned: u32 = negative as u32;
println!("i32: {} -> u32: {}", negative, unsigned); // -1 -> 4294967295

// Числа с плавающей точкой в целые
let float_val: f64 = 3.99;
let int_val: i32 = float_val as i32; // Отбрасывает дробную часть
println!("f64: {} -> i32: {}", float_val, int_val); // 3.99 -> 3

// Слишком большие значения
let huge_float: f64 = 1e20;
let overflow_int: i32 = huge_float as i32; // Неопределённое поведение
println!("f64: {} -> i32: {}", huge_float, overflow_int);
}

Проверяемые преобразования

Безопасные методы преобразования
fn main() {
use std::convert::TryInto;

let large: i32 = 300;

// Проверяемое преобразование
match large.try_into() {
Ok(small_val) => {
let small_val: i8 = small_val;
println!("Успешное преобразование: {}", small_val);
}
Err(_) => println!("Ошибка: {} не помещается в i8", large),
}

// Более короткая запись с unwrap_or
let safe_small: i8 = large.try_into().unwrap_or(0);
println!("Безопасное преобразование: {}", safe_small);

// Проверка границ вручную
if large >= i8::MIN as i32 && large <= i8::MAX as i32 {
let small = large as i8;
println!("Безопасное приведение: {}", small);
} else {
println!("Значение {} выходит за границы i8", large);
}
}

Составные литералы и суффиксы типов

Суффиксы типов

Использование суффиксов для указания типов
fn main() {
// Целые числа с суффиксами
let a = 42i32; // i32
let b = 42u64; // u64
let c = 42isize; // isize
let d = 42usize; // usize

// Числа с плавающей точкой с суффиксами
let e = 3.14f32; // f32
let f = 2.71828f64; // f64

println!("i32: {}, u64: {}, isize: {}, usize: {}", a, b, c, d);
println!("f32: {}, f64: {}", e, f);

// Суффиксы полезны при передаче в функции
fn process_u64(value: u64) {
println!("Обработка u64: {}", value);
}

fn process_f32(value: f32) {
println!("Обработка f32: {}", value);
}

process_u64(123u64); // Явно указываем тип
process_f32(3.14f32); // Явно указываем тип

// Без суффиксов компилятор может не понять тип
// process_u64(123); // Ошибка: неясен тип
process_u64(123_u64); // Альтернативный синтаксис
}

Разделители в числах

Улучшение читаемости больших чисел
fn main() {
// Разделители подчёркивания для читаемости
let million = 1_000_000;
let binary_mask = 0b1111_0000_1010_1010;
let hex_color = 0xFF_EC_DE;
let float_precise = 1_234.567_890;

println!("Миллион: {}", million);
println!("Двоичная маска: {} ({})", binary_mask, binary_mask);
println!("Hex цвет: {} ({})", hex_color, hex_color);
println!("Точное число: {}", float_precise);

// Разделители можно использовать в любых местах
let weird_but_valid = 1_2_3_4_5_u32;
println!("Странное, но валидное: {}", weird_but_valid); // 12345

// Популярные паттерны разделителей
let bytes_in_gb = 1_073_741_824; // Гигабайт в байтах
let nanoseconds = 1_000_000_000; // Наносекунд в секунде
let big_number = 123_456_789_012_345_u64; // Большое число

println!("ГБ в байтах: {}", bytes_in_gb);
println!("Наносекунд в секунде: {}", nanoseconds);
println!("Большое число: {}", big_number);
}

Практические примеры

Калькулятор с разными типами

Простой калькулятор
fn main() {
// Функции для работы с разными типами
fn add_integers(a: i32, b: i32) -> i32 {
a + b
}

fn add_floats(a: f64, b: f64) -> f64 {
a + b
}

fn is_even(n: i32) -> bool {
n % 2 == 0
}

fn first_letter(text: &str) -> Option<char> {
text.chars().next()
}

// Тестирование функций
let int_result = add_integers(42, 58);
let float_result = add_floats(3.14159, 2.71828);
let even_check = is_even(int_result);
let letter = first_letter("Rust");

println!("Сумма целых: {}", int_result);
println!("Сумма дробных: {}", float_result);
println!("Результат чётный: {}", even_check);
if let Some(ch) = letter {
println!("Первая буква: {}", ch);
}

// Демонстрация точности
let precise_calc = (int_result as f64) * float_result;
println!("Произведение: {}", precise_calc);
}

Анализатор текста

Анализ символов в тексте
fn main() {
let text = "Hello, 世界! 🦀 Rust 2024";

let mut letter_count = 0;
let mut digit_count = 0;
let mut whitespace_count = 0;
let mut punctuation_count = 0;
let mut emoji_count = 0;
let mut other_count = 0;

println!("Анализ текста: '{}'", text);

for ch in text.chars() {
if ch.is_alphabetic() {
letter_count += 1;
} else if ch.is_numeric() {
digit_count += 1;
} else if ch.is_whitespace() {
whitespace_count += 1;
} else if ch.is_ascii_punctuation() {
punctuation_count += 1;
} else if (ch as u32) > 127 && !ch.is_alphabetic() && !ch.is_numeric() {
// Простая проверка на эмодзи и специальные символы
emoji_count += 1;
} else {
other_count += 1;
}

println!("'{}': код U+{:04X}, категория: {}",
ch,
ch as u32,
categorize_char(ch));
}

println!("\nСтатистика:");
println!("Букв: {}", letter_count);
println!("Цифр: {}", digit_count);
println!("Пробельных: {}", whitespace_count);
println!("Знаков препинания: {}", punctuation_count);
println!("Эмодзи и спец. символов: {}", emoji_count);
println!("Прочих: {}", other_count);
}

fn categorize_char(ch: char) -> &'static str {
if ch.is_ascii_alphabetic() {
"ASCII буква"
} else if ch.is_alphabetic() {
"Unicode буква"
} else if ch.is_ascii_digit() {
"ASCII цифра"
} else if ch.is_numeric() {
"Unicode цифра"
} else if ch.is_whitespace() {
"Пробел"
} else if ch.is_ascii_punctuation() {
"Пунктуация"
} else if ch.is_control() {
"Управляющий символ"
} else {
"Прочее"
}
}

Размеры типов в памяти

Размеры различных типов
fn main() {
use std::mem;

println!("Размеры скалярных типов в байтах:");
println!("i8: {}", mem::size_of::<i8>());
println!("i16: {}", mem::size_of::<i16>());
println!("i32: {}", mem::size_of::<i32>());
println!("i64: {}", mem::size_of::<i64>());
println!("i128: {}", mem::size_of::<i128>());
println!("isize: {}", mem::size_of::<isize>());

println!("u8: {}", mem::size_of::<u8>());
println!("u16: {}", mem::size_of::<u16>());
println!("u32: {}", mem::size_of::<u32>());
println!("u64: {}", mem::size_of::<u64>());
println!("u128: {}", mem::size_of::<u128>());
println!("usize: {}", mem::size_of::<usize>());

println!("f32: {}", mem::size_of::<f32>());
println!("f64: {}", mem::size_of::<f64>());

println!("bool: {}", mem::size_of::<bool>());
println!("char: {}", mem::size_of::<char>());

// Выравнивание (alignment)
println!("\nВыравнивание типов:");
println!("i8: {} байт", mem::align_of::<i8>());
println!("i16: {} байт", mem::align_of::<i16>());
println!("i32: {} байт", mem::align_of::<i32>());
println!("i64: {} байт", mem::align_of::<i64>());
println!("f64: {} байт", mem::align_of::<f64>());
println!("char: {} байт", mem::align_of::<char>());
}

Лучшие практики работы с типами

1. Выбор подходящих типов

✅ Правильный выбор типов
fn main() {
// Для индексов массивов используйте usize
let items = vec![1, 2, 3, 4, 5];
for index in 0..items.len() { // len() возвращает usize
println!("Элемент {}: {}", index, items[index]);
}

// Для счётчиков обычно достаточно u32
let mut counter: u32 = 0;
for _ in 0..1000 {
counter += 1;
}

// Для денежных расчётов избегайте f64
// Лучше использовать целые числа (копейки вместо рублей)
let price_in_cents: u64 = 1599; // 15.99 рублей
let quantity: u32 = 3;
let total_cents = price_in_cents * (quantity as u64);
println!("Итого: {:.2} руб.", total_cents as f64 / 100.0);
}

2. Явное указание типов когда нужно

Когда указывать типы явно
fn main() {
// Когда компилятор не может вывести тип
let numbers: Vec<i32> = Vec::new(); // Пустой вектор

// При парсинге строк
let parsed: i32 = "42".parse().expect("Не число");

// При работе с generic функциями
let result = std::cmp::max(1u8, 2u8); // Указываем тип через суффикс

// В match выражениях для ясности
let value = 42;
match value {
x if x > 100i32 => println!("Большое число"),
_ => println!("Обычное число"),
}
}

3. Избегание переполнений

Безопасная работа с числами
fn main() {
// Проверяющие операции
let a: u8 = 200;
let b: u8 = 100;

match a.checked_add(b) {
Some(sum) => println!("Сумма: {}", sum),
None => println!("Переполнение при сложении!"),
}

// Насыщающие операции
let saturated_sum = a.saturating_add(b);
println!("Насыщающая сумма: {}", saturated_sum); // 255 (максимум u8)

// Обёртывающие операции (когда переполнение ожидается)
let wrapping_sum = a.wrapping_add(b);
println!("Обёртывающая сумма: {}", wrapping_sum); // 44 (300 - 256)
}

Заключение

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

Целые числа — знаковые и беззнаковые, разных размеров ✅ Числа с плавающей точкой — f32 и f64, особенности точности ✅ Булевы значения — логические операции и преобразования ✅ Символы — Unicode-поддержка и методы работы с char ✅ Преобразования типов — безопасные и небезопасные приведения ✅ Размеры и выравнивание — оптимизация использования памяти ✅ Лучшие практики — правильный выбор типов для задач

Понимание типов данных критически важно для написания эффективных и безопасных программ на Rust. Статическая типизация помогает избежать многих ошибок на этапе компиляции.

Что дальше?

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


Практические задания

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

  2. Напишите анализатор паролей, который проверяет пароль на наличие различных типов символов (буквы, цифры, специальные символы) и возвращает булевы значения

  3. Реализуйте безопасный калькулятор, который использует проверяющие арифметические операции и обрабатывает переполнения

  4. Создайте программу для работы с цветами, которая использует u8 для компонентов RGB и демонстрирует преобразования между разными представлениями цветов

Вопросы для самопроверки

  1. В чём разница между i32 и isize?
  2. Почему char занимает 4 байта в Rust?
  3. Когда следует использовать f32 вместо f64?
  4. Как безопасно преобразовать i64 в i32?
  5. Что происходит при переполнении в debug и release режимах?

Полезные ссылки