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

Przechowywanie list wartości za pomocą wektorów

Pierwszym typem kolekcji, który omówimy, jest Vec<T>, znany również jako wektor. Wektory umożliwiają przechowywanie wielu wartości w jednej strukturze danych, która umieszcza wszystkie wartości obok siebie w pamięci. Wektory mogą przechowywać tylko wartości tego samego typu. Są przydatne, gdy masz listę elementów, takich jak linie tekstu w pliku lub ceny przedmiotów w koszyku na zakupy.

Tworzenie nowego wektora

Aby utworzyć nowy, pusty wektor, wywołujemy funkcję Vec::new, jak pokazano w Listing 8-1.

fn main() {
    let v: Vec<i32> = Vec::new();
}

Zauważ, że dodaliśmy tutaj adnotację typu. Ponieważ nie wstawiamy żadnych wartości do tego wektora, Rust nie wie, jakiego rodzaju elementy zamierzamy przechowywać. Jest to ważna uwaga. Wektory są implementowane przy użyciu generyków; jak używać generyków z własnymi typami, omówimy w Rozdziale 10. Na razie wiedz, że typ Vec<T> dostarczany przez standardową bibliotekę może przechowywać dowolny typ. Kiedy tworzymy wektor do przechowywania określonego typu, możemy określić typ w nawiasach ostrych. W Listing 8-1 poinformowaliśmy Rusta, że Vec<T> w v będzie przechowywać elementy typu i32.

Częściej będziesz tworzyć Vec<T> z wartościami początkowymi, a Rust wywnioskuje typ wartości, którą chcesz przechowywać, więc rzadko będziesz musiał dodawać adnotacje typu. Rust wygodnie udostępnia makro vec!, które utworzy nowy wektor przechowujący podane wartości. Listing 8-2 tworzy nowy Vec<i32> przechowujący wartości 1, 2 i 3. Typ liczby całkowitej to i32, ponieważ jest to domyślny typ liczby całkowitej, jak omówiliśmy w sekcji „Typy danych” w Rozdziale 3.

fn main() {
    let v = vec![1, 2, 3];
}

Ponieważ podaliśmy początkowe wartości i32, Rust może wywnioskować, że typ v to Vec<i32>, a adnotacja typu nie jest konieczna. Następnie przyjrzymy się, jak modyfikować wektor.

Aktualizowanie wektora

Aby utworzyć wektor, a następnie dodać do niego elementy, możemy użyć metody push, jak pokazano w Listing 8-3.

fn main() {
    let mut v = Vec::new();

    v.push(5);
    v.push(6);
    v.push(7);
    v.push(8);
}

Jak w przypadku każdej zmiennej, jeśli chcemy móc zmieniać jej wartość, musimy uczynić ją mutowalną za pomocą słowa kluczowego mut, jak omówiono w Rozdziale 3. Liczby, które umieszczamy wewnątrz, są wszystkie typu i32, a Rust wnioskuje to z danych, więc nie potrzebujemy adnotacji Vec<i32>.

Odczytywanie elementów wektorów

Istnieją dwa sposoby odwoływania się do wartości przechowywanej w wektorze: poprzez indeksowanie lub za pomocą metody get. W poniższych przykładach dodaliśmy adnotacje typów wartości zwracanych przez te funkcje dla dodatkowej przejrzystości.

Listing 8-4 pokazuje obie metody dostępu do wartości w wektorze, ze składnią indeksowania i metodą get.

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

    let third: &i32 = &v[2];
    println!("Trzeci element to {third}");

    let third: Option<&i32> = v.get(2);
    match third {
        Some(third) => println!("Trzeci element to {third}"),
        None => println!("Nie ma trzeciego elementu."),
    }
}

Zauważmy kilka szczegółów. Używamy wartości indeksu 2, aby uzyskać trzeci element, ponieważ wektory są indeksowane liczbami, zaczynając od zera. Użycie & i [] daje nam odniesienie do elementu o wartości indeksu. Kiedy używamy metody get z indeksem przekazanym jako argument, otrzymujemy Option<&T>, którego możemy użyć z match.

Rust udostępnia te dwa sposoby odwoływania się do elementu, abyś mógł wybrać, jak program zachowa się, gdy spróbujesz użyć wartości indeksu spoza zakresu istniejących elementów. Na przykład, zobaczmy, co się stanie, gdy mamy wektor pięciu elementów, a następnie spróbujemy uzyskać dostęp do elementu o indeksie 100 za pomocą każdej techniki, jak pokazano w Listing 8-5.

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

    let does_not_exist = &v[100];
    let does_not_exist = v.get(100);
}

