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

Referencje i Pożyczanie

Problem z kodem krotki w Listingu 4-5 polega na tym, że musimy zwrócić String do funkcji wywołującej, abyśmy mogli nadal używać String po wywołaniu calculate_length, ponieważ String zostało przeniesione do calculate_length. Zamiast tego możemy dostarczyć referencję do wartości String. Referencja jest jak wskaźnik w tym sensie, że jest to adres, za którym możemy podążać, aby uzyskać dostęp do danych przechowywanych pod tym adresem; te dane są własnością innej zmiennej. W przeciwieństwie do wskaźnika, referencja gwarantuje, że wskazuje na ważną wartość określonego typu przez cały okres życia tej referencji.

Oto jak zdefiniować i użyć funkcji calculate_length, która jako parametr przyjmuje referencję do obiektu zamiast przejmować własność wartości:

fn main() {
    let s1 = String::from("hello");

    let len = calculate_length(&s1);

    println!("Długość '{s1}' wynosi {len}.");
}

fn calculate_length(s: &String) -> usize {
    s.len()
}

Po pierwsze, zauważ, że cały kod krotki w deklaracji zmiennej i wartości zwracanej funkcji zniknął. Po drugie, zauważ, że przekazujemy &s1 do calculate_length i, w jej definicji, przyjmujemy &String zamiast String. Te ampersandy reprezentują referencje i pozwalają odwoływać się do pewnej wartości bez przejmowania jej własności. Rysunek 4-6 przedstawia tę koncepcję.

Trzy tabele: tabela dla s zawiera tylko wskaźnik do tabeli dla s1. Tabela dla s1 zawiera dane stosu dla s1 i wskazuje na dane ciągu na stercie.

Rysunek 4-6: Diagram &String s wskazującego na String s1

Uwaga: Przeciwieństwem referencji za pomocą & jest dereferencjowanie, które jest realizowane za pomocą operatora dereferencji, *. Zobaczymy kilka zastosowań operatora dereferencji w Rozdziale 8 i omówimy szczegóły dereferencji w Rozdziale 15.

Przyjrzyjmy się bliżej wywołaniu funkcji:

fn main() {
    let s1 = String::from("hello");

    let len = calculate_length(&s1);

    println!("Długość '{s1}' wynosi {len}.");
}

fn calculate_length(s: &String) -> usize {
    s.len()
}

Składnia &s1 pozwala nam utworzyć referencję, która odnosi się do wartości s1, ale jej nie posiada. Ponieważ referencja nie jest jej właścicielem, wartość, na którą wskazuje, nie zostanie usunięta, gdy referencja przestanie być używana.

Podobnie, sygnatura funkcji używa & do wskazania, że typ parametru s jest referencją. Dodajmy kilka wyjaśniających adnotacji:

fn main() {
    let s1 = String::from("hello");

    let len = calculate_length(&s1);

    println!("Długość '{s1}' wynosi {len}.");
}

fn calculate_length(s: &String) -> usize { // s jest referencją do String
    s.len()
} // Tutaj s wychodzi z zasięgu. Ale ponieważ s nie posiada własności tego,
  // do czego się odnosi, String nie jest usuwane.

Zasięg, w którym zmienna s jest ważna, jest taki sam jak zasięg każdego parametru funkcji, ale wartość wskazywana przez referencję nie jest usuwana, gdy s przestaje być używane, ponieważ s nie posiada własności. Gdy funkcje mają referencje jako parametry zamiast rzeczywistych wartości, nie będziemy musieli zwracać wartości, aby oddać własność, ponieważ nigdy jej nie posiadaliśmy.

Nazywamy czynność tworzenia referencji pożyczaniem. Jak w prawdziwym życiu, jeśli ktoś posiada coś, możesz to od niego pożyczyć. Kiedy skończysz, musisz to zwrócić. Nie jesteś jego właścicielem.

Więc co się stanie, jeśli spróbujemy zmodyfikować coś, co pożyczamy? Wypróbuj kod z Listingu 4-6. Uwaga, spoiler: To nie działa!

fn main() {
    let s = String::from("hello");

    change(&s);
}

fn change(some_string: &String) {
    some_string.push_str(", world");
}

Oto błąd:

