Beetje Coderen

Les 3

Variabelen, mutability en typen

Variabelen met let en let mut, waarom ze standaard onveranderlijk zijn, shadowing, constanten, en de scalaire typen van Rust: gehele getallen, kommagetallen, bool en char.

⏱ 45 minuten leestijd + oefenen

🎯 Na deze les kun je

  • variabelen declareren met en zonder mut
  • shadowing uitleggen en toepassen
  • scalaire typen kiezen en expliciet omzetten met as
  • overflow en typefouten herkennen

In de meeste talen kun je een variabele altijd aanpassen. In Rust niet: een variabele is standaard onveranderlijk. Dat lijkt onhandig, maar het maakt code voorspelbaar: als je let prijs = 10; ziet, weet je zeker dat prijs verderop nog steeds 10 is. Wil je wél aanpassen, dan zeg je dat expliciet.

let en let mut

fn main() {
    let x = 5;
    // x = 6;           // fout: cannot assign twice to immutable variable
    let mut teller = 0;
    teller += 1;
    teller += 1;
    println!("{x} {teller}");    // 5 2
}

Probeer de regel met x = 6 en lees de foutmelding: de compiler stelt zelf voor om let mut x te schrijven.

Gebruik mut alleen waar het nodig is. Krijg je de waarschuwing variable does not need to be mutable, haal het dan weg.

Typen en type-inferentie

Elke waarde in Rust heeft een vast type, dat de compiler bij het compileren kent. Meestal leidt hij het zelf af (type-inferentie), maar je kunt het ook opschrijven:

fn main() {
    let a = 42;            // i32 (standaard voor gehele getallen)
    let b: u8 = 200;       // expliciet: 8 bits, zonder teken
    let c = 3.14;          // f64 (standaard voor kommagetallen)
    let d: f32 = 2.5;
    let e = true;          // bool
    let f = 'ß';           // char: één Unicode-teken, 4 bytes
    let g = 1_000_000;     // underscores voor leesbaarheid
    println!("{a} {b} {c} {d} {e} {f} {g}");
}

Gehele getallen

Type Bereik
i8 / u8 −128..127 / 0..255
i16 / u16 −32 768..32 767 / 0..65 535
i32 / u32 ±2,1 miljard / 0..4,3 miljard
i64 / u64 ±9,2 × 10¹⁸ / 0..1,8 × 10¹⁹
isize / usize 64 bits op een 64-bitscomputer

i = met teken (signed), u = zonder (unsigned). usize gebruik je voor indexen en lengtes: vec.len() geeft een usize.

Overflow

Wat gebeurt er bij 255u8 + 1?

fn main() {
    let x: u8 = 255;
    let y = x.wrapping_add(1);      // 0: bewust "rondlopen"
    let z = x.checked_add(1);       // None: geen geldige uitkomst
    let s = x.saturating_add(1);    // 255: blijft op het maximum
    println!("{y} {z:?} {s}");
}

Met gewone + hangt het af van de bouw: in een debugbuild stopt het programma met een panic ("attempt to add with overflow"); in een releasebuild loopt het rond naar 0. Een stille fout wordt dus in elk geval tijdens het testen opgemerkt. Wil je bewust een bepaald gedrag, gebruik dan wrapping_, checked_ of saturating_. (z:? drukt een waarde af met debug-opmaak; meer over Option in les 9.)

Geen automatische omzettingen

fn main() {
    let a: i32 = 10;
    let b: f64 = 2.5;
    // let c = a * b;          // fout: cannot multiply `i32` by `f64`
    let c = a as f64 * b;      // 25.0
    let d = 7 / 2;             // 3: geheeltallige deling
    let e = 7.0 / 2.0;         // 3.5
    let f = -7 % 3;            // -1: rest heeft het teken van het deeltal
    println!("{c} {d} {e} {f}");
}

Rust zet nooit stilletjes een type om. Met as doe je het expliciet. Let op: as kapt af zonder waarschuwing (300i32 as u8 is 44, 3.9 as i32 is 3). Voor veilige omzettingen zijn er try_from en try_into (les 11).

Shadowing

Je kunt een nieuwe variabele met dezelfde naam maken. De oude wordt dan "overschaduwd":

