Tipuri de date

Fiecare valoare în Rust este de un anumit tip de date, care indică Rust ce fel de date sunt specificate pentru a ști cum să lucreze cu acele date. Vom analiza două subansamble de tipuri de date: scalar și compus.

Amintiți-vă că Rust este un limbaj cu tipizare statică, ceea ce înseamnă că trebuie să cunoaște tipurile tuturor variabilelor la momentul compilării. În general, compilatorul poate infera ce tip dorim să folosim în funcție de valoare și cum o utilizăm. În cazurile când sunt posibile mai multe tipuri, cum ar fi atunci când am convertit un String într-un tip numeric folosind parse în secțiunea „Comparând guess-ul cu numărul secret” din Capitolul 2, trebuie să adăugăm o adnotare de tip, astfel:

#![allow(unused)]
fn main() {
let guess: u32 = "42".parse().expect("Nu este un număr!");
}

Dacă nu adăugăm adnotarea de tip : u32 arătată în codul precedent, Rust va afișa următoarea eroare, ceea ce înseamnă că compilatorul are nevoie de mai multe informații de la noi pentru a ști ce tip dorim să folosim:

$ 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!");
  |         ^^^^^
  |
help: consider giving `guess` an explicit type
  |
2 |     let guess: _ = "42".parse().expect("Not a number!");
  |              +++

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

Vei vedea adnotări de tip diferite pentru alte tipuri de date.

Tipuri scalare

Un tip scalar reprezintă o singură valoare. Rust are patru tipuri scalare principale: numere întregi, numere în virgulă mobilă, Booleani și caractere. S-ar putea ca acestea să ți se pară familiare din alte limbaje de programare. Să trecem la modul în care funcționează tipurile scalabile în Rust.

Tipuri de întregi

Un întreg este un număr fără componentă fracționară. Am folosit un tip de întreg în Capitolul 2, tipul u32. Această declarație de tip indică faptul că valoarea cu care este asociată ar trebui să fie un întreg fără semn (tipurile de întregi cu semn încep cu i în loc de u) care ocupă 32 de biți de spațiu. Tabelul 3-1 arată tipurile de întregi integrate în Rust. Putem folosi fiecare dintre aceste variante pentru a declara tipul unei valori întregi.

Tabelul 3-1: Tipuri de întregi în Rust

LungimeCu semnFără semn
8-biții8u8
16-biții16u16
32-biții32u32
64-biții64u64
128-biții128u128
arhitect.isizeusize

Fiecare variantă poate fi fie cu semn, fie fără semn, și are o dimensiune explicită. Cu semn și fără semn se referă la faptul dacă este posibil ca numărul să fie negativ - cu alte cuvinte, dacă numărul are nevoie să aibă un semn asociat (signed) sau dacă va fi mereu pozitiv și deci poate fi reprezentat fără un semn (unsigned). Este similar cu scrierea numerelor pe hârtie: când semnul contează, un număr este prezentat cu un semn plus sau un semn minus; totuși, când este sigur că numărul este pozitiv, acesta este prezentat fără semn. Numerele cu semn sunt stocate folosind reprezentarea de complement față de doi .

Fiecare variantă cu semn poate stoca numere de la -(2n - 1) la 2n - 1 - 1 inclusiv, unde n este numărul de biți pe care îl folosește acea variantă. Așadar un i8 poate stoca numere de la -(27) la 27 - 1, care este egal cu -128 până la 127. Variantele fără semn pot stoca numere de la 0 la 2n - 1, așadar un u8 poate stoca numere de la 0 la 28 - 1, care este egal cu 0 până la 255.

În plus, tipurile isize si usize depind de arhitectura computerului pe care rulează programul, care este notată în tabelul de mai sus ca „arhitectura sistemului”: 64 de biți dacă te afli pe o arhitectură de 64-biți și 32 de biți dacă te afli pe o arhitectură de 32-biți.

