Co to jest własność?

Ukończone

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, elsei 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.