fn main() {
    let invoer = "42";
    let invoer: i32 = invoer.parse().unwrap();
    let invoer = invoer * 2;
    println!("{invoer}");    // 84
}

Dit is iets anders dan mut: elke let maakt een nieuwe variabele, die zelfs een ander type mag hebben. Handig om een waarde stap voor stap om te vormen zonder namen als invoer_tekst en invoer_getal. (.parse().unwrap() zet tekst om naar een getal; wat unwrap doet en waarom je het later vervangt, zie je in les 11.)

Constanten

const BTW_HOOG: f64 = 0.21;
const MAX_POGINGEN: u32 = 3;

fn main() {
    println!("{BTW_HOOG} {MAX_POGINGEN}");
}

Een const heeft altijd een expliciet type, wordt in HOOFDLETTERS geschreven, mag buiten functies staan, en moet bij het compileren bekend zijn.

Een temperatuurconverter

fn main() {
    let celsius = [-40.0, 0.0, 21.5, 100.0];
    for c in celsius {
        let f = c * 9.0 / 5.0 + 32.0;
        println!("{c:>6.1} °C = {f:>6.1} °F");
    }
}
 -40.0 °C =  -40.0 °F
   0.0 °C =   32.0 °F
  21.5 °C =   70.7 °F
 100.0 °C =  212.0 °F

Merk op: 9.0 / 5.0, niet 9 / 5. Met gehele getallen geeft dat 1, en klopt de omrekening niet; met een kommagetal (c) en een geheel getal (9) compileert het niet eens.

Typische fouten

  • cannot assign twice to immutable variable: mut vergeten.
  • mismatched types: geheel getal en kommagetal gemengd. Gebruik 2.0 of as f64.
  • Overflow-panic in debug: kies een groter type of een checked_-variant.
  • as voor onveilige omzettingen zonder het bereik te controleren.

Oefeningen

  1. Wat drukt dit af: let x = 5; let x = x + 1; { let x = x * 2; println!("{x}"); } println!("{x}");?
  2. Welk type kies je voor: (a) de leeftijd van een mens, (b) het aantal bytes in een bestand, (c) een temperatuur met één decimaal?
  3. Schrijf de omrekening de andere kant op: Fahrenheit naar Celsius, voor 0, 50 en 100 °F.
Modelantwoord
  1. 12 en daarna 6. Het binnenste blok maakt een eigen x (12) die alleen binnen het blok bestaat; daarna is de buitenste x (6) weer zichtbaar.
  2. (a) u8 is genoeg (0–255); u16 of u32 mag ook. (b) u64: bestanden kunnen groter dan 4 GB zijn. (c) f64, of een geheel getal in tienden van graden (i32).
  3. fn main() {
        for f in [0.0, 50.0, 100.0] {
            let c = (f - 32.0) * 5.0 / 9.0;
            println!("{f} °F = {c:.1} °C");
        }
    }
    

Lab: temperatuurconverter

  1. Druk een tabel af van −20 tot 40 °C in stappen van 5, met Fahrenheit en Kelvin.
  2. Gebruik een const voor het absolute nulpunt (−273,15).
  3. Geen onnodige mut; cargo build zonder waarschuwingen.
  4. beetje check rust-les03

Samenvatting

  • let is onveranderlijk; let mut mag veranderen.
  • Gehele getallen i8…i64, u8…u64, usize; kommagetallen f32/f64; bool; char.
  • Geen automatische omzetting: as, of veilig met try_from.
  • Overflow geeft in debug een panic; kies bewust wrapping_, checked_ of saturating_.
  • Shadowing maakt een nieuwe variabele met dezelfde naam; const voor vaste waarden.

Volgende les: functies, en waarom bijna alles in Rust een expressie is.

Lab

Temperatuurconverter

💻 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-les3

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-les3

📝 Quiz: variabelen, mutability en typen

6 vragen · 70% nodig om te slagen

1. Wat gebeurt er bij let x = 5; x = 6;?
2. Wat is het standaardtype van let a = 42;?
3. Wat gebeurt er bij 255u8 + 1 in een debugbuild?
4. Hoe vermenigvuldig je een i32 met een f64?
5. Wat is shadowing?
6. Wat is 7 / 2 in Rust (beide gehele getallen)?