Przejdź do treści
Kurs podstawowy Rust
Podgląd za darmo

Własność i pożyczanie

Cel: to najważniejsza lekcja kursu. Po niej rozumiesz, gdzie żyją dane (stos i sterta), co znaczy, że wartość ma właściciela, czym jest przeniesienie (move), czym różni się Copy od Clone, jak działają referencje & i &mut oraz reguły pożyczania. Potrafisz też przeczytać i naprawić najczęstsze błędy „borrow checkera”: E0382, E0499, E0502, E0106, E0597 i E0507.

Nie przejmuj się, jeśli nie wszystko „kliknie” za pierwszym razem. Własność to pojęcie, którego nie ma w popularnych językach, i każdy uczący się Rusta przechodzi przez etap walki z kompilatorem. Wracaj do tej lekcji, gdy w kolejnych zobaczysz błąd z tej rodziny.

Po co to wszystko?

Każdy program musi jakoś zarządzać pamięcią: przydzielać ją na dane i zwalniać, gdy nie są już potrzebne. Są trzy główne podejścia:

  • Ręczne zarządzanie (C): sam wywołujesz malloc i free. Szybkie, ale łatwo o błąd: zapomniane free to wyciek pamięci, podwójne free albo użycie zwolnionej pamięci to awaria lub luka bezpieczeństwa.
  • Odśmiecacz pamięci, czyli garbage collector (Python, JavaScript, Java, Go): środowisko uruchomieniowe samo co jakiś czas szuka nieużywanych danych i je zwalnia. Wygodne i bezpieczne, ale kosztuje czas procesora i pamięć, a moment zwolnienia jest nieprzewidywalny.
  • Własność (Rust): kompilator na podstawie kilku prostych reguł wie, w którym miejscu kodu dana wartość przestaje być potrzebna, i sam wstawia tam jej zwolnienie. Nie ma odśmiecacza ani ręcznego free, a błędy w stylu C są wykrywane podczas kompilacji.

Ceną jest to, że musisz pisać kod zgodnie z tymi regułami. Poznajmy je.

Stos i sterta

Żeby zrozumieć własność, trzeba wiedzieć, gdzie przechowywane są dane. W trakcie działania program korzysta z dwóch obszarów pamięci.

Stos (stack) przechowuje zmienne lokalne funkcji. Działa jak stos talerzy: przy wywołaniu funkcji na wierzch trafia „ramka” z jej zmiennymi, a po jej zakończeniu cała ramka jest zdejmowana. To bardzo szybkie, ale na stosie mogą się znaleźć tylko dane o rozmiarze znanym w czasie kompilacji: liczby, bool, char, krotki i tablice o stałej długości.

Sterta (heap) służy do danych, których rozmiar poznamy dopiero w trakcie działania programu albo które mogą rosnąć – na przykład tekst wpisany przez użytkownika albo lista, do której dokładamy elementy. Program prosi system o blok pamięci odpowiedniej wielkości i dostaje wskaźnik (adres) do niego. Sam wskaźnik ma stały rozmiar, więc może leżeć na stosie.

Dobrym przykładem jest typ String, czyli tekst, który może rosnąć. Zmienna typu String to na stosie trzy liczby: wskaźnik do danych na stercie, długość i pojemność. Same znaki leżą na stercie:

tekst
        STOS                         STERTA
  s1: ┌──────────┬─────┐
      │ wskaźnik │  ───┼──────►  ┌───┬───┬───┬───┬───┐
      ├──────────┼─────┤         │ h │ e │ j │ k │ a │
      │ długość  │  5  │         └───┴───┴───┴───┴───┘
      ├──────────┼─────┤
      │ pojemność│  5  │
      └──────────┴─────┘

Ktoś musi kiedyś zwolnić ten blok na stercie. Tu właśnie wkracza własność.

Trzy zasady własności

  1. Każda wartość w Ruście ma właściciela – zmienną, do której należy.
  2. W danej chwili wartość ma dokładnie jednego właściciela.
  3. Gdy właściciel wychodzi poza zasięg (scope), wartość jest usuwana (drop), a zajmowana przez nią pamięć – zwalniana.

Zasięg zmiennej to zwykle blok w nawiasach klamrowych, w którym ją zadeklarowano:

