Dlaczego?
Ownership w Rust jest konceptem, który determinuje w jaki sposób kontrolowana jest pamięć.
Wyobraźmy sobie że alokujemy obiekty w pamięci — w językach z garbage collectorem sprawa jest prosta: GC skanuje które obiekty są nieosiągalne i zwalnia zasoby.
…ale Rust nie posiada garbage collectora. Bez żadnych zasad moglibyśmy alokować i alokować aż skończyłaby nam się nieoczekiwanie pamięć i program by się nieoczekiwanie zakończył — i tutaj zaczyna się Ownership.
fn main() {
let first_owner = String::from("Book");
let second_owner = first_owner;
println!("{}", first_owner);
println!("{}", second_owner);
} error[E0382]: borrow of moved value: `first_owner`
--> src/main.rs:4:20
|
2 | let first_owner = String::from("Book");
| ----------- move occurs because `first_owner` has type `String`, which does not implement the `Copy` trait
3 | let second_owner = first_owner;
| ----------- value moved here
4 | println!("{}", first_owner);
| ^^^^^^^^^^^ value borrowed here after move
|
help: consider cloning the value if the performance cost is acceptable
|
3 | let second_owner = first_owner.clone();
| ++++++++
For more information about this error, try `rustc --explain E0382`.
error: could not compile `playground` (bin "playground") due to 1 previous error
Jak widać - ledwo zdążyliśmy cokolwiek napisać i już Rust marudzi - ale to zamierzone!
Po usunięciu pierwszego println!() (możesz sam to zrobić tutaj :)) - kod uruchamia nam się bez problemu.
Dlaczego?
Ponieważ każda wartość ma swojego właściciela, i przez przypisanie jej do następnej zmiennej również unieważniliśmy pierwszą.
Problem czy feature?
Aby dokładniej zobrazować problem napiszmy coś podobnego ale w języku C
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main() {
char *s = malloc(strlen("Rust") + 1);
strcpy(s, "Rust");
free(s);
printf("%s\n", s);
return 0;
} // random buffer output after free
Jak to ma się do Rusta?
Na załączonym przykładzie w języku C widzimy że byliśmy w stanie zwolnić pamięć przed wywołaniem tworząc potencjalny exploit lub prowadząc do undefined behavior.
W C nie ma ownership więc kto jest właścicielem s ?
Nikt, to tylko wskaźnik.
Na programiście spoczywa cała odpowiedzialność aby być pewnym że w odpowiednim momencie zwalnia zasoby nie tworząc okazji do remote code execution!
To jest kategoria z CVE z wysokim CVSS score i byliśmy świadkami setek takich bugów. Rust eliminuje to statycznie - program się nie skompiluje zamiast działać przez przypadek.
Transferowanie ownership przez funkcje
Spróbujmy teraz przekazać ownership przez funkcje
fn main() {
let first_owner = String::from("Book");
takes_ownership(first_owner);
}
fn takes_ownership(str: String) {
println!("takes_ownership: {}", str);
} takes_ownership: BookJak widać, nasz program bez problemu się wywołuje printując zawartość, dodajmy zatem println! aby wyprintować first_owner
fn main() {
let first_owner = String::from("Book");
takes_ownership(first_owner);
println!("first owner: {}", first_owner);
}
fn takes_ownership(str: String) {
println!("takes_ownership: {}", str);
} error[E0382]: borrow of moved value: `first_owner`
--> src/main.rs:4:33
|
2 | let first_owner = String::from("Book");
| ----------- move occurs because `first_owner` has type `String`, which does not implement the `Copy` trait
3 | takes_ownership(first_owner);
| ----------- value moved here
4 | println!("first owner: {}", first_owner);
| ^^^^^^^^^^^ value borrowed here after move
|
note: consider changing this parameter type in function `takes_ownership` to borrow instead if owning the value isn't necessary
--> src/main.rs:7:25
|
7 | fn takes_ownership(str: String) {
| --------------- ^^^^^^ this parameter takes ownership of the value
| |
| in this function
help: consider cloning the value if the performance cost is acceptable
|
3 | takes_ownership(first_owner.clone());
| ++++++++
For more information about this error, try `rustc --explain E0382`.
error: could not compile `playground` (bin "playground") due to 1 previous errorRust znowu nie pozwala nam się skompilować! - Ownership został przekazany do funkcji takes_ownership.
Niewidzialny Drop
Już wiemy że w Rust istnieje pojedynczy ownership — skompilujmy nasz program i zajrzyjmy na wynikowy assembler przez objdump:
objdump -d -M intel target/debug/ownership | grep -A 20 "takes_ownership"
===============================================================
2318b: mov rsi, [rsp+0x8] ← drugi argument dla _print (len)
========= println implementation ============
2319e: jmp 231a0 ← skocz do dropu
231a0: mov rdi, [rsp+0x18] ← wskaźnik do String (argument dla drop)
231a5: call drop_in_place ← drop za }
231aa: add rsp, 0x58 ← przywróć stos
231ae: ret ← powrót z funkcji
W świecie języków z garbage collectorem GC sprawdza czy obiekty są poza zasięgiem i zwalnia zasoby.
W przypadku Rust kompilator wstrzykuje zwalnianie zasobów w momencie zakończenia czasu życia funkcji — w tym przypadku }.
fn main() {
let first_owner = String::from("Book"); //_move - przenieś właściciela na takes_ownership
takes_ownership(first_owner);
println!("first owner: {}", first_owner);
}
fn takes_ownership(str: String) {
println!("takes_ownership: {}", str);
} // wstrzyknięcie _drop
Jest to analogiczny przykład wywołania destruktora (~MyClass()) jak w przypadku C++ — z tą różnicą, że Rust gwarantuje wywołanie dokładnie raz przez system ownership, podczas gdy w C++ zależy to od dyscypliny programisty.
Dodatkowo Rust umożliwia implementację trait Drop dzięki któremu jeśli wartość wychodzi poza scope wywoływany jest drop.
Użyjmy mechanizmu otwierania plików z std
use std::fs::File;
fn main() {
{
let f = File::open("/etc/hostname").unwrap();
println!("plik otwarty");
} // close(fd) tutaj
println!("plik już zamknięty");
}
impl Drop for OwnedFd {
fn drop(&mut self) {
unsafe {
let _ = libc::close(self.fd.as_inner()); // ← syscall close(fd)
}
}
}
libc::close jest wywoływany automatycznie - dzieje się to under the hood!
Aby oddać ownership wystarczy je z powrotem zwrócić
fn takes_ownership(str: String) {
println!("takes_ownership: {}", str);
str // oddaje ownership z powrotem
}
Copy: kiedy transfer ownership staje się kopią
fn main() {
let first_owner:i32 = 10;
takes_ownership(first_owner);
println!("first_owner: {}", first_owner);
}
fn takes_ownership(num: i32) {
println!("takes_ownership: {}", num);
}Porównując to z naszym pierwszym przykładem tutaj Rust powinien krzyczeć a zamiast tego ładnie się skompilował.
use std::fs::File;
fn main() {
println!("i32: {}", std::mem::needs_drop::<i32>());
println!("f64: {}", std::mem::needs_drop::<f64>());
println!("bool: {}", std::mem::needs_drop::<bool>());
println!("char: {}", std::mem::needs_drop::<char>());
println!("String: {}", std::mem::needs_drop::<String>());
println!("Vec: {}", std::mem::needs_drop::<Vec<i32>>());
println!("File: {}", std::mem::needs_drop::<File>());
println!("Box: {}", std::mem::needs_drop::<Box<i32>>());
println!("(i32, i32): {}", std::mem::needs_drop::<(i32, i32)>());
println!("(i32, String): {}", std::mem::needs_drop::<(i32, String)>());
println!("[i32; 3]: {}", std::mem::needs_drop::<[i32; 3]>());
}i32: false
f64: false
bool: false
char: false
String: true
Vec: true
File: true
Box: true
(i32, i32): false
(i32, String): true
[i32; 3]: falseJak widać - dla typów prymitywnych Rust zamiast przenosić ownership tworzy kopię. Są to typy w całości przechowywane na stosie — kopia to dosłownie kilka bajtów, bez żadnych skutków ubocznych. Nie ma alokacji, wskaźników.
Dwie kopie są całkowicie niezależne.
Rust implementuje trait który mówi kompilatorowi że ten typ można kopiować bitowo np:
#[derive(Copy, Clone)]
struct Point {
x: f64,
y: f64,
}
Ma to sens tylko gdy typ jest w całości na stosie i nie zarządza żadnymi zasobami (np. pamięcią heap, deskryptorami plików).
Dlatego String nie może implementować Copy — posiada wewnętrzny bufor na stercie.
Ale kod czasami wymaga nie tylko przenoszenia ownership, ale też wielokrotnego dostępu do tej samej wartości z różnych miejsc.
Do tego służy Borrowing, który będzie tematem następnego posta - Stay Tuned!