Poți scrie literali întregi în oricare dintre formele prezentate în Tabelul 3-2. Observă că literali numerici care pot fi de mai multe tipuri numerice permit un sufix de tip, cum ar fi 57u8, pentru a desemna tipul. Literalii numerici pot de asemenea folosi _ ca separator vizual pentru a face numărul mai ușor de citit, precum 1_000, care va avea aceeași valoare ca și cum ai fi specificat 1000.

Tabelul 3-2: Literali întregi în Rust

Literali numericiExemplu
Zecimal98_222
Hexadecimal0xff
Octal0o77
Binar0b1111_0000
Byte (doar u8)b'A'

Deci, cum să știi ce tip de întreg să folosești? Dacă nu ești sigur, valorile setate inițial de Rust sunt în general un punct bun de plecare: tipurile întregi au implicit setat i32. Principala situație în care ai folosi isize sau usize este atunci când indexezi o colecție de elemente.

Depășirea întregilor

Să presupunem că ai o variabilă de tip u8 care poate stoca valori între 0 și 255. Dacă încerci să schimbi valoarea variabilei în afara acestui interval, cum ar fi 256, va apărea o depășire a întregilor (integer overflow), care poate produce unul dintre două comportamente. Atunci când compilezi în modul de depanare, Rust include verificări pentru depășirea întregilor care cauzează panica programului la runtime dacă acest comportament apare. Rust utilizează termenul de panică când un program se închide cu o eroare; vom discuta despre panici în profunzime în secțiunea „Erorile irecuperabile cu panic! în Capitolul 9.

Când compilezi în modul de release cu opțiunea --release, Rust nu include verificări pentru depășirea întregilor care cauzează panici. În schimb, dacă apare o depășire, Rust efectuează împachetarea complementul lui doi. Pe scurt, valorile mai mari decât valoarea maximă pe care o poate deține tipul „este împachetat” la minimul valorilor pe care tipul le poate deţine. În cazul unui u8, valoarea 256 devine 0, valoarea 257 devine 1 și așa mai departe. Programul nu va genera panică, dar variabila va avea o valoare care probabil nu este ceea ce te așteptai să aibă. Să te bazezi pe comportamentul de împachetare a întregilor depășiți este considerat greșit.

Pentru a gestiona în mod explicit posibilitatea de depășire, poți folosi aceste familii de metode oferite de biblioteca standard pentru tipurile numerice primitive:

  • Împachetează toate operațiunile cu metodele wrapping_*, cum ar fi wrapping_add.
  • Întoarce valoarea None dacă există depășire cu metodele checked_*.
  • Întoarce valoarea și un boolean care indică dacă a existat depășire cu metodele overflowing_*.
  • Saturează la valorile minime sau maxime ale valorii cu metodele saturating_*.

Tipurile cu virgulă mobilă

Rust are, de asemenea, două tipuri primitive pentru numerele în virgulă mobilă, care sunt numerele cu puncte zecimale. Tipurile Rust cu virgulă mobilă sunt f32 și f64, care au 32 de biți și 64 de biți în dimensiune, respectiv. Tipul implicit este f64 deoarece pe procesoarele moderne este aproximativ la fel de rapid ca f32, dar este capabil de mai multă precizie. Toate tipurile cu virgulă mobilă sunt semnate.

Iată un exemplu care arată numerele cu virgulă mobilă în acțiune:

Numele fișierului: src/main.rs

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

    let y: f32 = 3.0; // f32
}

Numerele cu virgulă mobilă sunt reprezentate conform standardului IEEE-754. Tipul f32 este un număr cu precizie simplă, iar f64 are precizie dublă.

Operațiuni numerice

Rust suportă operațiunile matematice de bază pe care le-ai aștepta pentru toate tipurile de numere: adunare, scădere, înmulțire, împărțire și rest. Împărțirea întreagă trunchiază spre zero la cel mai apropiat număr întreg. Următorul cod arată cum ai folosi fiecare operațiune numerică într-o declarație let:

Numele fișierului: 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;
}

Fiecare expresie în aceste declarații folosește un operator matematic și evaluează la o singură valoare, care este apoi legată la o variabilă. Anexa B conține o listă cu toți operatorii pe care Rust îi pune la dispoziție.