Kiedy uruchomimy ten kod, pierwsza metoda [] spowoduje panikę programu, ponieważ odwołuje się do nieistniejącego elementu. Ta metoda jest najlepiej używana, gdy chcesz, aby program uległ awarii, jeśli istnieje próba dostępu do elementu poza końcem wektora.

Jeśli do metody get zostanie przekazany indeks spoza zakresu wektora, zwraca None bez paniki. Użyłbyś tej metody, jeśli dostęp do elementu spoza zakresu wektora może sporadycznie występować w normalnych okolicznościach. Twój kod będzie wtedy zawierał logikę do obsługi Some(&element) lub None, jak omówiono w Rozdziale 6. Na przykład, indeks może pochodzić od osoby wpisującej liczbę. Jeśli przypadkowo wpisze zbyt dużą liczbę, a program otrzyma wartość None, możesz poinformować użytkownika, ile elementów znajduje się w bieżącym wektorze i dać mu kolejną szansę na wprowadzenie prawidłowej wartości. Byłoby to bardziej przyjazne dla użytkownika niż awaria programu z powodu literówki!

Gdy program ma prawidłową referencję, sprawdzanie pożyczek (borrow checker) egzekwuje zasady własności i pożyczania (omówione w Rozdziale 4), aby upewnić się, że ta referencja i wszelkie inne referencje do zawartości wektora pozostają prawidłowe. Przypomnijmy sobie zasadę, która mówi, że nie można mieć jednocześnie mutowalnych i niemutowalnych referencji w tym samym zakresie. Ta zasada ma zastosowanie w Listing 8-6, gdzie trzymamy niemutowalną referencję do pierwszego elementu w wektorze i próbujemy dodać element na końcu. Ten program nie zadziała, jeśli później spróbujemy odwołać się do tego elementu w funkcji.

fn main() {
    let mut v = vec![1, 2, 3, 4, 5];

    let first = &v[0];

    v.push(6);

    println!("Pierwszy element to: {first}");
}

Kompilowanie tego kodu spowoduje następujący błąd:

