$ sudo teach IT
Модуль 1 · Первые шаги · Урок 1.4

Типы данных

После урока вы будете выбирать подходящий тип для числа, работать с bool, char, кортежами и массивами и понимать, что такое переполнение.

Теория + практика~25 минутНовичок

Переменные мы уже умеем объявлять: let spent = 0;. Но что именно лежит в такой «коробке»: целое число, дробное, буква? Для денег это важно. А ещё нужно держать вместе несколько трат, а не по переменной на каждую. Разберёмся с тем и другим.

Целые числа: сколько места нужно под число

Представьте тару: стакан, ведро, бочка. Числа в Rust устроены так же: у каждого есть тип — он говорит, сколько места отведено под число и какие значения в него влезают. Имя типа состоит из буквы и числа бит: i32 — целое со знаком (может быть отрицательным), u32 — целое без знака (только ноль и больше).

ТипЧто влезаетДля чего
u8от 0 до 255маленькие счётчики
i32примерно от −2,1 млрд до 2,1 млрдтип по умолчанию
u32от 0 до 4 294 967 295наши рубли
i64 / u64до очень больших чиселкогда u32 мало
usizeзависит от компьютерадлины и номера элементов

Если тип не указан, Rust сам выбирает i32. Чтобы выбрать другой, после имени пишут двоеточие и тип — это называется аннотацией типа:

Rust · main.rs
let a = 350;
let spent: u32 = 1550;
let big: u64 = 5_000_000_000;

Разбор по строкам

  • let a = 350; — тип не указан, будет i32.
  • let spent: u32 = 1550; — явно просим u32.
  • let big: u64 = 5_000_000_000; — в u32 не влезло бы; подчёркивания для удобства.

Для денег без копеек мы выбираем u32: отрицательных трат у нас нет, а четырёх миллиардов рублей хватит с запасом.

Дробные числа и арифметика

Дробные числа — тип f64 (от английского float, «плавающая точка»). Литерал пишут с точкой: 3.5. Складывать, вычитать и умножать можно как обычно: + - *. С делением есть сюрприз: у целых чисел / отбрасывает дробную часть, а % даёт остаток.

Rust · main.rs
println!("{}", 7 / 2);
println!("{}", 7 % 2);
println!("{}", 7.0 / 2.0);
Вывод
3
1
3.5

Разбор по строкам

  • 7 / 2 — целые числа, ответ целый: 3.
  • 7 % 2 — остаток: 7 минус 6, будет 1.
  • 7.0 / 2.0 — дробные числа, ответ 3.5.

Rust не смешивает целые и дробные числа молча. Чтобы превратить одно в другое, пишут as и имя типа. Официально это приведение типов: spent as f64 даёт дробную копию значения. При обратном превращении дробная часть отбрасывается: 3.99 as u32 будет 3.

Переполнение

Коробка бывает слишком мала. У u32 нижняя граница — ноль, и ниже неё опуститься нельзя. Вычтем из 100 число 350: ответ был бы отрицательным — это переполнение (overflow). Очевидные случаи компилятор ловит до запуска (при cargo run и cargo build):

Rust · main.rs
let spent: u32 = 100;
let refund: u32 = 350;
println!("{}", spent - refund);
Ошибка компилятора
error: this arithmetic operation will overflow
  attempt to compute `100_u32 - 350_u32`, which would overflow

Когда числа станут известны только во время работы (например, их введёт пользователь), ловить будет программа: в обычной сборке cargo run она остановится с паникой — аварийной остановкой с сообщением attempt to subtract with overflow. В сборке cargo build --release проверки нет, и число молча «заворачивается» (0 − 1 станет 4294967295). Поэтому о границах типа нужно думать заранее.

bool и char

Тип bool хранит ровно два значения: true (да) и false (нет), как выключатель. Тип char — одна буква или знак, в одинарных кавычках: 'р'. Он занимает 4 байта, поэтому вмещает любой символ Юникода, и кириллица тут не исключение. Двойные кавычки ("рубли") — уже текст, о нём позже.

Rust · main.rs
let paid = true;
let letter = 'р';
println!("{paid} {letter}");
Вывод
true р

