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); }
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
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); }
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); }
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); }
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); }
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
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.