Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Типы данных

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

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

#![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[E0284]: type annotations needed
 --> src/main.rs:2:9
  |
2 |     let guess = "42".parse().expect("Not a number!");
  |         ^^^^^        ----- type must be known at this point
  |
  = note: cannot satisfy `<_ as FromStr>::Err == _`
help: consider giving `guess` an explicit type
  |
2 |     let guess: /* Type */ = "42".parse().expect("Not a number!");
  |              ++++++++++++

For more information about this error, try `rustc --explain E0284`.
error: could not compile `no_type_annotations` (bin "no_type_annotations") due to 1 previous error

Для других типов данных вы будете видеть другие аннотации типов.

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

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

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

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

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

ДлинаЗнаковыйБеззнаковый
8 битi8u8
16 битi16u16
32 битаi32u32
64 битаi64u64
128 битi128u128
Зависит от архитектурыisizeusize

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

Каждый знаковый вариант может хранить числа от −(2n − 1) до 2n − 1 − 1 включительно, где n — количество бит, используемых этим вариантом. Например, i8 может хранить числа от −(27) до 27 − 1, то есть от −128 до 127. Беззнаковые варианты могут хранить числа от 0 до 2n − 1, поэтому u8 может хранить числа от 0 до 28 − 1, то есть от 0 до 255.

Кроме того, типы isize и usize зависят от архитектуры компьютера, на котором выполняется ваша программа: 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 включает проверки целочисленного переполнения, из-за которых ваша программа вызовет panic во время выполнения, если такое поведение произойдёт. Rust использует термин panic, когда программа завершается с ошибкой; мы подробнее обсудим panic в разделе «Неустранимые ошибки с panic!» Главы 9.

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

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

  • Выполнять циклическое переполнение во всех режимах с помощью методов 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.

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

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 truncated = -5 / 3; // Results in -1

    // remainder
    let remainder = 43 % 5;
}

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

Логический тип

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

Имя файла: src/main.rs

fn main() {
    let t = true;

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

Основной способ использовать логические значения — условные конструкции, например выражение if. Мы рассмотрим, как выражения if работают в Rust, в разделе «Управление потоком выполнения».

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

Тип 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 занимает 4 байта и представляет скалярное значение Unicode. Это означает, что он может представлять намного больше, чем только ASCII. Буквы с диакритическими знаками; китайские, японские и корейские символы; эмодзи; и пробелы нулевой ширины — всё это допустимые значения char в Rust. Скалярные значения Unicode находятся в диапазонах от U+0000 до U+D7FF и от U+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. Это называется деструктурированием, потому что оно разбивает один кортеж на три части. В конце программа выводит значение 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 указывает, что массив содержит пять элементов.

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

#![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 src/main.rs:19:19:
index out of bounds: the len is 5 but the index is 10
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

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

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