Разбор по строкам

  • let paid = true; — это bool.
  • let letter = 'р'; — одинарные кавычки — значит char.
  • println!("{paid} {letter}"); — имена можно писать прямо в скобках.

Кортежи и массивы

Одна трата — это сумма и категория. Чтобы держать их вместе, есть кортеж (tuple): как конверт, в который кладут несколько вещей разных типов. Часть берут по номеру через точку (с нуля) или распаковывают целиком:

Rust · main.rs
let expense = (350, "еда");
println!("{}", expense.0);
let (amount, category) = expense;
println!("{amount} руб., {category}");
println!("{:?}", expense);
Вывод
350
350 руб., еда
(350, "еда")

Разбор по строкам

  • let expense = (350, "еда"); — кортеж из числа и текста (текст в двойных кавычках).
  • expense.0 — элемент с номером 0, то есть первый: 350.
  • let (amount, category) = expense; — имена по порядку получают элементы кортежа.
  • {:?} — отладочная печать; обычное {} для кортежа не работает.

Если нужно много значений одного типа, берут массив: ряд одинаковых ячеек. Длина тоже фиксирована и входит в тип: [u32; 3] — три числа u32.

Rust · main.rs
let expenses: [u32; 3] = [350, 1200, 99];
println!("{}", expenses[1]);
println!("{}", expenses.len());
println!("{:?}", expenses);
Вывод
1200
3
[350, 1200, 99]

Разбор по строкам

  • [u32; 3] — тип массива: тип элементов, точка с запятой, длина.
  • expenses[1] — индекс с нуля: это вторая трата.
  • expenses.len() — длина массива.
  • {:?} — массив тоже печатается так.

Выход за границы

У массива из трёх элементов индексы 0, 1 и 2. Обращение expenses[5] — ошибка: очевидный случай компилятор остановит (this operation will panic at runtime), а если индекс известен только при работе, программа упадёт с паникой index out of bounds: the len is 3 but the index is 5.

Практика: трата и массив в копилке

В файле src/main.rs проекта kopilka оставьте всё, что было, и перед закрывающей } функции main добавьте:

Rust · main.rs
    let expenses: [u32; 3] = [350, 1200, 99];
    let first = expenses[0];
    println!("Первая трата: {first} руб.");
    println!("Трат: {}", expenses.len());
    println!("Все траты: {:?}", expenses);

Запустите cargo run. К знакомым строкам добавится:

Вывод
Первая трата: 350 руб.
Трат: 3
Все траты: [350, 1200, 99]

Разбор по строкам

  • let expenses: [u32; 3] = ... — массив пробных трат.
  • let first = expenses[0]; — первая трата, индекс 0.
  • expenses.len() — число трат: 3.

Проверьте себя:

  • Что произойдёт, если заменить expenses[0] на expenses[3]? Запустите и прочитайте сообщение.
  • Уберите :? из последнего println!. Какую ошибку покажет компилятор?
  • Замените [u32; 3] на [u32; 4]. Что скажет компилятор и почему?
  • Почему для MONTH_LIMIT и трат мы выбрали u32, а не i32?

Частые ошибки

1. Печать массива через {}

Видно: error[E0277]: `[{integer}; 3]` doesn't implement `std::fmt::Display`. Причина: у массива нет обычной печати. Исправление: println!("{:?}", expenses);.

2. Целое и дробное вперемешку

Видно: error[E0277]: cannot multiply `u32` by `f64`. Причина: типы Rust сам не смешивает. Исправление: spent as f64 * 2.5.

3. Индекс за границей

Видно: this operation will panic at runtime или паника index out of bounds. Причина: индексы идут с нуля, последний — len() - 1. Исправление: проверьте номер.

Что важно запомнить

  • Без аннотации целое число — i32, дробное — f64; деньги в рублях держим в u32.
  • 7 / 2 даёт 3, % — остаток, as меняет тип.
  • Выход за границы типа — переполнение (паника в debug, заворачивание в release).
  • bool — true/false, char — один символ в одинарных кавычках.
  • Кортеж: разные типы, .0 или распаковка. Массив: один тип, фиксированная длина, индекс с нуля.
  • Кортеж и массив печатаются через {:?}.

Подробнее — в главе о типах данных книги Rust.

Дальше — практикум: соберём из всего изученного первую настоящую копилку.

На следующий урок