$ cargo run
   Compiling collections v0.1.0 (file:///projects/collections)
error[E0502]: cannot borrow `v` as mutable because it is also borrowed as immutable
 --> src/main.rs:6:5
  |
4 |     let first = &v[0];
  |                  - immutable borrow occurs here
5 |
6 |     v.push(6);
  |     ^^^^^^^^^ mutable borrow occurs here
7 |
8 |     println!("The first element is: {first}");
  |                                      ----- immutable borrow later used here

For more information about this error, try `rustc --explain E0502`.
error: could not compile `collections` (bin "collections") due to 1 previous error

Kod w Listing 8-6 może wyglądać, jakby powinien działać: Dlaczego referencja do pierwszego elementu miałaby przejmować się zmianami na końcu wektora? Ten błąd wynika ze sposobu działania wektorów: Ponieważ wektory umieszczają wartości obok siebie w pamięci, dodanie nowego elementu na końcu wektora może wymagać alokacji nowej pamięci i skopiowania starych elementów do nowej przestrzeni, jeśli nie ma wystarczająco dużo miejsca, aby umieścić wszystkie elementy obok siebie tam, gdzie wektor jest obecnie przechowywany. W takim przypadku referencja do pierwszego elementu wskazywałaby na zwolnioną pamięć. Zasady pożyczania zapobiegają programom wpadaniu w taką sytuację.

Uwaga: Aby uzyskać więcej informacji na temat szczegółów implementacji typu Vec<T>, zobacz „Rustonomicon”.

Iteracja po wartościach w wektorze

Aby kolejno uzyskiwać dostęp do każdego elementu w wektorze, iterowalibyśmy po wszystkich elementach, zamiast używać indeksów do jednoczesnego dostępu do jednego. Listing 8-7 pokazuje, jak użyć pętli for do uzyskania niemutowalnych referencji do każdego elementu w wektorze wartości i32 i ich wydrukowania.

fn main() {
    let v = vec![100, 32, 57];
    for i in &v {
        println!("{i}");
    }
}

Możemy również iterować po mutowalnych referencjach do każdego elementu w mutowalnym wektorze, aby wprowadzić zmiany do wszystkich elementów. Pętla for w Listing 8-8 doda 50 do każdego elementu.

fn main() {
    let mut v = vec![100, 32, 57];
    for i in &mut v {
        *i += 50;
    }
}

Aby zmienić wartość, do której odwołuje się mutowalna referencja, musimy użyć operatora dereferencji *, aby uzyskać dostęp do wartości w i, zanim będziemy mogli użyć operatora +=. Więcej o operatorze dereferencji omówimy w sekcji „Śledzenie referencji do wartości” w Rozdziale 15.

Iteracja po wektorze, niezależnie od tego, czy jest niemutowalna, czy mutowalna, jest bezpieczna dzięki zasadom sprawdzania pożyczek. Gdybyśmy próbowali wstawiać lub usuwać elementy w treściach pętli for w Listing 8-7 i Listing 8-8, otrzymalibyśmy błąd kompilacji podobny do tego, który otrzymaliśmy w kodzie w Listing 8-6. Referencja do wektora, którą przechowuje pętla for, zapobiega jednoczesnej modyfikacji całego wektora.

Używanie Enum do przechowywania wielu typów

Wektory mogą przechowywać tylko wartości tego samego typu. Może to być niewygodne; z pewnością istnieją przypadki użycia, w których trzeba przechowywać listę elementów różnych typów. Na szczęście warianty enum są zdefiniowane pod tym samym typem enum, więc gdy potrzebujemy jednego typu do reprezentowania elementów różnych typów, możemy zdefiniować i użyć enum!

Na przykład, powiedzmy, że chcemy pobrać wartości z wiersza w arkuszu kalkulacyjnym, w którym niektóre kolumny w wierszu zawierają liczby całkowite, niektóre liczby zmiennoprzecinkowe, a niektóre ciągi znaków. Możemy zdefiniować enum, którego warianty będą przechowywać różne typy wartości, a wszystkie warianty enum będą traktowane jako ten sam typ: typ enum. Następnie możemy utworzyć wektor do przechowywania tego enum i w ten sposób ostatecznie przechowywać różne typy. Zilustrowaliśmy to w Listing 8-9.

fn main() {
    enum SpreadsheetCell {
        Int(i32),
        Float(f64),
        Text(String),
    }

    let row = vec![
        SpreadsheetCell::Int(3),
        SpreadsheetCell::Text(String::from("niebieski")),
        SpreadsheetCell::Float(10.12),
    ];
}

Rust musi znać typy, które będą znajdować się w wektorze w czasie kompilacji, aby dokładnie wiedzieć, ile pamięci na stercie będzie potrzebne do przechowywania każdego elementu. Musimy również wyraźnie określić, jakie typy są dozwolone w tym wektorze. Gdyby Rust pozwalał na przechowywanie dowolnego typu w wektorze, istniałoby ryzyko, że jeden lub więcej typów spowodowałoby błędy w operacjach wykonywanych na elementach wektora. Użycie enum plus wyrażenia match oznacza, że Rust zapewni w czasie kompilacji, że każdy możliwy przypadek zostanie obsłużony, jak omówiono w Rozdziale 6.

Jeśli nie znasz wyczerpującego zestawu typów, które program otrzyma w czasie wykonywania, aby przechowywać w wektorze, technika enum nie zadziała. Zamiast tego możesz użyć obiektu cechy, który omówimy w Rozdziale 18.

Teraz, gdy omówiliśmy niektóre z najczęstszych sposobów użycia wektorów, upewnij się, że zapoznałeś się z dokumentacją API, aby zapoznać się ze wszystkimi wieloma przydatnymi metodami zdefiniowanymi w Vec<T> przez standardową bibliotekę. Na przykład, oprócz push, metoda pop usuwa i zwraca ostatni element.

Usuwanie wektora usuwa jego elementy

Podobnie jak każda inna struct, wektor jest zwalniany, gdy wychodzi poza zasięg, jak to zanotowano w Listing 8-10.

fn main() {
    {
        let v = vec![1, 2, 3, 4];

        // rób coś z v
    } // <- v wychodzi z zasięgu i jest zwalniane tutaj
}

Kiedy wektor zostaje usunięty, cała jego zawartość również zostaje usunięta, co oznacza, że przechowywane w nim liczby całkowite zostaną posprzątane. Sprawdzający pożyczki zapewnia, że wszelkie referencje do zawartości wektora są używane tylko wtedy, gdy sam wektor jest prawidłowy.

Przejdźmy do następnego typu kolekcji: String!