Типи даних

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

Пам'ятайте, що Rust - статично типізована мова, тобто типи всіх змінних має бути відомим під час компіляції. Компілятор зазвичай може вивести, який тип ми хочемо використати, виходячи зі значення і того, як ми його використовуємо. У випадках, коли може підійти кілька типів, наприклад коли якщо ми перетворювали 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.

Цілі типи

Ціле - це число без дробової частини. Ви використали один цілий тип у Розділі 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 за замовучанням зазвичай непоганий, а цілий тип за замовчуванням в Rust - i32. Основна ситуація, в якій варто використовувати isize та usize - індексація якого виду колекції.

Переповнення цілого числа

Скажімо, що у вас є змінна типу u8, що може мати значення між 0 та 255. Якщо ви спробуєте змінити її значення на те, що виходить за межі цього діапазону, скажімо 256, стається переповнення, що призводить однієї з двох поведінок. Коли ви компілюєте програму в режимі дебагу, Rust додає перевірки на переповнення, які призведуть до паніки під час роботи програми, якщо воно станеться. Rust використовує термін паніка, коли програма завершується із помилкою; ми обговоримо паніку детальніше у підрозділі “Невідновлювані помилки за допомогою panic! Розділу 9.

Коли ж ви компілюєте в режимі релізу за допомогою прапорця --release, Rust не додає перевірок на переповнення, що спричинили б паніку. Натомість якщо виникає переповнення, Rust загортає з доповненням до двох це число. Якщо коротко, значення, більші за максимальне значення, що вміщується в тип, "загортаються" до мінімального значення, що вміщується в тип. У випадку з 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;
}

Кожен вираз використовує математичний оператор і обчислює значення, яке прив'язується до змінної. Додаток B містить список усіх операторів, які використовуються в мові Rust.

Булівський тип

Як і в більшості інших мов програмування, булівський тип у Rust має два можливі значення: true ("істина") та false ("неправда"). Булівський тип займає 1 байт. Булівський тип у 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 має чотири байти і представляє cкалярне значення в Юнікоді, тобто може представляти значно більше, ніж просто ASCII. Літери з наголосами, китайські, японські і корейські символи, смайлики і пробіли нульової ширини є коректними значеннями для char у Rust. Скалярні значення Юнікода можуть бути в діапазоні від U+0000 до U+D7FF і U+E000 до U+10FFFF включно. Однак "символ" насправді не є концепцією Юнікода, тому ваше інтуїтивне уявлення про те, що таке "символ" може не зовсім відповідати тому, чим є char у Rust. Цю тему ми детальніше обговоримо в підрозділі "Зберігання тексту, кодованого в UTF-8, у стрічках" Розділу 8.

Складені типи

Складені типи дозволяють об'єднувати багато значень в один тип. Rust має два базових складених типи: кортежі та масиви.

Тип кортеж

Кортеж (tuple) - основний спосіб збирати до купи ряд значень різних типів у один складений тип. Кортежі мають фіксовану довжину: один раз проголошені, вони не можуть зростати чи скорочуватися.

Кортеж утворюється списком значень, розділених комами, в дужках. Кожна позиція в кортежі має тип, і типи різних значень у кортежі не обов'язково мають збігатися. Ми додали необов'язкову анотацію типу у цьому прикладі:

Файл: 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 елементів:

#![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,, ``, або 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 і як ви можете писати читаний, безпечний код що не панікує і не дозволяє некоректний доступ до пам'яті. ch02-00-guessing-game-tutorial.html#comparing-the-guess-to-the-secret-number