Tipul Boolean

Ca în majoritatea celorlalte limbaje de programare, un tip Boolean în Rust are două posibile valori: true și false. Booleanii au dimensiunea de un octet. Tipul Boolean în Rust este specificat folosind bool. De exemplu:

Numele fișierului: src/main.rs

fn main() {
    let t = true;

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

Principalul mod de a folosi valorile Boolean este prin condiționale, cum ar fi o expresie if. Vom discuta cum funcționează expresiile if în Rust în secțiunea „Fluxul de control”.

Tipul caracter

Tipul char al lui Rust este cel mai primitiv tip alfabetic al limbajului. Aici avem câteva exemple de declarare a valorilor char:

Numele fișierului: src/main.rs

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

Observați că specificăm constantele de tip char cu ghilimele simple, spre deosebire de sirurile de caractere, care utilizează ghilimele duble. Tipul char al lui Rust este de patru octeți și reprezintă o valoare scalară Unicode, ceea ce înseamnă că poate reprezenta mult mai mult decât doar ASCII. Litere accentuate; caractere chinezești, japoneze și coreene; emoji și spații de lățime zero sunt toate valori char valide în Rust. Valorile scalare Unicode variază de la U+0000 la U+D7FF și U+E000 la U+10FFFF inclusiv. Totuși, un „caracter” nu este chiar un concept în Unicode, așa că intuiția ta umană pentru ceea ce este un „caracter” s-ar putea să nu se potrivească cu ceea ce este un char în Rust. Vom discuta acest subiect în detaliu în „Stocarea textului codificat UTF-8 cu string-uri” în Capitolul 8.

Tipuri compuse

Tipurile compuse pot grupa mai multe valori într-un singur tip. Rust are două tipuri compuse primitive: tuple și array-uri.

Tipul tuplă

O tuplă este o modalitate generală de a grupa împreună un număr de valori cu o varietate de tipuri într-un singur tip compus. Tuplele au o lungime fixă: odată declarate, nu pot crește sau scădea în dimensiune.

Creăm o tuplă scriind o listă de valori separate prin virgulă între paranteze. Fiecare poziție din tuplă are un tip, și tipurile diferitelor valori din tuplă nu trebuie să fie aceleași. Am adăugat în acest exemplu opțional adnotări de tip:

Numele fișierului: src/main.rs

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

Variabila tup se leagă de întreaga tuplă pentru că o tuplă este considerată un singur element compus. Pentru a obține valorile individuale dintr-o tuplă, putem folosi potrivirea de modele pentru a destrucura o valoare a tuplei, așa cum este arătat aici:

Numele fișierului: src/main.rs

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

    let (x, y, z) = tup;

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

Acest program creează mai întâi o tuplă și o leagă la variabila tup. Apoi folosește un model cu let pentru a lua tup și a o transforma în trei variabile separate, x, y, și z. Acest lucru este numit destructurare pentru că descompune singura tuplă în trei părți. În final, programul afișează valoarea lui y, care este 6.4.

Putem accesa direct un element al tuplei folosind un punct (.) urmat de indexul valorii pe care dorim să o accesăm. De exemplu:

Numele fișierului: 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;
}

Acest program creează tupla x și apoi accesează fiecare element al tuplei folosind indicii lor respectivi. La fel ca în majoritatea limbajelor de programare, primul index într-o tuplă este 0.

Tupla fără nicio valoare are un nume special, unit. Această valoare și tipul său corespunzător sunt ambele scrise () și reprezintă o valoare goală sau un tip de returnare gol. Expresiile returnează implicit valoarea unit dacă nu returnează nicio altă valoare.

Tipul array

Un alt mod de a avea o colecție de valori multiple este cu un array. Spre deosebire de o tuplă, fiecare element al unui array trebuie să aibă același tip. Spre deosebire de array-urile din alte limbaje, array-urile din Rust au o lungime fixă.

