Les 5
Ownership
Het kernidee van Rust: elke waarde heeft één eigenaar, eigendom kan verhuizen (move), sommige typen worden gekopieerd (Copy), en geheugen wordt vrijgegeven als de eigenaar verdwijnt.
π― Na deze les kun je
- de drie regels van ownership uitleggen
- het verschil tussen stack en heap globaal beschrijven
- move en copy onderscheiden, en clone bewust gebruiken
- ownership-fouten van de compiler lezen en oplossen
Elke taal moet geheugen opruimen. C laat het aan jou over (free, met lekken en crashes als gevolg). Python, Java en JavaScript gebruiken een garbage collector die af en toe kijkt wat niet meer gebruikt wordt. Rust doet iets anders: de compiler bepaalt precies wanneer geheugen vrijkomt, aan de hand van een paar eenvoudige regels. Dat heet ownership. Geen garbage collector, geen handmatig free, en toch veilig. Het is ook het onderdeel van Rust waar beginners het meest tegenaan lopen, dus neem er de tijd voor.
Stack en heap, kort
- Op de stack staan waarden met een vaste, bekende grootte: een
i32, eenf64, eenbool. Snel, en automatisch weg als de functie eindigt. - Op de heap staan gegevens waarvan de grootte pas tijdens het draaien bekend is, of kan groeien, zoals de tekst in een
String. Iemand moet die ruimte aanvragen en later weer vrijgeven.
Een String bestaat uit twee delen: op de stack een klein "kaartje" (pointer, lengte, capaciteit), en op de heap de tekens zelf.
stack: s heap
ββββββββββββ¬ββββ βββββ¬ββββ¬ββββ¬ββββ¬ββββ
β pointer ββΌββββΌββββββββββββΆβ h β a β l β l β o β
β lengte 5 β β βββββ΄ββββ΄ββββ΄ββββ΄ββββ
β capac. 5 β β
ββββββββββββ΄ββββ
De drie regels
- Elke waarde heeft een eigenaar (een variabele).
- Er is op elk moment precies één eigenaar.
- Als de eigenaar buiten bereik (scope) gaat, wordt de waarde opgeruimd (dropped).
fn main() {
{
let s = String::from("hallo"); // s is eigenaar
println!("{s}");
} // s gaat buiten bereik: geheugen vrij
// println!("{s}"); // fout: s bestaat hier niet meer
}
Move
Wat gebeurt er hier?
fn main() {
let a = String::from("hallo");
let b = a;
println!("{b}");
// println!("{a}"); // fout!
}
Als b = a de heap-tekst zou kopiΓ«ren, zou dat duur zijn. Als het alleen het kaartje kopieert, wijzen a en b naar dezelfde tekst, en zouden ze aan het eind allebei proberen die vrij te geven (een double free, een klassieke bron van crashes in C). Rust kiest een derde weg: het eigendom verhuist (move) naar b. a is daarna ongeldig, en de compiler laat je hem niet meer gebruiken:
error[E0382]: borrow of moved value: `a`
--> src/main.rs:4:15
|
2 | let a = String::from("hallo");
| - move occurs because `a` has type `String`, which does not implement the `Copy` trait
3 | let b = a;
| - value moved here
4 | println!("{a}");
| ^^^ value borrowed here after move
|
help: consider cloning the value if the performance cost is acceptable
|
3 | let b = a.clone();
| ++++++++
Lees de melding van boven naar beneden: waar de waarde vandaan komt, waar hij verhuisd is, en waar je hem daarna nog probeerde te gebruiken. Plus een suggestie.
Move bij functies
Een waarde meegeven aan een functie is ook een move:
fn druk_af(tekst: String) {
println!("{tekst}");
} // tekst wordt hier opgeruimd
fn main() {
let naam = String::from("Fatima");
druk_af(naam);
// druk_af(naam); // fout: naam is verhuisd
}
En teruggeven verhuist het eigendom terug naar de aanroeper:
fn maak_groet(naam: &str) -> String {
let mut s = String::from("Hallo, ");
s.push_str(naam);
s // eigendom gaat naar de aanroeper
}
fn main() {
let g = maak_groet("Tim");
println!("{g}");
}
Het is onhandig als je een waarde kwijt bent zodra je hem aan een functie geeft. De oplossing is lenen (borrowing), in les 6. Hier zie je eerst de basis.
Copy
Bij getallen werkt het anders:
fn main() {
let x = 5;
let y = x;
println!("{x} {y}"); // prima
}
Typen die helemaal op de stack staan en goedkoop te kopiΓ«ren zijn, implementeren de trait Copy: bij toewijzen wordt de waarde gekopieerd en blijft het origineel bruikbaar. Dat geldt voor alle getaltypen, bool, char, en tuples en arrays daarvan. String, Vec en alles met heap-geheugen zijn niet Copy.
clone
Wil je echt een tweede, onafhankelijke kopie, vraag er dan expliciet om:
fn main() {
let a = String::from("hallo");
let mut b = a.clone();
b.push('!');
println!("{a} {b}"); // hallo hallo!
}
clone() kopieert ook de heap-gegevens. Dat is correct, maar kost tijd en geheugen. Het staat expliciet in de code, zodat je het ziet als het in een lus gebeurt. Beginners lossen elke ownership-fout op met .clone(); dat werkt, maar meestal is lenen (les 6) de betere oplossing.
Wanneer wordt er opgeruimd?
Precies op het einde van de scope van de eigenaar, in omgekeerde volgorde van aanmaken. Dit heet RAII en geldt niet alleen voor geheugen, maar ook voor bestanden, netwerkverbindingen en locks: een geopend bestand wordt automatisch gesloten als de variabele verdwijnt (les 34).
Typische fouten
borrow of moved value: je gebruikt een variabele na een move. Leen in plaats daarvan, of clone bewust.use of moved valuein een lus: eenStringin de eerste ronde aan een functie geven, en in de tweede ronde nog eens.- Verwachten dat
let b = avoor eenStringkopieert, zoals in Python een tweede verwijzing maakt. - Overal
.clone(): werkt, maar verbergt een ontwerpprobleem.
Oefeningen
- Welke regels compileren niet, en waarom?
let a = 10; let b = a; let s = String::from("x"); let t = s; println!("{a} {b}"); println!("{s} {t}"); - Herschrijf
druk_afzodat het deStringteruggeeft, en gebruik dat inmainomnaamna de aanroep nog te printen. - Waarom is
i32CopymaarStringniet?
Modelantwoord
- Alleen de laatste regel:
sis verhuisd naart.aenbzijni32en dusCopy. -
Het werkt, maar is omslachtig. In les 6 leen je de tekst gewoon uit.fn druk_af(tekst: String) -> String { println!("{tekst}"); tekst } fn main() { let naam = String::from("Fatima"); let naam = druk_af(naam); println!("nog steeds: {naam}"); } - Een
i32is 4 bytes op de stack; kopiΓ«ren is even goedkoop als verplaatsen, en er is geen heap-geheugen om dubbel vrij te geven. EenStringbeheert heap-geheugen; een stille kopie zou dat geheugen dubbel bezitten (of duur moeten kopiΓ«ren).
Lab: strings doorgeven zonder fouten
In het lab staat een programma met vijf ownership-fouten.
- Repareer ze zonder meer dan één
.clone()te gebruiken. - Schrijf bij elke fout in een commentaar welke regel van ownership geschonden werd.
beetje check rust-les05
Samenvatting
- Elke waarde heeft één eigenaar; als die verdwijnt, wordt de waarde opgeruimd.
- Toewijzen of meegeven aan een functie verplaatst (move) niet-
Copy-waarden; het origineel is daarna ongeldig. Copy-typen (getallen,bool,char) worden gekopieerd.clone()maakt bewust een volledige kopie; gebruik het met mate.
Volgende les: waarden uitlenen zonder het eigendom op te geven.
Lab
Strings doorgeven zonder fouten
π» Dit lab doe je op je eigen computer met Docker. Start de lab-container met (geeft de opdracht een eigen startcommando, gebruik dan dat):
$ docker run -it --rm registry.wizg.it/webhosting/beetje-labs:rust-les5
Klaar? Controleer je werk met:
$ beetje check rust-les5
π Quiz: ownership
Dit was de laatste gratis les
Je hebt les 1β5 van Een beetje Rust gratis gevolgd. Wil je verder? Met één aankoop (€ 29,00, eenmalig) krijg je de overige 30 lessen, de live labs en het certificaat. Geen abonnement.