Co to jest własność?
Rust zawiera system własności do zarządzania pamięcią. W czasie kompilacji system własności sprawdza zestaw reguł, aby upewnić się, że funkcje własności umożliwiają uruchamianie programu bez spowalniania.
Aby zrozumieć własność, najpierw przyjrzyjmy się regułom określania zakresu rust i przenieśmy semantyka.
Reguły określania zakresu
W języku Rust, podobnie jak większość innych języków programowania, zmienne są prawidłowe tylko w określonym zakresie. W języku Rust zakresy są często oznaczane za pomocą nawiasów klamrowych {}
. Typowe zakresy obejmują elementy funkcji i if
, else
i match
gałęzie.
Uwaga
W języku Rust "zmienne" są często nazywane "powiązaniami". Dzieje się tak, ponieważ "zmienne" w języku Rust nie są bardzo zmienne — nie zmieniają się one często, ponieważ są one domyślnie niezmienione. Zamiast tego często myślimy o nazwach "powiązanych" z danymi, stąd nazwa "powiązanie". W tym module użyjemy terminów "zmienna" i "wiązanie" zamiennie.
Załóżmy, że mamy zmienną mascot
, która jest ciągiem zdefiniowanym w zakresie:
// `mascot` is not valid and cannot be used here, because it's not yet declared.
{
let mascot = String::from("ferris"); // `mascot` is valid from this point forward.
// do stuff with `mascot`.
}
// this scope is now over, so `mascot` is no longer valid and cannot be used.
Jeśli spróbujemy użyć mascot
poza jej zakresem, wystąpi błąd podobny do następującego przykładu:
{
let mascot = String::from("ferris");
}
println!("{}", mascot);
error[E0425]: cannot find value `mascot` in this scope
--> src/main.rs:5:20
|
5 | println!("{}", mascot);
| ^^^^^^ not found in this scope
Ten przykład można uruchomić w trybie online na placu zabaw dla rust.
Zmienna jest prawidłowa od momentu, w którym jest zadeklarowana do końca tego zakresu.
Własność i upuszczanie
Rust dodaje zwrot akcji do idei zakresów. Za każdym razem, gdy obiekt wykracza poza zakres, jest on "porzucony". Porzucanie zmiennej zwalnia wszystkie zasoby, które są z nią powiązane. W przypadku zmiennych plików plik kończy się zamykany. W przypadku zmiennych, które przydzielono pamięć skojarzona z nimi, pamięć zostanie zwolniona.
W Rust powiązania, które mają rzeczy "skojarzone" z nimi, że będą wolne, gdy powiązanie zostanie porzucone, mówi się do "własnych" tych rzeczy.
W poprzednim przykładzie zmienna jest właścicielem mascot
skojarzonych z nim danych Ciąg. Sam String
jest właścicielem przydzielonej sterty pamięci, która zawiera znaki tego ciągu. Na końcu zakresu mascot
jest "porzucony", String
jego właściciel jest porzucany, a na koniec pamięć, która String
jest właścicielem, jest zwalniana.
{
let mascot = String::from("ferris");
}
// mascot is dropped here. The string data memory will be freed here.
Przenoszenie semantyki
Czasami jednak nie chcemy, aby elementy skojarzone ze zmienną zostały porzucone na końcu zakresu. Zamiast tego chcemy przenieść własność elementu z jednego powiązania do innego.
Najprostszym przykładem jest deklarowanie nowego powiązania:
{
let mascot = String::from("ferris");
// transfer ownership of mascot to the variable ferris.
let ferris = mascot;
}
// ferris is dropped here. The string data memory will be freed here.
Kluczową rzeczą do zrozumienia jest to, że po przeniesieniu własności stara zmienna nie jest już prawidłowa. W poprzednim przykładzie, po przeniesieniu String
własności obiektu z mascot
do ferris
, nie możemy już użyć zmiennej mascot
.
W języku Rust "przenoszenie własności" jest nazywane "przenoszeniem". Innymi słowy, własność String
wartości została przeniesiona z mascot
do ferris
.
Jeśli spróbujemy użyć mascot
polecenia po przeniesieniu String
elementu z mascot
do ferris
, kompilator nie skompiluje naszego kodu:
{
let mascot = String::from("ferris");
let ferris = mascot;
println!("{}", mascot) // We'll try to use mascot after we've moved ownership of the string data from mascot to ferris.
}
error[E0382]: borrow of moved value: `mascot`
--> src/main.rs:4:20
|
2 | let mascot = String::from("ferris");
| ------ move occurs because `mascot` has type `String`, which does not implement the `Copy` trait
3 | let ferris = mascot;
| ------ value moved here
4 | println!("{}", mascot);
| ^^^^^^ value borrowed here after move
Ten wynik jest znany jako błąd kompilowania "użyj po przeniesieniu".
Ważne
W rust tylko jedna rzecz może kiedykolwiek posiadać kawałek danych naraz.
Własność w funkcjach
Przyjrzyjmy się przykładowi ciągu przekazywanego do funkcji jako argumentu. Przekazanie elementu jako argumentu do funkcji powoduje przeniesienie tej funkcji do funkcji.
fn process(input: String) {}
fn caller() {
let s = String::from("Hello, world!");
process(s); // Ownership of the string in `s` moved into `process`
process(s); // Error! ownership already moved.
}
Kompilator skarży się, że wartość s
została przeniesiona.
error[E0382]: use of moved value: `s`
--> src/main.rs:6:13
|
4 | let s = String::from("Hello, world!");
| - move occurs because `s` has type `String`, which does not implement the `Copy` trait
5 | process(s); // Transfers ownership of `s` to `process`
| - value moved here
6 | process(s); // Error! ownership already transferred.
| ^ value used here after move
Jak widać w poprzednim fragmencie kodu, pierwsze wywołanie process
do przeniesienia własności zmiennej s
. Kompilator śledzi własność, więc drugie wywołanie process
powoduje wystąpienie błędu. Po przeniesieniu zasobów nie można już używać poprzedniego właściciela.
Ten wzorzec ma głęboki wpływ na sposób pisania kodu Rust. Jest to kluczowe dla obietnicy bezpieczeństwa pamięci, którą proponuje Rust.
W innych językach String
programowania wartość zmiennej s
można niejawnie skopiować przed przekazaniem do naszej funkcji. Ale w Rust ta akcja się nie dzieje.
W pliku Rust przeniesienie własności (czyli przenoszenie) jest zachowaniem domyślnym.
Kopiowanie zamiast przenoszenia
W poprzednim przykładzie można zauważyć wzmiankę o Copy
cechie w komunikatach o błędach kompilatora (raczej informacyjnego). Nie mówiliśmy jeszcze o cechach, ale wartość, która implementuje Copy
cechę, nie jest przenoszona, ale jest kopiowana.
Przyjrzyjmy się wartości, która implementuje cechę Copy
: u32
. Poniższy kod odzwierciedla nasz uszkodzony kod, ale kompiluje się bez problemu.
fn process(input: u32) {}
fn caller() {
let n = 1u32;
process(n); // Ownership of the number in `n` copied into `process`
process(n); // `n` can be used again because it wasn't moved, it was copied.
}
Proste typy, takie jak liczby, to typy kopii . Implementują cechę Copy
, co oznacza, że są kopiowane, a nie przenoszone. Ta sama akcja występuje w przypadku większości prostych typów. Kopiowanie liczb jest niedrogie, dlatego warto skopiować te wartości. Kopiowanie ciągów lub wektorów lub innych typów złożonych może być kosztowne, więc nie implementują Copy
cech i zamiast tego są przenoszone.
Kopiowanie typów, które nie implementują Copy
Jednym ze sposobów obejścia błędów, które widzieliśmy w poprzednim przykładzie, jest jawne kopiowanie typów przed ich przeniesieniem: nazywane klonowaniem w języku Rust. Wywołanie funkcji duplikuje .clone
pamięć i generuje nową wartość. Nowa wartość jest przenoszona, co oznacza, że stara wartość nadal może być używana.
fn process(s: String) {}
fn main() {
let s = String::from("Hello, world!");
process(s.clone()); // Passing another value, cloned from `s`.
process(s); // s was never moved and so it can still be used.
}
Takie podejście może być przydatne, ale może sprawić, że kod będzie wolniejszy, ponieważ każde wywołanie clone
wykonuje pełną kopię danych. Ta metoda często obejmuje alokacje pamięci lub inne kosztowne operacje. Możemy uniknąć tych kosztów, jeśli "pożyczymy" wartości przy użyciu odwołań. Dowiesz się, jak używać odwołań w następnej lekcji.