Rust
fn main() {
    let a = String::from("zewnętrzny");
    {
        let b = String::from("wewnętrzny");
        println!("{a}, {b}");
    } // tu kończy się zasięg b: tekst "wewnętrzny" zostaje usunięty z pamięci

    println!("{a}");
} // tu kończy się zasięg a: tekst "zewnętrzny" zostaje usunięty
tekst
zewnętrzny, wewnętrzny
zewnętrzny

Kompilator w miejscu każdej zamykającej klamry wstawia wywołanie, które zwalnia pamięć zmiennych kończących tam życie. Działa to podobnie jak destruktor w C++ (wzorzec RAII). Możemy to podejrzeć, definiując typ, który wypisuje komunikat przy usuwaniu. Nie przejmuj się składnią struct i impl Drop – poznasz ją w lekcji 6, teraz liczy się tylko kolejność komunikatów:

Rust
struct Glosny {
    nazwa: &'static str,
}

impl Drop for Glosny {
    fn drop(&mut self) {
        println!("usuwam {}", self.nazwa);
    }
}

fn main() {
    let _x = Glosny { nazwa: "x" };
    {
        let _y = Glosny { nazwa: "y" };
        println!("koniec bloku wewnętrznego");
    }
    let _z = Glosny { nazwa: "z" };
    println!("koniec main");
}
tekst
koniec bloku wewnętrznego
usuwam y
koniec main
usuwam z
usuwam x

Widzisz, że y zostało usunięte na końcu swojego bloku, a x i z na końcu main, w kolejności odwrotnej do utworzenia.

Przeniesienie własności (move)

Co się dzieje, gdy przypiszesz jedną zmienną typu String do drugiej?

Rust
fn main() {
    let s1 = String::from("hejka");
    let s2 = s1;
    println!("{s2}");
}
tekst
hejka

Kopiowana jest tylko część ze stosu (wskaźnik, długość, pojemność). Dane na stercie nie są kopiowane – oba wskaźniki pokazywałyby ten sam blok:

tekst
  s1: [wskaźnik | 5 | 5] ──┐        (s1 jest już nieważne)
                           ├──►  "hejka" na stercie
  s2: [wskaźnik | 5 | 5] ──┘

Gdyby obie zmienne były ważne, to na końcu main obie próbowałyby zwolnić ten sam blok pamięci – to klasyczny błąd „double free” z C. Dlatego Rust po przypisaniu let s2 = s1; uznaje, że własność przeszła z s1 na s2, a s1 przestaje być ważne. Mówimy, że wartość została przeniesiona (moved). W Pythonie czy JS po takim przypisaniu obie zmienne wskazywałyby ten sam obiekt; w Ruście właściciel jest tylko jeden.

Spróbuj użyć s1 po przeniesieniu (kod celowo błędny):

Rust
fn main() {
    let s1 = String::from("hejka");
    let s2 = s1;
    println!("{s1} i {s2}");
}
tekst
error[E0382]: borrow of moved value: `s1`
 --> src/main.rs:4:16
  |
2 |     let s1 = String::from("hejka");
  |         -- move occurs because `s1` has type `String`, which does not implement the `Copy` trait
3 |     let s2 = s1;
  |              -- value moved here
4 |     println!("{s1} i {s2}");
  |                ^^ value borrowed here after move
  |
help: consider cloning the value if the performance cost is acceptable
  |
3 |     let s2 = s1.clone();
  |                ++++++++

For more information about this error, try `rustc --explain E0382`.

To jest E0382, najczęstszy błąd własności. Przeczytajmy go dokładnie:

  1. borrow of moved value: s1 – próbujesz użyć (tutaj: pożyczyć do wypisania) wartości, która została przeniesiona.
  2. Etykieta przy linii 2: move occurs because s1 has type String, which does not implement the Copy trait – dlaczego doszło do przeniesienia: typ String nie jest kopiowany automatycznie (o Copy za chwilę).
  3. Etykieta przy linii 3: value moved here – tu nastąpiło przeniesienie.
  4. Etykieta przy linii 4: value borrowed here after move – a tu jest niedozwolone użycie.
  5. help: consider cloning the value if the performance cost is acceptable – jedna z możliwych poprawek: sklonuj wartość, jeśli koszt kopiowania jest do przyjęcia.

