Beetje Coderen

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.

⏱ 55 minuten leestijd + oefenen

🎯 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, een f64, een bool. 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

  1. Elke waarde heeft een eigenaar (een variabele).
  2. Er is op elk moment precies één eigenaar.
  3. 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 value in een lus: een String in de eerste ronde aan een functie geven, en in de tweede ronde nog eens.
  • Verwachten dat let b = a voor een String kopieert, zoals in Python een tweede verwijzing maakt.
  • Overal .clone(): werkt, maar verbergt een ontwerpprobleem.

Oefeningen

  1. 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}");
    
  2. Herschrijf druk_af zodat het de String teruggeeft, en gebruik dat in main om naam na de aanroep nog te printen.
  3. Waarom is i32 Copy maar String niet?
Modelantwoord
  1. Alleen de laatste regel: s is verhuisd naar t. a en b zijn i32 en dus Copy.
  2. 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}");
    }
    
    Het werkt, maar is omslachtig. In les 6 leen je de tekst gewoon uit.
  3. Een i32 is 4 bytes op de stack; kopiΓ«ren is even goedkoop als verplaatsen, en er is geen heap-geheugen om dubbel vrij te geven. Een String beheert 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.

  1. Repareer ze zonder meer dan één .clone() te gebruiken.
  2. Schrijf bij elke fout in een commentaar welke regel van ownership geschonden werd.
  3. 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

Volg daarna de stappen van de opdracht hierboven. Nog geen Docker? Zie les 14 van Een beetje Linux & Docker. Voor dit lab is er geen live lab in de browser.

Klaar? Controleer je werk met:

$ beetje check rust-les5

πŸ“ Quiz: ownership

5 vragen Β· 70% nodig om te slagen

1. Hoeveel eigenaren heeft een waarde in Rust op een bepaald moment?
2. Wat gebeurt er bij let b = a; als a een String is?
3. Waarom kun je na let y = x; (x een i32) x nog gebruiken?
4. Wanneer wordt het geheugen van een String vrijgegeven?
5. Wat doet clone()?

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.

Cursus kopen en verdergaan