Rust – Borrowing Ownership – Pożyczanie własności

W dwóch poprzednich odcinkach cyklu omówiliśmy:

Dziś skupimy się na mechanizmie Borrowing, czyli na pożyczaniu własności.

Po co nam pożyczanie?

Do tej pory pokazałem, że możemy przekazać własność do zmiennej. A co, gdy nie chcemy oddawać własności?

W jakich sytuacjach to może okazać się przydatne? Gdy chcemy nadal w scopie mieć własność, a funkcja potrzebuje tylko wykorzystać tą zmienną do swoich celów bez zmiany właściciela.

Wróćmy do przykładu z książką. To my jesteśmy właścicielem książki. W momencie, w którym nasz kolega chce tylko coś w książce sprawdzić, to możemy mu ją chwilowo pożyczyć. Gdy skończy, to książka wraca w nasze ręce i to na nas spoczywa odpowiedzialność za odłożenie jej na półkę, gdy skończymy czytać.

Różnica między przeniesieniem (Moving), a pożyczaniem (Borrowing) jest bardzo istotna. W pierwszym przypadku nastąpi zmiana właściciela, w drugim wypadku nie.

Dzięki zastosowaniu pożyczenia możliwe jest korzystanie ze zmiennej w obu scopach – zarówno przez właściciela, jak i przez pożyczającego.

Jak to zrobić w kodzie?

Do pożycznia służy operator: & przy operacji przypisania.

fn main() {
   let book: Book = Book {title: String::from("The Title")};
   {
      let book2 = &book;
      println!("Book:{}", book2.title);
   }
   println!("Book:{}", book.title);
}

Uruchom przykład

Specjalnie stworzyłem nowy scope (linia 3 – początek, linia 6 – koniec). W tym zakresie “żyje” zmienna book2.

Powyższy kod powoduje, że zmienna book2 tylko pożycza sobie własność.

“Pożycza”, czyli:

  • Zmienna pożyczająca book2 nie staje się właścicielem zawartości zmiennej (czyli powiązanego z nią fragmentu pamięci),
  • Po wyjściu z zakresu życia zmiennej pożyczającej nie nastąpi zwolnienie segmentu pamięci.
  • Tylko wyjście właściciela (w naszym wypadku zmiennej book) poza zakres użycia jest jednoznaczne ze zwolnieniem segmentu pamięci

Można tu zauważyć kilka różnic między przenoszeniem, a pożyczeniem. Gdybyśmy w linii 4 zrealizowali przeniesienie (gdybyśmy nie użyli operatora &) to kod by się nie skompilował. Pożyczenie sprawia, że możemy swobodnie uzyskać dostęp do zmiennej book bez zmiany właściciela.

Można na ten mechanizm patrzeć jak na tworzenie aliasu do zmiennej.

Wyobraźcie sobie, że pożyczając zmienną do metody, mam możliwość korzystania z niej, ale bez praw związanych z bycia jej właścicielem, wykorzystując do tego ten alias.

Jest to przydatne, gdy chcemy przekazać np: zmienną do funkcji i nadal móc z niej korzystać w macierzystym zakresie bez konieczności przenoszenia własności z powrotem.

Ograniczenia mechanizmu

  • Nie można przenieść własności, jeśli własność została pożyczona wcześniej, do momentu zakończenia pożyczenia.

Poniższy kod zakończy się błędem na poziomie kompilacji:

fn main() {
   let book: Book = Book {title: String::from("The Title")};
   let book2 = &book;
   let book3 = book;
   println!("Book:{}", book.title);
   println!("Book:{}", book2.title);
   println!("Book:{}", book3.title);
}
error[E0505]: cannot move out of `book` because it is borrowed
 --> src/main.rs:7:16
  |
6 |    let book2 = &book;
  |                ----- borrow of `book` occurs here
7 |    let book3 = book;
  |                ^^^^ move out of `book` occurs here
8 |    println!("Book:{}", book.title);
9 |    println!("Book:{}", book2.title);
  |                        ----------- borrow later used here

Uruchom przykład

Jeśli lekko zmodyfikujemy kod i sprawimy, że zmienna pożyczająca (book2) wyjdzie z zakresu – co jest jednoznaczne z zakończeniem wypożyczenia, to kod będzie się kompilował.

fn main() {
    let book: Book = Book {title: String::from("The Title")};
    {
        let book2 = &book;
        println!("Book:{}", book.title);
        println!("Borrowed book:{}", book2.title);
    }
    let book3 = book;
    println!("Book:{}", book3.title);
}

Uruchom przykład

Prawidłowe jest natomiast takie działanie, by najpierw przekazać własność (book -> book2), a potem ją pożyczyć (book2->book3). Po zakończeniu operacji .

fn main() {
   let book: Book = Book {title: String::from("The Title")};
   let book2 = book;
   let book3 = &book2;
   println!("Book:{}", book2.title);
   println!("Book:{}", book3.title);
}

Uruchom przykład

Kolejność wykonywania operacji jest tutaj bardzo istotna i trzeba się zaprzyjaźnić z oboma mechanizmami. Z tego powodu musiałem zakomentować pierwszego println.

Funkcje a pożyczanie

  • Funkcje mogą pożyczać własność od prawowitego właściciela z wykorzystaniem operatora & przy parametrze. Wymaga to także użycia opreatora & przy wywołaniu funkcji.
fn only_borrow(book: &Book) {           
     println!("Book:{}", book.title);
}
fn main() {
   let book: Book = Book {title: String::from("The Title")};
   only_borrow(&book);
   println!("Book:{}", book.title);
}