Komunikaty borrow checkera prawie zawsze opowiadają taką historię w kilku krokach: „tu coś się stało z wartością, a tu próbujesz zrobić coś, co się z tym kłóci”. Szukaj etykiet przy kolejnych liniach kodu.

Jak to naprawić? Zależy od tego, czego potrzebujesz: jeśli obie zmienne mają mieć własną kopię tekstu – sklonuj go; jeśli druga zmienna ma tylko „zajrzeć” do tekstu – użyj referencji (za chwilę).

Copy i Clone

Z liczbami tego problemu nie ma:

Rust
fn main() {
    let a = 5;
    let b = a; // kopia: a nadal jest ważne
    println!("a = {a}, b = {b}");
}
tekst
a = 5, b = 5

Typy, których wartości w całości leżą na stosie i dają się tanio skopiować bit po bicie, mają cechę (trait) Copy. Przy przypisaniu lub przekazaniu do funkcji są kopiowane, a oryginał pozostaje ważny. Copy mają:

  • wszystkie liczby całkowite i zmiennoprzecinkowe, bool, char,
  • krotki i tablice, jeśli wszystkie ich elementy są Copy (np. (i32, f64) tak, (i32, String) nie),
  • niemutowalne referencje &T (o nich za chwilę) – dlatego tekst zapisany jako let t = "abc"; (typ &str) możesz swobodnie przypisywać dalej.

String, Vec i inne typy zarządzające pamięcią na stercie nie są Copy. Jeśli naprawdę potrzebujesz niezależnej kopii, wywołaj jawnie metodę clone(). Kopiuje ona także dane na stercie (to tzw. głęboka kopia):

Rust
fn main() {
    let s1 = String::from("hejka");
    let s2 = s1.clone(); // nowy blok na stercie z kopią tekstu
    println!("s1 = {s1}, s2 = {s2}");
}
tekst
s1 = hejka, s2 = hejka

Różnica między nimi:

Copy Clone
Kiedy działa automatycznie, przy każdym przypisaniu i przekazaniu tylko gdy jawnie wywołasz .clone()
Koszt zawsze tani (kopia kilku bajtów) może być drogi (np. kopia dużego tekstu)
Przykłady i32, f64, bool, char, &str String, Vec<T> i prawie wszystkie typy

clone() jest widoczne w kodzie celowo: od razu widać, że w tym miejscu kopiujemy dane. Na etapie nauki śmiało używaj clone(), gdy nie możesz poradzić sobie z błędem własności – to w pełni poprawny kod, tylko czasem mniej wydajny. Z czasem nauczysz się zastępować go referencjami.

Własność a funkcje

Przekazanie wartości do funkcji działa jak przypisanie: typy Copy są kopiowane, a pozostałe – przenoszone do parametru funkcji. Funkcja staje się ich właścicielem i usuwa je na swoim końcu (kod celowo błędny):

Rust
fn wypisz(tekst: String) {
    println!("{tekst}");
} // tu tekst jest usuwany

fn main() {
    let s = String::from("Rust");
    wypisz(s); // własność s przechodzi do funkcji
    wypisz(s); // błąd: s zostało już przeniesione
}
tekst
error[E0382]: use of moved value: `s`
 --> src/main.rs:8:12
  |
6 |     let s = String::from("Rust");
  |         - move occurs because `s` has type `String`, which does not implement the `Copy` trait
7 |     wypisz(s); // własność s przechodzi do funkcji
  |            - value moved here
8 |     wypisz(s); // błąd: s zostało już przeniesione
  |            ^ value used here after move
  |
note: consider changing this parameter type in function `wypisz` to borrow instead if owning the value isn't necessary
 --> src/main.rs:1:18
  |