$ cargo run
   Compiling ownership v0.1.0 (file:///projects/ownership)
error[E0596]: cannot borrow `*some_string` as mutable, as it is behind a `&` reference
 --> src/main.rs:8:5
  |
8 |     some_string.push_str(", world");
  |     ^^^^^^^^^^^ `some_string` is a `&` reference, so the data it refers to cannot be borrowed as mutable
  |
help: consider changing this to be a mutable reference
  |
7 | fn change(some_string: &mut String) {
  |                         +++

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

Tak jak zmienne są domyślnie niezmienne, tak samo referencje. Nie wolno nam modyfikować czegoś, do czego mamy referencję.

Mutowalne referencje

Możemy naprawić kod z Listingu 4-6, aby umożliwić modyfikację pożyczonej wartości za pomocą kilku drobnych zmian, które zamiast tego używają mutowalnej referencji:

fn main() {
    let mut s = String::from("hello");

    change(&mut s);
}

fn change(some_string: &mut String) {
    some_string.push_str(", world");
}

Najpierw zmieniamy s na mut. Następnie tworzymy mutowalną referencję za pomocą &mut s w miejscu wywołania funkcji change i aktualizujemy sygnaturę funkcji, aby akceptowała mutowalną referencję za pomocą some_string: &mut String. Dzięki temu bardzo jasno wynika, że funkcja change będzie mutować pożyczoną wartość.

Mutowalne referencje mają jedno duże ograniczenie: jeśli masz mutowalną referencję do wartości, nie możesz mieć żadnych innych referencji do tej wartości. Ten kod, który próbuje utworzyć dwie mutowalne referencje do s, zakończy się niepowodzeniem:

fn main() {
    let mut s = String::from("hello");

    let r1 = &mut s;
    let r2 = &mut s;

    println!("{r1}, {r2}");
}

Oto błąd:

$ cargo run
   Compiling ownership v0.1.0 (file:///projects/ownership)
error[E0499]: cannot borrow `s` as mutable more than once at a time
 --> src/main.rs:5:14
  |
4 |     let r1 = &mut s;
  |              ------ first mutable borrow occurs here
5 |     let r2 = &mut s;
  |              ^^^^^^ second mutable borrow occurs here
6 |
7 |     println!("{r1}, {r2}");
  |                -- first borrow later used here

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

Ten błąd mówi, że ten kod jest nieprawidłowy, ponieważ nie możemy pożyczyć s jako mutowalne więcej niż raz w tym samym czasie. Pierwsze mutowalne pożyczenie następuje w r1 i musi trwać, dopóki nie zostanie użyte w println!, ale między utworzeniem tej mutowalnej referencji a jej użyciem próbowaliśmy utworzyć kolejną mutowalną referencję w r2, która pożycza te same dane co r1.

Ograniczenie zapobiegające równoczesnemu posiadaniu wielu mutowalnych referencji do tych samych danych pozwala na mutację, ale w bardzo kontrolowany sposób. Jest to coś, z czym borykają się nowi Rustaceanowie, ponieważ większość języków pozwala mutować, kiedy tylko chcesz. Korzyścią z tego ograniczenia jest to, że Rust może zapobiegać wyścigom danych w czasie kompilacji. Wyścig danych jest podobny do warunku wyścigu i występuje, gdy zachodzą te trzy zachowania:

  • Dwa lub więcej wskaźników uzyskuje dostęp do tych samych danych w tym samym czasie.
  • Co najmniej jeden ze wskaźników jest używany do zapisu danych.
  • Nie ma mechanizmu synchronizującego dostęp do danych.

Wyścigi danych powodują niezdefiniowane zachowanie i mogą być trudne do zdiagnozowania i naprawienia, gdy próbujesz je śledzić w czasie wykonania; Rust zapobiega temu problemowi, odmawiając kompilacji kodu z wyścigami danych!

Jak zawsze, możemy użyć nawiasów klamrowych do stworzenia nowego zasięgu, co pozwala na wiele mutowalnych referencji, ale nie jednoczesnych:

fn main() {
    let mut s = String::from("hello");

    {
        let r1 = &mut s;
    } // r1 wychodzi z zasięgu tutaj, więc możemy stworzyć nową referencję bez problemów.

    let r2 = &mut s;
}

Rust narzuca podobną zasadę dla łączenia mutowalnych i niemutowalnych referencji. Ten kod powoduje błąd:

fn main() {
    let mut s = String::from("hello");

    let r1 = &s; // bez problemu
    let r2 = &s; // bez problemu
    let r3 = &mut s; // DUŻY PROBLEM

    println!("{r1}, {r2}, and {r3}");
}

Oto błąd:

$ cargo run
   Compiling ownership v0.1.0 (file:///projects/ownership)
error[E0502]: cannot borrow `s` as mutable because it is also borrowed as immutable
 --> src/main.rs:6:14
  |
4 |     let r1 = &s; // no problem
  |              -- immutable borrow occurs here
5 |     let r2 = &s; // no problem
6 |     let r3 = &mut s; // BIG PROBLEM
  |              ^^^^^^ mutable borrow occurs here
7 |
8 |     println!("{r1}, {r2}, and {r3}");
  |                -- immutable borrow later used here

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

Uff! Nie możemy również mieć mutowalnej referencji, gdy mamy niezmienną referencję do tej samej wartości.

Użytkownicy referencji niezmiennej nie spodziewają się, że wartość nagle się zmieni! Jednakże, wiele referencji niezmiennych jest dozwolonych, ponieważ nikt, kto tylko odczytuje dane, nie ma możliwości wpływania na odczytywanie danych przez nikogo innego.

Zauważ, że zasięg referencji rozpoczyna się w miejscu jej wprowadzenia i trwa do ostatniego użycia tej referencji. Na przykład, ten kod skompiluje się, ponieważ ostatnie użycie referencji niezmiennych jest w println!, zanim zostanie wprowadzona referencja mutowalna:

fn main() {
    let mut s = String::from("hello");

    let r1 = &s; // bez problemu
    let r2 = &s; // bez problemu
    println!("{r1} i {r2}");
    // Zmienne r1 i r2 nie będą używane po tym punkcie.

    let r3 = &mut s; // bez problemu
    println!("{r3}");
}

Zasięgi niezmiennych referencji r1 i r2 kończą się po println!, gdzie są ostatnio używane, co następuje przed utworzeniem mutowalnej referencji r3. Te zasięgi nie nakładają się, więc ten kod jest dozwolony: kompilator może stwierdzić, że referencja nie jest już używana w punkcie przed końcem zasięgu.

Chociaż błędy pożyczania mogą czasem frustrować, pamiętaj, że to kompilator Rust wskazuje potencjalny błąd wcześnie (w czasie kompilacji, a nie w czasie wykonania) i pokazuje dokładnie, gdzie leży problem. Wtedy nie musisz szukać przyczyny, dla której twoje dane nie są tym, czego się spodziewałeś.

Zwisające referencje

W językach ze wskaźnikami łatwo jest błędnie utworzyć zwisający wskaźnik — wskaźnik, który odwołuje się do miejsca w pamięci, które mogło zostać przekazane komuś innemu — zwalniając część pamięci, jednocześnie zachowując wskaźnik do tej pamięci. W Rust, natomiast, kompilator gwarantuje, że referencje nigdy nie będą zwisającymi referencjami: jeśli masz referencję do jakichś danych, kompilator zapewni, że dane te nie wyjdą poza zasięg przed referencją do nich.

Spróbujmy utworzyć zwisającą referencję, aby zobaczyć, jak Rust im zapobiega, sygnalizując błąd kompilacji:

fn main() {
    let reference_to_nothing = dangle();
}

fn dangle() -> &String {
    let s = String::from("hello");

    &s
}

Oto błąd:

$ cargo run
   Compiling ownership v0.1.0 (file:///projects/ownership)
error[E0106]: missing lifetime specifier
 --> src/main.rs:5:16
  |
5 | fn dangle() -> &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`
  |
5 | fn dangle() -> &'static String {
  |                 +++++++
help: instead, you are more likely to want to return an owned value
  |
5 - fn dangle() -> &String {
5 + fn dangle() -> String {
  |

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

Ten komunikat o błędzie odnosi się do funkcji, której jeszcze nie omówiliśmy: czasów życia. Szczegółowo omówimy czasy życia w Rozdziale 10. Ale jeśli zignorujesz części dotyczące czasów życia, komunikat zawiera klucz do tego, dlaczego ten kod jest problemem:

typ zwracany przez tę funkcję zawiera wartość pożyczoną, ale nie ma wartości,
z której można ją pożyczyć

Przyjrzyjmy się bliżej, co dokładnie dzieje się na każdym etapie naszego kodu dangle:

fn main() {
    let reference_to_nothing = dangle();
}

fn dangle() -> &String { // dangle zwraca referencję do String

    let s = String::from("hello"); // s to nowy String

    &s // zwracamy referencję do String, s
} // Tutaj s wychodzi z zasięgu i jest usuwane, więc jego pamięć znika.
  // Niebezpieczeństwo!

Ponieważ s jest tworzone wewnątrz dangle, kiedy kod dangle się zakończy, s zostanie dealokowane. Ale próbowaliśmy zwrócić do niego referencję. Oznacza to, że ta referencja wskazywałaby na nieprawidłowy String. To niedobrze! Rust nam na to nie pozwoli.

Rozwiązaniem jest bezpośrednie zwrócenie String:

fn main() {
    let string = no_dangle();
}

fn no_dangle() -> String {
    let s = String::from("hello");

    s
}

To działa bez problemów. Własność zostaje przeniesiona, a nic nie jest dealokowane.

Zasady referencji

Podsumujmy, co omówiliśmy na temat referencji:

  • W dowolnym momencie możesz mieć albo jedną mutowalną referencję albo dowolną liczbę niemutowalnych referencji.
  • Referencje muszą być zawsze ważne.

Następnie przyjrzymy się innemu rodzajowi referencji: wycinkom (slices).