Uruchom przykład

Ważne jest to, że jeśli pożyczymy własność, to nie możemy jej przenieść. Poniższy kod zakończy się błędem kompilacji.

fn only_borrow(book: &Book) -> Book{           
     println!("Book:{}", book.title);
     book
}
fn main() {
   let book: Book = Book {title: String::from("The Title")};
   only_borrow(&book);
   println!("Book:{}", book.title);
}
Error[E0308]: mismatched types
 --> src/main.rs:6:6
  |
4 | fn only_borrow(book: &Book) -> Book{           
  |                                ---- expected `Book` because of return type
5 |      println!("Book:{}", book.title);
6 |      book
  |      ^^^^ expected struct `Book`, found &Book
  |
  = note: expected type `Book`

Na pierwszy rzut oka może to wyglądać myląco, gdyż informacja mówi o mismatch type. Wynika to z tego, że zmienna jest typu &Book, a my staramy się zwrócić Book.

Gdy bliżej się przyjrzymy, to widać, że staramy się zwrócić pożyczoną zmienną jako własną.

Uruchom przykład Zasady są proste:

  • Funkcje, które pożyczają własność od zmiennej, mogą tylko przekazać prawo do pożyczenia.
  • Funkcje, które mają własność, mogą przekazać własność dalej.

Przekazanie pożyczenia można zauważyć w poniższym kodzie:

fn only_borrow(book: &Book) -> &Book{           
     println!("Borrowed book:{}", book.title);
     book
}
fn main() {
   let book: Book = Book {title: String::from("The Title")};
   let borrowed_book = only_borrow(&book);
   println!("Book:{}", book.title);
   println!("Borrowed book:{}", borrowed_book.title);
}

Uruchom przykład

Jedna rzecz jest godna uwagi. Ważny jest typ zmiennej zwracanej, a nie jej nazwa. Zmienna book jest typu &Book, co oznacza, że jest pożyczającą własność.

Wiszące referencje

W poprzednich artykułach tylko delikatnie zarysowałem problem wiszących referencji. Skoro już wiesz, jak działa borrowing, to pora pokazać, jak Rust sobie radzi z tym problemem.

Wyobraź sobie, że posiadasz książkę, która została Ci pożyczona. W momencie, w którym otwierasz tą książkę by ją przeczytać, okazuje się, że nie ma zawartości. Kolega, który Ci ją pożyczył, zdążył ją odebrać, usunąć lub komuś sprzedać.

Gdy wrócimy na poziom kodu, to powyżej opisana sytuacja odpowiada pożyczeniu komuś prawa do zmiennej, która została usunięta.

Efekt:

Po odwołaniu się do zmiennej możesz natrafić na … No właśnie – na co? Na pewno nie jest to poprawna wartość?

To jest zależne od języka. Mogą to być śmieci, może to być jeszcze nie usunięty obiekt. Duża doza niepewności.

Rust, by wyeliminować tę niepewność, w ogóle nie dopuszcza do takich sytuacji. Na etapie kompilacji ujrzymy stosowny komunikat błędu.

Jak by to wyglądało – w funkcji create_dangling_reference tworzymy zmienną book. Zmienna book jest właścicielem żyjącym w zakresie funkcji.

fn create_dangling_reference() -> &Book{           
    let book: Book = Book {title: String::from("The Title")};
    &book
}
fn main() {
   let book: Book = create_dangling_reference();
   println!("Book:{}", book.title);
}
error[E0106]: missing lifetime specifier
 --> src/main.rs:4:35
  |
4 | fn create_dangling_reference() -> &Book{           
  |                                   ^ help: consider giving it a 'static lifetime: `&'static`
  |
  = help: this function's return type contains a borrowed value, but there is no value for it to be borrowed from

Uruchom przykład

Podsumowanie

Borrowing, czyli pożyczanie, to drugi oprócz przenoszenia (Moving Ownership) mechanizm w języku Rust, zajmujący się zakresem zarządzania pamięcią.

Pożyczając innej zmiennej lub parametrowi funkcji (który też jest zmienną) jakąś własność powodujemy, że możliwe jest użycie pożyczonej zmiennej w innym zakresie. Ale nadal zmienna będąca właścicielem jest odpowiedzialna za ten segment pamięci.

Może istnieć tylko jeden właściciel zmiennej. To pozostaje niezmienne.

Dopiero po wyjściu poza zakres zmiennej, która posiada daną własność, zmienna zostanie usunięta.

Można pożyczyć własność tylko do takiej zmiennej, do której my mamy własność.

Funkcje pożyczają własność na tej samej zasadzie, co zmienne.

W dzień Senior Big Data Architect | Lead Developer | Software Developer w firmie Future Processing, w nocy śpi. Ponad 10 lat doświadczenia w zakresie wytwarzania oprogramowania w różnych technologiach oraz domenach, również w takich, w których nikt nie chciał pracować. Jak trzeba usunąć problem w dowolnej dziedzinie to wiesz do kogo dzwonić :) Zafascynowany rozwojem technologii związanej z przetwarzaniem danych a w szczególności tworzeniem rozwiązań z rodziny Big Data. Prelegent oraz organizator licznych wydarzeń, których głównym celem jest dzielenie się wiedzą oraz krzewienie potrzeby stosowania dobrych praktyk, w celu maksymalizacji jakości wytwarzanego produktu. Współorganizator Wakacyjnych Praktyk w Future Processing oraz prowadzący przedmiot na Politechnice Śląskiej „Tworzenie Oprogramowania w Zmiennym Środowisku Biznesowym”.
PODZIEL SIĘ