Простые типы

Язык Rust имеет несколько типов, которые считаются «простыми» («примитивными»). Это означает, что они встроены в язык. Rust структурирован таким образом, что стандартная библиотека также предоставляет ряд полезных типов, построенных на базе этих простых типов, но это самые простые.

Логический тип (bool)

Rust имеет встроенный логический тип, называемый bool. Он может принимать два значения, true и false:

fn main() { let x = true; let y: bool = false; }
let x = true;

let y: bool = false;

Логические типы часто используются в конструкции if.

Вы можете найти больше информации о логических типах (bool) в документации к стандартной библиотеке (англ.).

Символы (char)

Тип char представляет собой одиночное скалярное значение Unicode. Вы можете создать char с помощью одинарных кавычек: (')

fn main() { let x = 'x'; let two_hearts = '💕'; }
let x = 'x';
let two_hearts = '💕';

Это означает, что в отличие от некоторых других языков, char в Rust представлен не одним байтом, а четырьмя.

Вы можете найти больше информации о символах (char) в документации к стандартной библиотеке (англ.).

Числовые типы

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

Эти типы состоят из двух частей: категория и размер. Например, u16 представляет собой тип без знака с размером в шестнадцать бит. Чем большим количеством бит представлен тип, тем большее число мы можем задать.

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

fn main() { let x = 42; // x имеет тип i32 let y = 1.0; // y имеет тип f64 }
let x = 42; // x имеет тип i32

let y = 1.0; // y имеет тип f64

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

Давайте пройдёмся по их категориям.

Знаковые и беззнаковые

Целые типы бывают двух видов: знаковые и беззнаковые. Чтобы понять разницу, давайте рассмотрим число с размером в четыре бита. Знаковые четырёхбитные числа, позволяют хранить значения от -8 до +7. Знаковые числа используют представление «дополнение до двух» (дополнительный код). Беззнаковые четырёхбитные числа, ввиду того что не нужно хранить отрицательные значения, позволяют хранить значения от 0 до +15.

Беззнаковые типы используют u для своей категории, а знаковые типы используют i. i означает «integer». Так, u8 представляет собой число без знака с размером восемь бит, а i8 представляет собой число со знаком с размером восемь бит.

Типы фиксированного размера

Типы с фиксированным размером соответственно имеют фиксированное количество бит в своём представлении. Допустимыми размерами являются 8, 16, 32, 64. Таким образом, u32 представляет собой целое число без знака с размером 32 бита, а i64 — целое число со знаком с размером 64 бита.

Типы переменного размера

Rust также предоставляет типы, размер которых зависит от размера указателя на целевой машине. Эти типы имеют «size» в названии в качестве признака размера, и могут быть знаковыми или беззнаковыми. Таким образом, существует два типа: isize и usize.

С плавающей точкой

В Rust также есть два типа с плавающей точкой: f32 и f64. Они соответствуют IEEE-754 числам с плавающей точкой одинарной и двойной точности соответственно.

Массивы

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

fn main() { let a = [1, 2, 3]; // a: [i32; 3] let mut m = [1, 2, 3]; // m: [i32; 3] }
let a = [1, 2, 3]; // a: [i32; 3]
let mut m = [1, 2, 3]; // m: [i32; 3]

Массивы имеют тип [T; N]. О значении T мы поговорим позже, когда будем рассматривать обобщённое программирование. N — это константа времени компиляции, представляющая собой длину массива.

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

fn main() { let a = [0; 20]; // a: [i32; 20] }
let a = [0; 20]; // a: [i32; 20]

Вы можете получить число элементов массива a с помощью метода a.len():

fn main() { let a = [1, 2, 3]; println!("Число элементов в a: {}", a.len()); }
let a = [1, 2, 3];

println!("Число элементов в a: {}", a.len());

Вы можете получить определённый элемент массива с помощью индекса:

fn main() { let names = ["Graydon", "Brian", "Niko"]; // names: [&str; 3] println!("Второе имя: {}", names[1]); }
let names = ["Graydon", "Brian", "Niko"]; // names: [&str; 3]

println!("Второе имя: {}", names[1]);

Индексы нумеруются с нуля, как и в большинстве языков программирования, поэтому мы получаем первое имя с помощью names[0], а второе — с помощью names[1]. Пример выше печатает Второе имя: Brian. Если вы попытаетесь использовать индекс, который не входит в массив, вы получите ошибку: при доступе к массивам происходит проверка границ во время исполнения программы. Такая ошибочная попытка доступа — источник многих проблем в других языках системного программирования.

Вы можете найти больше информации о массивах (array) в документации к стандартной библиотеке (англ.).

Срезы

Срез — это ссылка на (или «проекция» в) другую структуру данных. Они полезны, когда нужно обеспечить безопасный, эффективный доступ к части массива без копирования. Например, возможно вам нужно сослаться на единственную строку файла, считанного в память. Из-за своей ссылочной природы, срезы создаются не напрямую, а из существующих значений. У срезов есть длина, они могут быть изменяемы или нет, и во многих случаях они ведут себя как массивы:

fn main() { let a = [0, 1, 2, 3, 4]; let middle = &a[1..4]; // Срез `a`: только элементы 1, 2, и 3 let complete = &a[..]; // Срез, содержащий все элементы массива `a` }
let a = [0, 1, 2, 3, 4];
let middle = &a[1..4]; // Срез `a`: только элементы 1, 2, и 3
let complete = &a[..]; // Срез, содержащий все элементы массива `a`

Срезы имеют тип &[T]. О значении T мы поговорим позже, когда будем рассматривать обобщённое программирование.

Вы можете найти больше информации о срезах (slice) в документации к стандартной библиотеке (англ.).

str

Тип str в Rust является наиболее простым типом строк. Это безразмерный тип, поэтому сам по себе он не очень полезен, но он становится полезным при использовании ссылки, &str. Пока просто остановимся на этом.

Вы можете найти больше информации о строках (str) в документации к стандартной библиотеке (англ.).

Кортежи

Кортеж — это последовательность фиксированного размера. Вроде такой:

fn main() { let x = (1, "привет"); }
let x = (1, "привет");

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

fn main() { let x: (i32, &str) = (1, "привет"); }
let x: (i32, &str) = (1, "привет");

Как вы можете видеть, тип кортежа выглядит как сам кортеж, но места элементов занимают типы. Внимательные читатели также отметят, что кортежи гетерогенны: в этом кортеже одновременно хранятся значения типов i32 и &str. В языках системного программирования строки немного более сложны, чем в других языках. Пока вы можете читать &str как срез строки. Мы вскоре узнаем об этом больше.

Можно присваивать один кортеж другому, если они содержат значения одинаковых типов и имеют одинаковую арность. Арность кортежей одинакова, когда их длина совпадает.

fn main() { let mut x = (1, 2); // x: (i32, i32) let y = (2, 3); // y: (i32, i32) x = y; }
let mut x = (1, 2); // x: (i32, i32)
let y = (2, 3); // y: (i32, i32)

x = y;

Стоит отметить и ещё один момент, касающийся длины кортежей: кортеж нулевой длины ((); пустой кортеж) часто называют «единичным значением». Соответственно, тип такого значения — «единичный тип».

Доступ к полям кортежа можно получить с помощью деконструирующего let. Вот пример:

fn main() { let (x, y, z) = (1, 2, 3); println!("x это {}", x); }
let (x, y, z) = (1, 2, 3);

println!("x это {}", x);

Помните, мы говорили, что левая часть оператора let может больше, чем просто присваивать имена? Мы имели ввиду то, что приведено выше. Мы можем написать слева от let шаблон, и, если он совпадает со значением справа, произойдёт присваивание имён сразу нескольким значениям. В данном случае, let «деконструирует» или «разбивает» кортеж, и присваивает его части трём именам.

Это очень удобный шаблон программирования, и мы ещё не раз увидим его.

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

fn main() { (0,); // одноэлементный кортеж (0); // ноль в круглых скобках }
(0,); // одноэлементный кортеж
(0); // ноль в круглых скобках

Индексация кортежей

Вы также можете получить доступ к полям кортежа с помощью индексации:

fn main() { let tuple = (1, 2, 3); let x = tuple.0; let y = tuple.1; let z = tuple.2; println!("x is {}", x); }
let tuple = (1, 2, 3);

let x = tuple.0;
let y = tuple.1;
let z = tuple.2;

println!("x is {}", x);

Как и в случае индексации массивов, индексы начинаются с нуля, но здесь, в отличие от массивов, используется ., а не [].

Вы можете найти больше информации о кортежах (tuple) в документации к стандартной библиотеке (англ.).

Функции

Функции тоже имеют тип! Это выглядит следующим образом:

fn main() { fn foo(x: i32) -> i32 { x } let x: fn(i32) -> i32 = foo; }
fn foo(x: i32) -> i32 { x }

let x: fn(i32) -> i32 = foo;

В данном примере x — это «указатель на функцию», которая принимает в качестве аргумента i32 и возвращает i32.