Scriem valorile într-un array ca o listă separată prin virgule în interiorul parantezelor pătrate:

Numele fișierului: src/main.rs

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

Array-urile sunt utile când vrem ca datele să fie alocate pe stivă, nu pe heap (vom discuta mai mult despre stivă și heap în Capitolul 4) sau când dorim să ne asigurăm că avem întotdeauna un număr fix de elemente. Un array nu este la fel de flexibil precum tipul vector, totuși. Un vector este un tip de colecție similar oferit de biblioteca standard dar care are voie să crească sau să scadă în dimensiune. Dacă nu ești sigur dacă să folosești un array sau un vector, atunci probabil că ar trebui să folosești un vector. Capitolul 8 discută vectorii în mai multe detalii.

Totuși, array-urile sunt mai utile când știi că numărul de elemente nu va avea nevoie să se schimbe. De exemplu, dacă ai folosi numele lunilor într-un program, ai folosi probabil un array în loc de un vector, deoarece știi că acesta va conține întotdeauna 12 elemente:

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

Scrii tipul unui array folosind paranteze pătrate cu tipul fiecărui element, un punct și virgulă, și apoi numărul de elemente din array, așa:

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

Aici, i32 este tipul fiecărui element. După punctul și virgula, numărul 5 indică faptul că array-ul conține cinci elemente.

Poți de asemenea inițializa un array pentru a conține aceeași valoare pentru fiecare element, specificând valoarea inițială, urmată de un punct și virgulă, și apoi lungimea array-ului în paranteze pătrate, așa cum este afișat aici:

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

Array-ul numit a va conține 5 elemente care vor fi inițializate toate cu valoarea 3. Acest lucru este la fel ca scrierea let a = [3, 3, 3, 3, 3];, dar într-un mod mai concis.

Accesarea elementelor unui array

Un array este un singur segment de memorie de o dimensiune cunoscută, fixă, care poate fi alocat pe stiva. Poți accesa elementele unui array folosind indexarea, în acest fel:

Numele fișierului: src/main.rs

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

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

În acest exemplu, variabila numită first va primi valoarea 1 pentru că aceasta este valoarea la indexul [0] în array. Variabila numită second va primi valoarea 2 de la indexul [1] în array.

Acces invalid la un element al unui array

Să vedem ce se întâmplă dacă începi să accesezi un element al unui array care este după sfârșitul array-ului. Să zicem că rulezi acest cod, asemănător cu jocul de ghicit din Capitolul 2, pentru a obține un index de array de la utilizator:

Numele fișierului: 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}");
}

Cod se compilează cu succes. Dacă rulezi acest cod folosind cargo run și introduci 0, 1, 2, 3, sau 4, programul va afișa valoarea corespunzătoare acelui index în array. Dacă introduci un număr după sfârșitul array-ului, cum ar fi 10, vei vedea o ieșire de acest gen:

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

Programul a rezultat într-o eroare de runtime în momentul utilizării unei valori invalide în operația de indexare. Programul se închide cu un mesaj de eroare și nu execută instrucțiunea finală println!. Când încerci să accesezi un element folosind indexarea, Rust va verifica dacă indexul specificat de tine este mai mic decât lungimea array-ului. Dacă indexul este mai mare sau egal cu lungimea, Rust va stopa cu panică. Verificarea dată trebuie să se întâmple la runtime, în special în așa caz, pentru că compilatorul nu poate ști cu siguranță ce valoare va introduce un utilizator când va rula codul mai târziu.

Acesta este un exemplu al principiilor de siguranță ale memoriei din Rust în acțiune. În multe limbaje de nivel scăzut, verificare de acces în array nu se face, și, când furnizezi un index incorect, poate fi accesată memorie invalidă. Rust te protejează împotriva acestui fel de erori prin ieșirea imediată în loc de a permitere accesul la memorie și continuare. Capitolul 9 discută mai multe despre gestionarea erorilor în Rust și cum poți să scrii un cod lizibil și sigur, care nu face niciun fel de panică nici nu permite acces la memorie invalidă.