Типы данных

Каждое значение в Rust имеет конкретный тип данных, сообщающий Rust, какие данные используются и как с ними работать. Мы рассмотрим два подмножества типов данных: скалярные и сложные.

Не забывайте, что Rust является статически типизированным (statically typed) языком. Это означает, что он должен знать типы всех переменных во время компиляции. Обычно компилятор может вывести (infer) какой тип мы хотим использовать, основываясь на значении и на том, как мы с ним работаем. В случаях, когда может быть выведено несколько типов, необходимо вручную добавлять аннотацию типа. Например, когда мы конвертировали String в число с помощью вызова parse в разделе "Сравнение предположения с загаданным номером" Главы 2, мы должны добавить такую аннотацию:


#![allow(unused)]
fn main() {
let guess: u32 = "42".parse().expect("Not a number!");
}

Если мы не добавим аннотацию типа : u32 выше, Rust отобразит ошибку, что компилятору нужно больше информации от нас, чтобы узнать, какой тип мы хотим использовать:

$ cargo build
   Compiling no_type_annotations v0.1.0 (file:///projects/no_type_annotations)
error[E0282]: type annotations needed
 --> src/main.rs:2:9
  |
2 |     let guess = "42".parse().expect("Not a number!");
  |         ^^^^^ consider giving `guess` a type

For more information about this error, try `rustc --explain E0282`.
error: could not compile `no_type_annotations` due to previous error

В будущем вы увидите различные аннотации для разных типов данных.

Скалярные типы данных

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

Целочисленные типы

Целое число integer, — это число без дробной части. В Главе 2 мы использовали один целочисленный тип — тип u32. Это объявление типа указывает, что значение, с которым оно связано, должно быть целым числом без знака (типы целых чисел со знаком начинаются с i вместо u), которое занимает 32 бита памяти. В Таблице 3-1 показаны встроенные целочисленные типы в Rust. Мы можем использовать любой из этих вариантов для объявления типа целочисленного значения.

Таблица 3-1: Целочисленные типы в Rust

ДлинаЗнаковыйБеззнаковый
8-битi8u8
16-битi16u16
32-битi32u32
64-битi64u64
128-битi128u128
archisizeusize

Каждый вариант может быть как со знаком, так и без знака и имеет явный размер. Характеристика типа как знаковый и без знаковый определяет возможность числа быть отрицательным, другими словами, должно ли число иметь знак (знаковое) или оно всегда будет только положительным и, следовательно, может быть представлено без знака (беззнаковое). Это похоже на написание чисел на бумаге: когда знак имеет значение, число отображается со знаком плюс или со знаком минус; однако, когда можно с уверенностью предположить, что число положительное, оно отображается без знака. Числа со знаком хранятся с использованием дополнительного кода.

Каждый вариант со знаком может хранить числа от -(2 n - 1 ) до 2 n - 1 - 1 включительно, где n - количество битов, которые использует этот вариант. Таким образом, i8 может хранить числа от -(2 7 ) до 2 7 - 1, что равно от -128 до 127. Варианты без знака могут хранить числа от 0 до 2 n - 1, поэтому u8 может хранить числа от 0 до 2 8 - 1, что равно от 0 до 255.

Кроме того, isize и usize зависят от архитектуры компьютера, на котором запускается ваша программа, что отражено в таблице как «arch»: 64-битный, если вы используете 64-битную архитектуру, и 32-битный, если вы на 32-битной архитектуре.

Целочисленные литералы можно записывать в любой из форм, показанных в Таблице 3-2. Обратите внимание, что числовые литералы, которые могут быть несколькими числовыми типами, позволяют использовать суффикс типа, например 57u8, для обозначения типа. Числовые литералы также могут использовать _ в качестве визуального разделителя, чтобы число было легче читать, например 1_000, которое будет иметь то же значение, как если бы вы указали 1000.

Таблица 3-2: Целочисленные литералы в Rust

Числовые литералыПример
Десятичный98_222
Шестнадцатеричный0xff
Восьмеричный0o77
Двоичный0b1111_0000
Байт (только u8)b'A'

Как же узнать, какой тип целого числа использовать? Если вы не уверены, значения по умолчанию в Rust, как правило, подходят для начала: целочисленные типы по умолчанию i32. Основной случай, в котором вы должны использовать isize или usize, — это индексация какой-либо коллекции.

Целочисленное переполнение

Допустим, у вас есть переменная типа u8, которая может содержать значения от 0 до 255. Если вы попытаетесь изменить переменную на значение вне этого диапазона, например 256, произойдёт целочисленное переполнение, что может привести к одному из двух вариантов поведения. Когда вы компилируете в режиме отладки, Rust включает проверки целочисленного переполнения, которые вызывают панику вашей программы во время выполнения, если происходит переполнение. Rust использует термин «паника», когда программа завершает работу с ошибкой; мы обсудим паники более подробно в разделе «Неисправимые ошибки с panic!» в Главе 9.

Когда вы компилируете финальную версию программы с флагом --release, Rust не включает проверки целочисленного переполнения, вызывающего панику. Вместо этого, если происходит переполнение, Rust выполняет оборачивание дополнительного кода(two's complement). Короче говоря, значения, превышающие максимальное значение, которое тип может содержать, «переходят» к минимуму значений, которые может содержать тип. В случае u8 значение 256 становится равным 0, значение 257 становится равным 1 и так далее. Программа не будет паниковать, но переменная будет иметь значение, которое, вероятно, будет не таким, как вы ожидали. Полагаться на такое поведение считается ошибкой.

Чтобы явно обрабатывать возможность переполнения, вы можете использовать эти семейства методов, предоставляемых стандартной библиотекой для примитивных числовых типов:

  • Обернуть все режимы с помощью wrapping_* методов, например wrapping_add
  • Вернуть значение None в случае переполнения при помощи методов checked_*
  • Вернуть значение и логическое значение, указывающее, было ли переполнение с помощью методов overflowing_*
  • Насыщать при минимальном или максимальном значениях с помощью методов saturating_*

Числа с плавающей запятой

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

Вот пример, демонстрирующий числа с плавающей запятой в действии:

Файл: src/main.rs

fn main() {
    let x = 2.0; // f64

    let y: f32 = 3.0; // f32
}

Числа с плавающей точкой представлены согласно стандарту IEEE-754. Тип f32 является числом с плавающей точкой одинарной точности, а f64 имеет двойную точность.

Числовые операции

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

Файл: src/main.rs

fn main() {
    // addition
    let sum = 5 + 10;

    // subtraction
    let difference = 95.5 - 4.3;

    // multiplication
    let product = 4 * 30;

    // division
    let quotient = 56.7 / 32.2;
    let floored = 2 / 3; // Results in 0

    // remainder
    let remainder = 43 % 5;
}

Каждое из этих выражений использует математические операции и вычисляет значение, которое затем присваивается переменной. "Приложение Б" содержит список всех операторов, имеющихся в Rust.

Логический тип данных

Как и в большинстве языков программирования, логический тип в Rust может иметь два значения: true и false, и занимает в памяти один байт. Логический тип в Rust аннотируется при помощи bool. Например:

Файл: src/main.rs

fn main() {
    let t = true;

    let f: bool = false; // with explicit type annotation
}

Основной способ использования значений логического типа - это условные конструкции, такие как выражение if. Мы расскажем про работу выражения if в разделе "Условные конструкции".

Символьный тип данных

Тип char в Rust — самый примитивный алфавитный тип языка. Вот несколько примеров объявления значений char:

Файл: src/main.rs

fn main() {
    let c = 'z';
    let z: char = 'ℤ'; // with explicit type annotation
    let heart_eyed_cat = '😻';
}

Обратите внимание, что мы указываем литералы char в одинарных кавычках, в отличие от строковых литералов, которые используют двойные кавычки. Тип char в Rust имеет размер четыре байта и представляет собой Скалярное Значение Unicode, что означает, что он может представлять гораздо больше, чем просто ASCII. Буквы с ударением; китайские, японские и корейские иероглифы; эмодзи; и пробелы нулевой ширины являются допустимыми значениями char в Rust. Скалярные Значения Unicode находятся в диапазоне от U+0000 до U+D7FF и от U+E000 E000 до U+10FFFF включительно. Однако «символ» на самом деле не является концепцией в Unicode, поэтому интуитивно может не совпадать с тем, что такое char в Rust. Мы подробно обсудим эту тему в разделе «Сохранение текста в кодировке UTF-8 со строками». в Главе 8.

Сложные типы данных

Сложные типы могут группировать несколько значений в один тип. В Rust есть два примитивных сложных (комбинированных) типа: кортежи и массивы.

Кортежи

Кортеж является общим способом совместной группировки нескольких значений различного типа в единый комбинированный тип. Кортежи имеют фиксированную длину: после объявления они не могут расти или уменьшаться в размере.

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

Файл: src/main.rs

fn main() {
    let tup: (i32, f64, u8) = (500, 6.4, 1);
}

Переменной с именем tup привязывается весь кортеж, потому что кортеж является единым комбинированным элементом. Чтобы получить отдельные значения из кортежа, можно использовать сопоставление с образцом для деструктурирования значений кортежа, как в примере:

Файл: src/main.rs

fn main() {
    let tup = (500, 6.4, 1);

    let (x, y, z) = tup;

    println!("The value of y is: {y}");
}

Программа создаёт кортеж, привязывает его к переменной tup. Затем в let используется шаблон для превращения tup в три отдельных переменные: x, y и z. Такого рода операция называется деструктуризацией (destructuring), потому что она разрушает один кортеж на три части. В конце программа печатает значение y, которое равно 6.4.

Мы также можем напрямую обращаться к элементу кортежа, используя точку ( . ), за которой следует индекс значения, к которому мы хотим получить доступ. Например:

Файл: src/main.rs

fn main() {
    let x: (i32, f64, u8) = (500, 6.4, 1);

    let five_hundred = x.0;

    let six_point_four = x.1;

    let one = x.2;
}

Эта программа создаёт кортеж x, а затем обращается к каждому элементу кортежа, используя соответствующие индексы. Как и в большинстве языков программирования, первый индекс в кортеже равен 0.

Кортеж без каких-либо значений имеет специальное имя unit. Это значение и соответствующий ему тип записываются () и представляют собой пустое значение или пустой возвращаемый тип. Выражения неявно возвращают unit, если они не возвращают никакого другого значения.

Массивы

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

Мы записываем значения в массиве в виде списка, разделённого запятыми, внутри квадратных скобок:

Файл: src/main.rs

fn main() {
    let a = [1, 2, 3, 4, 5];
}

Массивы полезны, когда вы хотите, чтобы ваши данные размещались в стеке, а не в куче (мы более подробно обсудим стек и кучу в Главе 4).) или когда вы хотите, чтобы у вас всегда было фиксированное количество элементов. Однако массив не такой гибкий, как векторный тип. Вектор — это аналогичный тип коллекции, предоставляемый стандартной библиотекой, размер которого может увеличиваться или уменьшаться. Если вы не уверены, использовать ли массив или вектор, скорее всего, вам следует использовать вектор. Глава 8 раскрывает векторы более подробно.

Однако массивы более полезны, когда вы знаете, что количество элементов не нужно будет изменять. Например, если бы вы использовали названия месяцев в программе, вы, вероятно, использовали бы массив, а не вектор, потому что вы знаете, что он всегда будет содержать 12 элементов:


#![allow(unused)]
fn main() {
let months = ["January", "February", "March", "April", "May", "June", "July",
              "August", "September", "October", "November", "December"];
}

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


#![allow(unused)]
fn main() {
let a: [i32; 5] = [1, 2, 3, 4, 5];
}

Здесь, i32 является типом каждого элемента массива. После точки с запятой указано число 5 показывающее, что массив содержит 5 элементов.

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


#![allow(unused)]
fn main() {
let a = [3; 5];
}

Массив в переменной a будет включать 5 элементов, значение которых будет равно 3. Данная запись аналогична коду let a = [3, 3, 3, 3, 3];, но является более краткой.

Доступ к элементам массива

Массив — это единый фрагмент памяти известного фиксированного размера, который может быть размещён в стеке. Вы можете получить доступ к элементам массива с помощью индексации, например:

Файл: src/main.rs

fn main() {
    let a = [1, 2, 3, 4, 5];

    let first = a[0];
    let second = a[1];
}

В этом примере переменная с именем first получит значение 1, потому что это значение с индексом [0] в массиве. Переменная с именем second получит значение 2 из индекса [1] в массиве.

Некорректный доступ к элементу массива

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

Файл: src/main.rs

use std::io;

fn main() {
    let a = [1, 2, 3, 4, 5];

    println!("Please enter an array index.");

    let mut index = String::new();

    io::stdin()
        .read_line(&mut index)
        .expect("Failed to read line");

    let index: usize = index
        .trim()
        .parse()
        .expect("Index entered was not a number");

    let element = a[index];

    println!("The value of the element at index {index} is: {element}");
}

Этот код успешно компилируется. Если вы запустите его, используя команду cargo run и введёте 0, 1, 2, 3 или 4, программа выведет соответствующее значение по этому индексу в массиве. Если вместо этого вы введёте число после конца массива, например 10, вы увидите следующий вывод:

thread 'main' panicked at 'index out of bounds: the len is 5 but the index is 10', src/main.rs:19:19
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

Программа привела к ошибке во время выполнения в момент использования недопустимого значения в операции индексирования. Программа завершилась с сообщением об ошибке и не выполнила утверждение println!. Когда вы пытаетесь получить доступ к элементу с помощью индексации, Rust проверит, что указанный вами индекс меньше длины массива. Если индекс больше или равен длине, Rust запаникует. Но в данном случае проверка также должна выполняться во время выполнения, потому что компилятор не может знать, какое значение введёт пользователь, когда позже запустит код.

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