Введение
Добро пожаловать в Типы данных. Этот лаба является частью Rust Book. Вы можете практиковать свои навыки Rust в LabEx.
В этом лабе мы будем изучать концепцию типов данных в Rust, где каждое значение имеет определенный тип, чтобы определить, как с ним работать, и в тех случаях, когда возможны несколько типов, необходимо добавить аннотации типов, чтобы предоставить компилятору необходимую информацию.
Типы данных
В Rust каждое значение имеет определенный тип данных, который сообщает Rust, какой тип данных задан, чтобы он знал, как работать с этими данными. Мы рассмотрим два подмножества типов данных: скалярные и составные.
Обратите внимание, что Rust - это статически типизированный язык, что означает, что он должен знать типы всех переменных на этапе компиляции. Обычно компилятор может определить, какой тип мы хотим использовать, исходя из значения и того, как мы его используем. В тех случаях, когда возможны несколько типов, например, когда мы преобразуем String в числовой тип с использованием parse в разделе "Сравнение предположения с секретным числом", мы должны добавить аннотацию типа, как показано ниже:
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
Вы увидите разные аннотации типов для других типов данных.
Скалярные типы
Скалярный тип представляет собой одно значение. В Rust есть четыре основных скалярных типа: целые числа, числа с плавающей точкой, логические значения и символы. Вы, вероятно, знакомы с ними из других языков программирования. Давайте рассмотрим, как они работают в Rust.
Целочисленные типы
Целое число (integer) — это число без дробной части. Мы использовали один целочисленный тип в главе 2, тип u32. Это объявление типа указывает, что связанное с ним значение должно быть беззнаковым целым числом (знаковые целочисленные типы начинаются с i вместо u), которое занимает 32 бита. В таблице 3-1 показаны встроенные целочисленные типы в Rust. Мы можем использовать любой из этих вариантов для объявления типа целочисленного значения.
Таблица 3-1: Целочисленные типы в Rust
Длина Знаковый Беззнаковый
8-битный i8 u8
16-битный i16 u16
32-битный i32 u32
64-битный i64 u64
128-битный i128 u128
arch isize usize
Каждый вариант может быть знаковым или беззнаковым и имеет явный размер. Знаковый (signed) и беззнаковый (unsigned) относятся к тому, может ли число быть отрицательным — другими словами, нужно ли числу иметь знак (знаковый) или оно всегда будет положительным и, следовательно, может быть представлено без знака (беззнаковый). Это как писать числа на бумаге: когда знак имеет значение, число отображается со знаком плюс или минус; однако, когда можно предположить, что число положительное, оно отображается без знака. Знаковые числа хранятся с использованием представления в дополнительном коде (two's complement).
Каждый знаковый вариант может хранить числа от -(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, — это при индексировании какой-либо коллекции.
Переполнение целых чисел (Integer Overflow)
Допустим, у вас есть переменная типа
u8, которая может хранить значения от 0 до 255. Если вы попытаетесь изменить переменную на значение вне этого диапазона, например, 256, произойдет переполнение целого числа (integer overflow), что может привести к одному из двух вариантов поведения. При компиляции в режиме отладки Rust включает проверки переполнения целых чисел, которые заставляют вашу программу паниковать (panic) во время выполнения, если это поведение происходит. Rust использует термин паника (panicking), когда программа завершается с ошибкой; мы обсудим паники более подробно в разделе "Невосстановимые ошибки с panic!".При компиляции в режиме release с флагом
--releaseRust не включает проверки переполнения целых чисел, которые вызывают панику. Вместо этого, если происходит переполнение, Rust выполняет заворачивание в дополнительном коде (two's complement wrapping). Короче говоря, значения, превышающие максимальное значение, которое может хранить тип, "заворачиваются" до минимума значений, которые может хранить тип. В случае сu8значение 256 становится 0, значение 257 становится 1 и так далее. Программа не будет паниковать, но переменная будет иметь значение, которое, вероятно, не то, которое вы ожидали. Опора на поведение заворачивания при переполнении целых чисел считается ошибкой.Чтобы явно обработать возможность переполнения, вы можете использовать следующие семейства методов, предоставляемых стандартной библиотекой для примитивных числовых типов:
- Заворачивание во всех режимах с методами
wrapping_*, такими какwrapping_add.- Возврат значения
None, если произошло переполнение, с методамиchecked_*.- Возврат значения и логического значения, указывающего, было ли переполнение, с методами
overflowing_*.- Насыщение до минимальных или максимальных значений значения с методами
saturating_*.
Числовые типы с плавающей точкой
В Rust также есть два примитивных типа для чисел с плавающей точкой, то есть чисел с десятичной точкой. Числовые типы с плавающей точкой в Rust - это f32 и f64, которые имеют размер 32 и 64 бита соответственно. Тип по умолчанию - f64, потому что на современных ЦП скорость выполнения примерно одинакова для f32 и f64, но f64 позволяет более высокую точность. Все числовые типы с плавающей точкой знаковые.
Создайте новый проект под названием data-types:
cargo new data-types
cd data-types
Вот пример, демонстрирующий использование чисел с плавающей точкой:
Имя файла: 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() {
// сложение
let sum = 5 + 10;
// вычитание
let difference = 95.5 - 4.3;
// умножение
let product = 4 * 30;
// деление
let quotient = 56.7 / 32.2;
let truncated = -5 / 3; // Результат -1
// взятие остатка
let remainder = 43 % 5;
}
Каждый выражение в этих инструкциях использует математический оператор и вычисляется до одного значения, которое затем связывается с переменной. Приложение B содержит список всех операторов, которые предоставляет Rust.
Логический тип
Как и в большинстве других языков программирования, в Rust логический тип имеет два возможных значения: true и false. Логические значения занимают один байт памяти. Логический тип в Rust указывается с использованием bool. Например:
Имя файла: src/main.rs
fn main() {
let t = true;
let f: bool = false; // с явной аннотацией типа
}
Основной способ использования логических значений - это с использованием условных конструкций, таких как if-выражение. Мы рассмотрим, как работают if-выражения в Rust в разделе "Контрольный поток".
Символьный тип
Тип char в Rust - это самый примитивный алфавитный тип в языке. Вот несколько примеров объявления значений char:
Имя файла: src/main.rs
fn main() {
let c = 'z';
let z: char = 'ℤ'; // с явной аннотацией типа
let heart_eyed_cat = '😻';
}
Обратите внимание, что мы указываем литералы char с одинарными кавычками, в отличие от литералов строк, которые используют двойные кавычки. Тип char в Rust имеет размер в четыре байта и представляет собой Юникодный скалярный код, что означает, что он может представлять гораздо больше, чем просто ASCII. Акусатированные буквы, китайские, японские и корейские символы, эмодзи и нулевые ширины пробелы - все это допустимые значения char в Rust. Юникодные скалярные коды находятся в диапазоне от U+0000 до U+D7FF и от U+E000 до U+10FFFF включительно. Однако "символ" не является концепцией в Юникоде, поэтому ваше человеческое представление о том, что такое "символ", может не совпадать с тем, что представляет собой char в Rust. Мы подробно обсудим эту тему в разделе "Сохранение текста в кодировке UTF-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.
Кортеж без значений имеет специальное имя - единица. Это значение и соответствующий ему тип записываются как () и представляют собой пустое значение или пустой тип возврата. Выражения неявно возвращают значение единицы, если они не возвращают никакого другого значения.
Тип массива
Еще один способ хранить коллекцию нескольких значений - это с использованием массива. В отличие от кортежа, каждый элемент массива должен иметь один и тот же тип. В отличие от массивов в некоторых других языках, массивы в Rust имеют фиксированную длину.
Мы записываем значения в массиве в виде списка, разделенных запятыми, внутри квадратных скобок:
Имя файла: src/main.rs
fn main() {
let a = [1, 2, 3, 4, 5];
}
Массивы полезны, когда вы хотите, чтобы ваши данные были выделены на стеке, а не на куче (мы поговорим о стеке и куче более подробно в главе 4), или когда вы хотите гарантировать, что всегда будете иметь фиксированное количество элементов. Однако массив менее гибок, чем тип вектор. Вектор - это похожая коллекция, предоставляемая стандартной библиотекой, которая может увеличиваться или уменьшаться по размеру. Если вы не уверены, какой тип использовать - массив или вектор, скорее всего, вы должны выбрать вектор. Глава 8 рассматривает векторы более подробно.
Однако массивы более полезны, когда вы знаете, что количество элементов не изменится. Например, если вы используете имена месяцев в программе, вы, вероятно, будете использовать массив вместо вектора, потому что вы знаете, что он всегда будет содержать 12 элементов:
let months = ["January", "February", "March", "April", "May", "June", "July",
"August", "September", "October", "November", "December"];
Вы записываете тип массива, используя квадратные скобки, указывая тип каждого элемента, точку с запятой и затем количество элементов в массиве, вот так:
let a: [i32; 5] = [1, 2, 3, 4, 5];
Здесь i32 - это тип каждого элемента. После точки с запятой число 5 показывает, что массив содержит пять элементов.
Вы также можете инициализировать массив так, чтобы каждый элемент имел одно и то же значение, указав начальное значение, за которым следует точка с запятой, а затем длину массива в квадратных скобках, как показано здесь:
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 и то, как вы можете писать читаемый, безопасный код, который не вызывает паники и не позволяет доступа к недействительной памяти.
Резюме
Поздравляем! Вы завершили лабораторную работу по типам данных. Вы можете выполнить больше лабораторных работ в LabEx, чтобы улучшить свои навыки.