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

Typy danych

Każda wartość w Ruście ma określony typ danych, który informuje Rusta, jaki rodzaj danych jest określany, aby wiedział, jak z nimi pracować. Przyjrzymy się dwóm podzbiorom typów danych: skalarnym i złożonym.

Pamiętaj, że Rust jest językiem statycznie typowanym, co oznacza, że musi znać typy wszystkich zmiennych w czasie kompilacji. Kompilator zazwyczaj może wywnioskować, jakiego typu chcemy użyć, na podstawie wartości i sposobu jej użycia. W przypadkach, gdy możliwych jest wiele typów, np. gdy konwertowaliśmy String na typ numeryczny za pomocą parse w sekcji „Porównywanie strzału z tajną liczbą” w Rozdziale 2, musimy dodać adnotację typu, taką jak ta:

#![allow(unused)]
fn main() {
let guess: u32 = "42".parse().expect("Not a number!");
}

Jeśli nie dodamy adnotacji typu : u32 pokazanej w poprzednim kodzie, Rust wyświetli następujący błąd, co oznacza, że kompilator potrzebuje od nas więcej informacji, aby wiedzieć, jakiego typu chcemy użyć:

$ 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

Zobaczysz różne adnotacje typów dla innych typów danych.

Typy skalarne

Typ skalarny reprezentuje pojedynczą wartość. Rust ma cztery główne typy skalarne: liczby całkowite, liczby zmiennoprzecinkowe, wartości boolowskie i znaki. Możesz je rozpoznać z innych języków programowania. Przejdźmy do tego, jak działają w Rust.

Typy całkowite

Liczba całkowita to liczba bez części ułamkowej. Użyliśmy jednego typu całkowitego w Rozdziale 2, typu u32. Ta deklaracja typu wskazuje, że wartość, z którą jest skojarzona, powinna być niepodpisaną liczbą całkowitą (typy całkowite ze znakiem zaczynają się od i zamiast u), która zajmuje 32 bity pamięci. Tabela 3-1 pokazuje wbudowane typy całkowite w Rust. Możemy użyć dowolnego z tych wariantów do zadeklarowania typu wartości całkowitej.

Tabela 3-1: Typy całkowite w Rust

DługośćZe znakiemBez znaku
8-bitówi8u8
16-bitówi16u16
32-bitówi32u32
64-bitówi64u64
128-bitówi128u128
Zależne od architekturyisizeusize

Każdy wariant może być ze znakiem lub bez znaku i ma wyraźny rozmiar. Ze znakiem i bez znaku odnoszą się do tego, czy liczba może być ujemna – inaczej mówiąc, czy liczba musi mieć znak (ze znakiem), czy zawsze będzie dodatnia i dlatego może być reprezentowana bez znaku (bez znaku). To jak pisanie liczb na papierze: gdy znak ma znaczenie, liczba jest pokazywana ze znakiem plus lub minus; jednak, gdy można bezpiecznie założyć, że liczba jest dodatnia, jest pokazywana bez znaku. Liczby ze znakiem są przechowywane za pomocą reprezentacji dopełnienia do dwóch.

Każdy wariant ze znakiem może przechowywać liczby od −(2n − 1) do 2n − 1 − 1 włącznie, gdzie n to liczba bitów, które używa ten wariant. Tak więc i8 może przechowywać liczby od −(27) do 27 − 1, co równa się −128 do 127. Warianty bez znaku mogą przechowywać liczby od 0 do 2n − 1, więc u8 może przechowywać liczby od 0 do 28 − 1, co równa się 0 do 255.

Dodatkowo, typy isize i usize zależą od architektury komputera, na którym działa Twój program: 64 bity, jeśli jesteś na architekturze 64-bitowej, i 32 bity, jeśli jesteś na architekturze 32-bitowej.

Literały całkowite można zapisywać w dowolnej z form pokazanych w tabeli 3-2. Zauważ, że literały liczbowe, które mogą być wieloma typami numerycznymi, dopuszczają sufiks typu, taki jak 57u8, do określenia typu. Literały liczbowe mogą również używać _ jako wizualnego separatora, aby ułatwić odczytanie liczby, np. 1_000, która będzie miała taką samą wartość, jakbyś określił 1000.

Tabela 3-2: Literały całkowite w Rust

Literały liczbowePrzykład
Dziesiętne98_222
Szesnastkowe0xff
Ósemkowe0o77
Binarne0b1111_0000
Bajtowe (tylko u8)b'A'