1 | fn wypisz(tekst: String) {
  |    ------        ^^^^^^ this parameter takes ownership of the value
  |    |
  |    in this function
help: consider cloning the value if the performance cost is acceptable
  |
7 |     wypisz(s.clone()); // własność s przechodzi do funkcji
  |             ++++++++

For more information about this error, try `rustc --explain E0382`.

Komunikat jest tym razem jeszcze bardziej pomocny: w notce note: wskazuje parametr funkcji wypisz i sugeruje, żeby zmienić jego typ tak, by funkcja pożyczała wartość zamiast ją przejmować („consider changing this parameter type … to borrow instead”).

Funkcja może też oddać własność, zwracając wartość:

Rust
fn utworz_powitanie(imie: &str) -> String {
    let mut s = String::from("Cześć, ");
    s.push_str(imie);
    s // własność przechodzi do wywołującego
}

fn main() {
    let powitanie = utworz_powitanie("Ola");
    println!("{powitanie}");
}
tekst
Cześć, Ola

Gdyby jednak każda funkcja, która chce tylko odczytać tekst, musiała go przejmować i oddawać, pisanie kodu byłoby męczące. Dlatego istnieją referencje.

Referencje i pożyczanie

Referencja to „wskaźnik z gwarancjami”: pozwala korzystać z wartości bez przejmowania jej na własność. Tworzysz ją operatorem &, a jej tworzenie nazywa się pożyczaniem (borrowing). Tak jak w życiu: pożyczoną rzecz możesz używać, ale musisz ją oddać, a właściciel dalej jest właścicielem.

Rust
fn dlugosc(s: &String) -> usize {
    s.len()
} // s to tylko referencja, więc nic nie jest tu usuwane

fn main() {
    let tekst = String::from("pożyczanie");
    let n = dlugosc(&tekst); // pożyczamy tekst funkcji
    println!("'{tekst}' ma {n} bajtów"); // tekst nadal należy do nas
}
tekst
'pożyczanie' ma 11 bajtów

(Tekst ma 10 liter, ale 11 bajtów, bo ż w UTF-8 zajmuje dwa bajty – więcej o tym w lekcji 5. Tam też zobaczysz, że parametr lepiej zapisać jako &str zamiast &String – cargo clippy zresztą to podpowie.)

Referencja w pamięci to wskaźnik na zmienną-właściciela:

tekst
  s (w dlugosc): [wskaźnik] ──► tekst: [wskaźnik | 11 | 11] ──► "pożyczanie" na stercie

Referencje utworzone przez & są niemutowalne: przez nie można czytać, ale nie zmieniać (kod celowo błędny):

Rust
fn dopisz(s: &String) {
    s.push_str("!");
}

fn main() {
    let tekst = String::from("hej");
    dopisz(&tekst);
    println!("{tekst}");
}
tekst
error[E0596]: cannot borrow `*s` as mutable, as it is behind a `&` reference
 --> src/main.rs:2:5
  |
2 |     s.push_str("!");
  |     ^ `s` is a `&` reference, so it cannot be borrowed as mutable
  |
help: consider changing this to be a mutable reference
  |
1 | fn dopisz(s: &mut String) {
  |               +++

For more information about this error, try `rustc --explain E0596`.

E0596: cannot borrow *s as mutable, as it is behind a & reference – nie możesz pożyczyć *s (wartości, na którą wskazuje s) do zmiany, bo masz tylko zwykłą referencję &. Podpowiedź jest gotowa: zmień typ na &mut String.

Referencje mutowalne: &mut

Żeby funkcja mogła zmienić pożyczoną wartość, potrzebujesz referencji mutowalnej &mut. Zwróć uwagę, że mut pojawia się w trzech miejscach: przy zmiennej (bo będzie zmieniana), przy pożyczeniu (&mut tekst) i w typie parametru (&mut String):

Rust
fn dopisz(s: &mut String) {
    s.push_str(", świecie");
}

fn zwieksz(x: &mut i32) {
    *x += 1; // * to dereferencja: "wartość, na którą wskazuje x"
}

fn main() {
    let mut tekst = String::from("witaj");
    dopisz(&mut tekst);
    println!("{tekst}");

    let mut licznik = 0;
    zwieksz(&mut licznik);
    zwieksz(&mut licznik);
    println!("licznik = {licznik}");
}
tekst
witaj, świecie
licznik = 2

Operator * (dereferencja) oznacza „wartość pod tym adresem”. Przy liczbie trzeba go napisać wprost (*x += 1), bo x to referencja, a nie liczba. Przy wywoływaniu metod (s.push_str(…)) Rust robi dereferencję automatycznie, więc gwiazdki nie widać.

Te same zasady działają nie tylko w funkcjach – referencję możesz też przechowywać w zmiennej: let r = &tekst; albo let r = &mut tekst;.

Zasady pożyczania

Borrow checker, czyli część kompilatora pilnująca pożyczania, egzekwuje dwie zasady:

  1. W danym momencie możesz mieć albo jedną referencję mutowalną (&mut), albo dowolnie wiele niemutowalnych (&) – nigdy obu rodzajów naraz.
  2. Referencja nie może żyć dłużej niż wartość, na którą wskazuje.

Pierwszą zasadę łatwo zapamiętać jako „wielu czytelników albo jeden pisarz”. Dlaczego jest potrzebna? Jeśli ktoś zmienia dane, a ktoś inny w tym samym czasie je czyta, czytający może zobaczyć dane w połowie zmiany albo – jak zaraz zobaczysz – wskaźnik na pamięć, której już nie ma. W programach wielowątkowych to przepis na wyścig danych (data race). Rust wyklucza takie sytuacje już podczas kompilacji.

Druga zasada gwarantuje, że referencja nigdy nie wskazuje na zwolnioną pamięć, czyli że nie ma „wiszących wskaźników” znanych z C.

E0499: dwie referencje mutowalne naraz

Kod celowo błędny:

Rust
fn main() {
    let mut s = String::from("tekst");
    let r1 = &mut s;
    let r2 = &mut s;
    r1.push_str(" pierwszy");
    r2.push_str(" drugi");
    println!("{s}");
}
tekst
error[E0499]: cannot borrow `s` as mutable more than once at a time
 --> src/main.rs:4:14
  |
3 |     let r1 = &mut s;
  |              ------ first mutable borrow occurs here
4 |     let r2 = &mut s;
  |              ^^^^^^ second mutable borrow occurs here
5 |     r1.push_str(" pierwszy");
  |     -- first borrow later used here

For more information about this error, try `rustc --explain E0499`.

Jak czytać: first mutable borrow occurs here (linia 3) – pierwsze pożyczenie mutowalne; second mutable borrow occurs here (linia 4) – drugie, niedozwolone, bo pierwsze nadal trwa; first borrow later used here (linia 5) – dowód, że pierwsze pożyczenie nadal trwa, bo r1 jest jeszcze używane. Ta trzecia etykieta jest kluczowa: pożyczenie trwa do ostatniego użycia referencji, a nie do końca bloku.

Poprawka: niech pożyczenia się nie nakładają. Skoro r1 nie jest potrzebne po linii 5, wystarczy zmienić kolejność:

Rust
fn main() {
    let mut s = String::from("tekst");
    let r1 = &mut s;
    r1.push_str(" pierwszy"); // ostatnie użycie r1: pożyczenie się kończy
    let r2 = &mut s; // teraz można pożyczyć ponownie
    r2.push_str(" drugi");
    println!("{s}");
}
tekst
tekst pierwszy drugi

E0502: referencja zwykła i mutowalna naraz

To bardzo praktyczny przykład. Używamy tu wektora Vec (rosnącej tablicy, szczegóły w lekcji 5; makro vec![…] tworzy wektor z podanych elementów): bierzemy referencję do pierwszego elementu, a potem dodajemy nowy element (kod celowo błędny):

Rust
fn main() {
    let mut liczby = vec![1, 2, 3];
    let pierwszy = &liczby[0];
    liczby.push(4);
    println!("pierwszy: {pierwszy}");
}
tekst
error[E0502]: cannot borrow `liczby` as mutable because it is also borrowed as immutable
 --> src/main.rs:4:5
  |
3 |     let pierwszy = &liczby[0];
  |                     ------ immutable borrow occurs here
4 |     liczby.push(4);
  |     ^^^^^^^^^^^^^^ mutable borrow occurs here
5 |     println!("pierwszy: {pierwszy}");
  |                          -------- immutable borrow later used here

For more information about this error, try `rustc --explain E0502`.

Komunikat: nie można pożyczyć liczby jako mutowalnego (bo push potrzebuje &mut), skoro jest ono już pożyczone jako niemutowalne (pierwszy) i ta niemutowalna referencja jest dalej używana (println!).

Dlaczego to nie jest czepialstwo? Wektor trzyma elementy w bloku na stercie. Gdy brakuje w nim miejsca, push przydziela nowy, większy blok, przenosi tam elementy i zwalnia stary. Referencja pierwszy wskazywałaby wtedy na zwolnioną pamięć. W C++ taki kod się skompiluje i będzie działał „losowo”; Rust go odrzuca.

Poprawki: użyj referencji przed zmianą wektora albo skopiuj wartość (i32 jest Copy):

Rust
fn main() {
    let mut liczby = vec![1, 2, 3];

    let pierwszy = &liczby[0];
    println!("pierwszy: {pierwszy}"); // ostatnie użycie referencji
    liczby.push(4); // teraz wolno zmieniać

    let pierwszy = liczby[0]; // kopia wartości, a nie referencja
    liczby.push(5);
    println!("pierwszy: {pierwszy}, wszystkie: {liczby:?}");
}
tekst
pierwszy: 1
pierwszy: 1, wszystkie: [1, 2, 3, 4, 5]

Wiszące referencje: E0106 i E0597

W C łatwo zwrócić z funkcji wskaźnik do zmiennej lokalnej, która po zakończeniu funkcji już nie istnieje. Spróbujmy tego w Ruście (kod celowo błędny):

Rust
fn utworz() -> &String {
    let s = String::from("tymczasowy");
    &s
}

fn main() {
    let r = utworz();
    println!("{r}");
}
tekst
error[E0106]: missing lifetime specifier
 --> src/main.rs:1:16
  |
1 | fn utworz() -> &String {
  |                ^ expected named lifetime parameter
  |
  = help: this function's return type contains a borrowed value, but there is no value for it to be borrowed from
help: consider using the `'static` lifetime, but this is uncommon unless you're returning a borrowed value from a `const` or a `static`
  |
1 | fn utworz() -> &'static String {
  |                 +++++++
help: instead, you are more likely to want to return an owned value
  |
1 - fn utworz() -> &String {
1 + fn utworz() -> String {
  |

For more information about this error, try `rustc --explain E0106`.

E0106: missing lifetime specifier – brakuje określenia czasu życia. Czas życia (lifetime) to informacja, jak długo referencja jest ważna. Zwykle kompilator ustala go sam, ale tu nie ma skąd: funkcja nie przyjmuje żadnej referencji, więc zwracana referencja nie może wskazywać na nic, co przeżyje funkcję („there is no value for it to be borrowed from”). Wszystko, co powstało wewnątrz funkcji, zostanie usunięte na jej końcu.

Kompilator proponuje dwie rzeczy. Pierwsza – czas życia 'static – sam opisuje jako rzadko stosowany („uncommon”) i tutaj rzeczywiście nie pomoże. Druga jest właściwa: zwróć wartość posiadaną (owned value), a nie referencję. Gdyby nawet dopisać czas życia, kompilator zgłosiłby kolejny błąd, E0515: cannot return reference to local variable s: zwracasz referencję do danych, których właścicielem jest bieżąca funkcja, a więc do danych, które za chwilę zostaną usunięte.

Poprawka – zwracamy String, a własność po prostu przechodzi do wywołującego:

Rust
fn utworz() -> String {
    String::from("tymczasowy")
}

fn main() {
    let r = utworz();
    println!("{r}");
}
tekst
tymczasowy

Jawne czasy życia (składnia 'a) wykraczają poza ten kurs. Na początek wystarczy zasada: funkcja może zwrócić referencję tylko do czegoś, co dostała z zewnątrz (przez parametr), a nie do własnych zmiennych lokalnych.

Druga zasada pożyczania działa także wewnątrz jednej funkcji. Referencja nie może przeżyć wartości, na którą wskazuje (kod celowo błędny):

Rust
fn main() {
    let r;
    {
        let x = 5;
        r = &x;
    } // x przestaje istnieć
    println!("r = {r}");
}
tekst
error[E0597]: `x` does not live long enough
 --> src/main.rs:5:13
  |
4 |         let x = 5;
  |             - binding `x` declared here
5 |         r = &x;
  |             ^^ borrowed value does not live long enough
6 |     } // x przestaje istnieć
  |     - `x` dropped here while still borrowed
7 |     println!("r = {r}");
  |                    - borrow later used here

For more information about this error, try `rustc --explain E0597`.

E0597: x does not live long enough – x nie żyje wystarczająco długo. Etykiety mówią wszystko: tu pożyczono x, tu x zostało usunięte (dropped here while still borrowed), a tu referencja jest dalej używana.

E0507: nie można przenieść wartości z pożyczonej zawartości

Ten błąd pojawia się często przy pracy z kolekcjami. Chcesz wziąć element wektora napisów (kod celowo błędny):

Rust
fn main() {
    let imiona = vec![String::from("Ola"), String::from("Jan")];
    let pierwsze = imiona[0];
    println!("{pierwsze}");
}
tekst
error[E0507]: cannot move out of index of `Vec<String>`
 --> src/main.rs:3:20
  |
3 |     let pierwsze = imiona[0];
  |                    ^^^^^^^^^ move occurs because value has type `String`, which does not implement the `Copy` trait
  |
help: consider borrowing here
  |
3 |     let pierwsze = &imiona[0];
  |                    +
help: consider cloning the value if the performance cost is acceptable
  |
3 |     let pierwsze = imiona[0].clone();
  |                             ++++++++

For more information about this error, try `rustc --explain E0507`.

let pierwsze = imiona[0]; próbowałoby przenieść napis z wektora do zmiennej, a to zostawiłoby w wektorze „dziurę”. Rust na to nie pozwala. Kompilator podpowiada dwa wyjścia: pożyczyć element (&imiona[0]) albo go sklonować (imiona[0].clone()). Z liczbami ten problem nie występuje, bo są Copy – dlatego w poprzednim przykładzie let pierwszy = liczby[0]; działało.

Jak podchodzić do błędów własności

Gdy kompilator odrzuca kod z błędem własności lub pożyczania:

  1. Przeczytaj etykiety po kolei. Znajdź miejsce, gdzie wartość została przeniesiona lub pożyczona, miejsce konfliktu i miejsce, gdzie pierwsze pożyczenie jest jeszcze używane.
  2. Zadaj sobie pytanie, czy ten kod naprawdę potrzebuje własności. Jeśli funkcja tylko czyta dane – niech przyjmuje &T. Jeśli je zmienia – &mut T. Przejmowanie (T) jest potrzebne, gdy funkcja ma zachować wartość na dłużej (np. zapisać ją w strukturze) albo ją „skonsumować”.
  3. Skróć czas pożyczenia. Często wystarczy zmienić kolejność instrukcji tak, żeby referencja przestała być używana, zanim nastąpi zmiana.
  4. W ostateczności sklonuj. clone() to uczciwe rozwiązanie, szczególnie na początku nauki.

Podsumowanie w porównaniu z innymi językami:

Python / JavaScript C Rust
Kto zwalnia pamięć odśmiecacz, w nieokreślonym momencie programista (free) kompilator wstawia zwolnienie na końcu zasięgu właściciela
b = a dla obiektu obie nazwy wskazują ten sam obiekt kopia wskaźnika, dwa „właścicielskie” wskaźniki przeniesienie: a przestaje być ważne (chyba że typ jest Copy)
Wiszący wskaźnik niemożliwy możliwy, błąd w czasie działania niemożliwy, błąd kompilacji
Zmiana danych, które ktoś czyta dozwolona dozwolona zabroniona przez reguły pożyczania

Ćwiczenia

  1. Napraw ten program na dwa sposoby: raz używając clone(), raz referencji.

    Rust
    fn main() {
        let a = String::from("Rust");
        let b = a;
        println!("{a} i {b}");
    }
    
  2. Zmień funkcję wypisz tak, żeby dało się ją wywołać dwa razy z tą samą zmienną:

    Rust
    fn wypisz(tekst: String) {
        println!("{tekst}");
    }
    
    fn main() {
        let s = String::from("Rust");
        wypisz(s);
        wypisz(s);
    }
    
  3. Napisz funkcję dodaj_wykrzyknik, która przyjmuje &mut String i dopisuje na końcu !. Wywołaj ją dwa razy na tym samym napisie i wypisz wynik.

  4. Napraw program (na dwa sposoby):

    Rust
    fn main() {
        let mut liczby = vec![10, 20, 30];
        let ostatni = &liczby[2];
        liczby.push(40);
        println!("{ostatni}");
    }
    
  5. Bez kompilowania odpowiedz: które z tych zmiennych (x, s, krotka, para) są nadal ważne po przypisaniu? Potem sprawdź kompilatorem.

    Rust
    let x = 5;
    let y = x;
    let s = String::from("a");
    let t = s;
    let krotka = (1, 2.5);
    let k2 = krotka;
    let para = (1, String::from("a"));
    let p2 = para;
    

Rozwiązania

Ćwiczenie 1:

Rust
fn main() {
    // sposób 1: klon – dwa niezależne napisy
    let a = String::from("Rust");
    let b = a.clone();
    println!("{a} i {b}");

    // sposób 2: referencja – b tylko pożycza napis od c
    let c = String::from("Rust");
    let d = &c;
    println!("{c} i {d}");
}
tekst
Rust i Rust
Rust i Rust

Ćwiczenie 2 – funkcja pożycza napis zamiast go przejmować:

Rust
fn wypisz(tekst: &str) {
    println!("{tekst}");
}

fn main() {
    let s = String::from("Rust");
    wypisz(&s);
    wypisz(&s);
}
tekst
Rust
Rust

Użyliśmy &str, a nie &String. Referencję &String Rust sam zamienia na &str, a taka funkcja przyjmie też zwykły literał (wypisz("abc")). Szczegóły w lekcji 5.

Ćwiczenie 3:

Rust
fn dodaj_wykrzyknik(s: &mut String) {
    s.push('!');
}

fn main() {
    let mut tekst = String::from("Uwaga");
    dodaj_wykrzyknik(&mut tekst);
    dodaj_wykrzyknik(&mut tekst);
    println!("{tekst}");
}
tekst
Uwaga!!

(push dodaje pojedynczy znak char, push_str – napis.)

Ćwiczenie 4:

Rust
fn main() {
    // sposób 1: użyj referencji przed zmianą wektora
    let mut liczby = vec![10, 20, 30];
    let ostatni = &liczby[2];
    println!("{ostatni}");
    liczby.push(40);

    // sposób 2: skopiuj wartość (i32 jest Copy)
    let mut liczby2 = vec![10, 20, 30];
    let ostatni2 = liczby2[2];
    liczby2.push(40);
    println!("{ostatni2} {liczby:?} {liczby2:?}");
}
tekst
30
30 [10, 20, 30, 40] [10, 20, 30, 40]

Ćwiczenie 5: ważne pozostają x (liczba jest Copy) i krotka (krotka liczb jest Copy). s i para zostały przeniesione – para zawiera String, więc cała krotka nie jest Copy. Użycie para po przypisaniu daje błąd:

Rust
fn main() {
    let krotka = (1, 2.5);
    let k2 = krotka;
    let para = (1, String::from("a"));
    let p2 = para;
    println!("{krotka:?} {k2:?} {p2:?}"); // w porządku
    println!("{para:?}"); // błąd
}
tekst
error[E0382]: borrow of moved value: `para`
 --> src/main.rs:7:16
  |
4 |     let para = (1, String::from("a"));
  |         ---- move occurs because `para` has type `(i32, String)`, which does not implement the `Copy` trait
5 |     let p2 = para;
  |              ---- value moved here
6 |     println!("{krotka:?} {k2:?} {p2:?}"); // w porządku
7 |     println!("{para:?}"); // błąd
  |                ^^^^ value borrowed here after move
  |
help: consider cloning the value if the performance cost is acceptable
  |
5 |     let p2 = para.clone();
  |                  ++++++++

For more information about this error, try `rustc --explain E0382`.

Podsumowanie

  • Dane o stałym rozmiarze leżą na stosie, dane o zmiennym rozmiarze (String, Vec) – na stercie, a na stosie jest tylko wskaźnik do nich.
  • Każda wartość ma jednego właściciela; gdy właściciel wychodzi poza zasięg, wartość jest usuwana.
  • Przypisanie i przekazanie do funkcji przenosi wartość (move), chyba że typ jest Copy. Niezależną kopię robisz jawnie przez clone().
  • &T pożycza do odczytu, &mut T – do zmiany. Naraz może istnieć jedno &mut albo wiele &, a referencja nie może przeżyć wartości.
  • Pożyczenie trwa do ostatniego użycia referencji – często wystarczy zmienić kolejność instrukcji.
Napisz