Więc skąd wiesz, jakiego typu liczby całkowitej użyć? Jeśli nie jesteś pewien, domyślne ustawienia Rusta są zazwyczaj dobrym punktem wyjścia: typy całkowite domyślnie przyjmują i32. Główna sytuacja, w której użyłbyś isize lub usize, to indeksowanie jakiegoś rodzaju kolekcji.

Przepełnienie liczby całkowitej

Załóżmy, że masz zmienną typu u8, która może przechowywać wartości od 0 do 255. Jeśli spróbujesz zmienić zmienną na wartość spoza tego zakresu, taką jak 256, nastąpi przepełnienie liczby całkowitej, co może skutkować jednym z dwóch zachowań. Podczas kompilacji w trybie debugowania Rust zawiera sprawdzenia przepełnienia liczby całkowitej, które powodują, że program panikuje w czasie wykonywania, jeśli wystąpi takie zachowanie. Rust używa terminu panicking, gdy program kończy działanie z błędem; omówimy paniki bardziej szczegółowo w sekcji „Błędy nie do odzyskania za pomocą panic! w Rozdziale 9.

Podczas kompilacji w trybie wydania z flagą --release, Rust nie zawiera sprawdzeń przepełnienia liczby całkowitej, które powodują paniki. Zamiast tego, jeśli wystąpi przepełnienie, Rust wykonuje zawijanie dopełnienia do dwóch. Krótko mówiąc, wartości większe niż maksymalna wartość, jaką może przechowywać typ, „zawijają się” do minimalnej wartości, jaką może przechowywać typ. W przypadku u8, wartość 256 staje się 0, wartość 257 staje się 1 i tak dalej. Program nie będzie panikował, ale zmienna będzie miała wartość, która prawdopodobnie nie jest tym, czego się spodziewałeś. Opieranie się na zachowaniu zawijania przepełnienia liczby całkowitej jest uważane za błąd.

Aby jawnie obsłużyć możliwość przepełnienia, możesz użyć tych rodzin metod dostarczanych przez standardową bibliotekę dla prymitywnych typów liczbowych:

  • Zawijaj we wszystkich trybach za pomocą metod wrapping_*, takich jak wrapping_add.
  • Zwracaj wartość None, jeśli wystąpi przepełnienie, za pomocą metod checked_*.
  • Zwracaj wartość i wartość boolowską wskazującą, czy wystąpiło przepełnienie, za pomocą metod overflowing_*.
  • Saturuj przy minimalnych lub maksymalnych wartościach typu za pomocą metod saturating_*.

Typy zmiennoprzecinkowe

Rust ma również dwa prymitywne typy dla liczb zmiennoprzecinkowych, które są liczbami z punktami dziesiętnymi. Typy zmiennoprzecinkowe Rusta to f32 i f64, które mają odpowiednio 32 i 64 bity. Domyślnym typem jest f64, ponieważ na nowoczesnych procesorach ma on w przybliżeniu taką samą prędkość jak f32, ale jest w stanie zapewnić większą precyzję. Wszystkie typy zmiennoprzecinkowe są ze znakiem.

Oto przykład, który pokazuje liczby zmiennoprzecinkowe w akcji:

Nazwa pliku: src/main.rs

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

    let y: f32 = 3.0; // f32
}

Liczby zmiennoprzecinkowe są reprezentowane zgodnie ze standardem IEEE-754.

Operacje numeryczne

Rust obsługuje podstawowe operacje matematyczne, których można oczekiwać dla wszystkich typów liczbowych: dodawanie, odejmowanie, mnożenie, dzielenie i reszta z dzielenia. Dzielenie całkowite obcina w kierunku zera do najbliższej liczby całkowitej. Poniższy kod pokazuje, jak użyć każdej operacji numerycznej w instrukcji let:

Nazwa pliku: src/main.rs

fn main() {
    // dodawanie
    let sum = 5 + 10;

    // odejmowanie
    let difference = 95.5 - 4.3;

    // mnożenie
    let product = 4 * 30;

    // dzielenie
    let quotient = 56.7 / 32.2;
    let truncated = -5 / 3; // Wynosi -1

    // reszta z dzielenia
    let remainder = 43 % 5;
}

Każde wyrażenie w tych instrukcjach używa operatora matematycznego i ocenia się do pojedynczej wartości, która następnie jest wiązana ze zmienną. Dodatek B zawiera listę wszystkich operatorów, dostarczanych przez Rusta.

Typ boolowski

Podobnie jak w większości innych języków programowania, typ boolowski w Rust ma dwie możliwe wartości: true i false. Wartości boolowskie mają rozmiar jednego bajta. Typ boolowski w Rust jest określany za pomocą bool. Na przykład:

Nazwa pliku: src/main.rs

fn main() {
    let t = true;

    let f: bool = false; // z jawną adnotacją typu
}

Głównym sposobem używania wartości boolowskich jest poprzez warunki, takie jak wyrażenie if. Omówimy, jak działają wyrażenia if w Rust w sekcji „Sterowanie przepływem”.

Typ znakowy

Typ char w Rust jest najbardziej prymitywnym typem alfabetycznym języka. Oto kilka przykładów deklarowania wartości char:

Nazwa pliku: src/main.rs

fn main() {
    let c = 'z';
    let z: char = 'ℤ'; // z jawną adnotacją typu
    let heart_eyed_cat = '😻';
}

Zwróć uwagę, że literały char określamy pojedynczymi cudzysłowami, w przeciwieństwie do literałów stringowych, które używają podwójnych cudzysłowów. Typ char w Rust ma rozmiar 4 bajtów i reprezentuje skalarną wartość Unicode, co oznacza, że może reprezentować znacznie więcej niż tylko ASCII. Litery akcentowane; znaki chińskie, japońskie i koreańskie; emotikony; oraz spacje o zerowej szerokości są w Ruście prawidłowymi wartościami char. Skalarne wartości Unicode mieszczą się w zakresie od U+0000 do U+D7FF oraz od U+E000 do U+10FFFF włącznie. Jednak „znak” nie jest tak naprawdę koncepcją w Unicode, więc Twoja ludzka intuicja co do tego, czym jest „znak”, może nie pasować do tego, czym jest char w Rust. Omówimy ten temat szczegółowo w sekcji „Przechowywanie tekstu kodowanego UTF-8 za pomocą ciągów znaków” w Rozdziale 8.

Typy złożone

Typy złożone mogą grupować wiele wartości w jeden typ. Rust ma dwa prymitywne typy złożone: krotki i tablice.

Typ krotki

Krotka to ogólny sposób grupowania wielu wartości o różnych typach w jeden typ złożony. Krotki mają stałą długość: po zadeklarowaniu nie mogą rosnąć ani kurczyć się.

Tworzymy krotkę, pisząc listę wartości oddzielonych przecinkami w nawiasach. Każda pozycja w krotce ma typ, a typy różnych wartości w krotce nie muszą być takie same. W tym przykładzie dodaliśmy opcjonalne adnotacje typów:

Nazwa pliku: src/main.rs

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

Zmienna tup wiąże się z całą krotką, ponieważ krotka jest traktowana jako pojedynczy element złożony. Aby uzyskać poszczególne wartości z krotki, możemy użyć dopasowania wzorców do dekonstrukcji wartości krotki, w ten sposób:

Nazwa pliku: src/main.rs

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

    let (x, y, z) = tup;

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

Ten program najpierw tworzy krotkę i wiąże ją ze zmienną tup. Następnie używa wzorca z let, aby wziąć tup i zamienić ją w trzy oddzielne zmienne: x, y i z. Nazywa się to dekonstrukcją, ponieważ rozbija pojedynczą krotkę na trzy części. Na koniec program wypisuje wartość y, która wynosi 6.4.

Możemy również uzyskać dostęp do elementu krotki bezpośrednio, używając kropki (.) następującej po indeksie wartości, do której chcemy uzyskać dostęp. Na przykład:

Nazwa pliku: 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;
}

Ten program tworzy krotkę x, a następnie uzyskuje dostęp do każdego elementu krotki za pomocą ich odpowiednich indeksów. Podobnie jak w większości języków programowania, pierwszy indeks w krotce to 0.

Krotka bez żadnych wartości ma specjalną nazwę, jednostka. Ta wartość i jej odpowiedni typ są zapisywane jako () i reprezentują pustą wartość lub pusty typ zwracany. Wyrażenia niejawnie zwracają wartość jednostkową, jeśli nie zwracają żadnej innej wartości.

Typ tablicowy

Innym sposobem posiadania kolekcji wielu wartości jest tablica. W przeciwieństwie do krotki, każdy element tablicy musi mieć ten sam typ. W przeciwieństwie do tablic w niektórych innych językach, tablice w Rust mają stałą długość.

Wartości w tablicy zapisujemy jako listę oddzieloną przecinkami w nawiasach kwadratowych:

Nazwa pliku: src/main.rs

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

Tablice są przydatne, gdy chcesz, aby dane były alokowane na stosie, tak jak inne typy, które widzieliśmy do tej pory, a nie na stercie (omówimy stos i sterę dokładniej w Rozdziale 4) lub gdy chcesz zapewnić, że zawsze będziesz mieć stałą liczbę elementów. Tablica nie jest jednak tak elastyczna jak typ wektora. Wektor to podobny typ kolekcji dostarczany przez standardową bibliotekę, który może rosnąć lub kurczyć się w rozmiarze, ponieważ jego zawartość znajduje się na stercie. Jeśli nie masz pewności, czy użyć tablicy, czy wektora, prawdopodobnie powinieneś użyć wektora. Rozdział 8 omawia wektory bardziej szczegółowo.

Jednak tablice są bardziej przydatne, gdy wiesz, że liczba elementów nie będzie musiała się zmieniać. Na przykład, jeśli używałbyś nazw miesięcy w programie, prawdopodobnie użyłbyś tablicy, a nie wektora, ponieważ wiesz, że zawsze będzie zawierać 12 elementów:

#![allow(unused)]
fn main() {
let months = ["Styczeń", "Luty", "Marzec", "Kwiecień", "Maj", "Czerwiec", "Lipiec",
              "Sierpień", "Wrzesień", "Październik", "Listopad", "Grudzień"];
}

Typ tablicy zapisujesz, używając nawiasów kwadratowych z typem każdego elementu, średnika, a następnie liczby elementów w tablicy, w ten sposób:

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

Tutaj i32 jest typem każdego elementu. Po średniku liczba 5 wskazuje, że tablica zawiera pięć elementów.

Możesz również zainicjalizować tablicę tak, aby zawierała tę samą wartość dla każdego elementu, określając wartość początkową, po której następuje średnik, a następnie długość tablicy w nawiasach kwadratowych, jak pokazano tutaj:

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

Tablica o nazwie a będzie zawierała 5 elementów, z których wszystkie początkowo zostaną ustawione na wartość 3. Jest to to samo, co napisanie let a = [3, 3, 3, 3, 3];, ale w bardziej zwięzły sposób.

Dostęp do elementów tablicy

Tablica to pojedynczy blok pamięci o znanej, stałej wielkości, który może być alokowany na stosie. Możesz uzyskać dostęp do elementów tablicy za pomocą indeksowania, w ten sposób:

Nazwa pliku: src/main.rs

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

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

W tym przykładzie zmienna o nazwie first otrzyma wartość 1, ponieważ jest to wartość pod indeksem [0] w tablicy. Zmienna o nazwie second otrzyma wartość 2 z indeksu [1] w tablicy.

Nieprawidłowy dostęp do elementów tablicy

Zobaczmy, co się stanie, jeśli spróbujesz uzyskać dostęp do elementu tablicy, który wykracza poza jej koniec. Powiedzmy, że uruchamiasz ten kod, podobny do gry w zgadywanie z Rozdziału 2, aby uzyskać indeks tablicy od użytkownika:

Nazwa pliku: 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}");
}

Ten kod kompiluje się pomyślnie. Jeśli uruchomisz ten kod za pomocą cargo run i wprowadzisz 0, 1, 2, 3 lub 4, program wyświetli odpowiednią wartość pod tym indeksem w tablicy. Jeśli zamiast tego wprowadzisz liczbę wykraczającą poza koniec tablicy, taką jak 10, zobaczysz dane wyjściowe podobne do tego:

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

Program spowodował błąd wykonawczy w momencie użycia nieprawidłowej wartości w operacji indeksowania. Program zakończył działanie z komunikatem o błędzie i nie wykonał końcowej instrukcji println!. Kiedy próbujesz uzyskać dostęp do elementu za pomocą indeksowania, Rust sprawdzi, czy podany indeks jest mniejszy niż długość tablicy. Jeśli indeks jest większy lub równy długości, Rust spanikuje. To sprawdzenie musi nastąpić w czasie wykonania, szczególnie w tym przypadku, ponieważ kompilator nie jest w stanie wiedzieć, jaką wartość użytkownik wprowadzi, gdy uruchomi kod później.

Jest to przykład działania zasad bezpieczeństwa pamięci Rusta. W wielu niskopoziomowych językach ten rodzaj sprawdzenia nie jest wykonywany, a gdy podasz nieprawidłowy indeks, można uzyskać dostęp do nieprawidłowej pamięci. Rust chroni Cię przed tego rodzaju błędami, natychmiast kończąc działanie, zamiast pozwalać na dostęp do pamięci i kontynuować. Rozdział 9 omawia więcej o obsłudze błędów w Rust i o tym, jak pisać czytelny, bezpieczny kod, który ani nie panikuje, ani nie pozwala na dostęp do nieprawidłowej pamięci.