Az előző leckében, a „Hello, rustacean!”-ben megírtad az első Rust programodat: találkoztál a fn main függvénnyel, a println! makróval és a kommentekkel. Most itt az ideje, hogy megtanuld, hogyan tárolj adatokat a programodban – hogyan hozz létre változókat, mikor és miért módosíthatod őket, és mi az a Rust-specifikus fogalom, hogy „shadowing”. Ez a lecke azért különösen fontos, mert a Rust egyik legjellemzőbb tulajdonsága, hogy szigorúan megkülönbözteti a módosítható és a nem módosítható adatokat – ez az alapja a néhány lecke múlva tárgyalt ownership-rendszernek is.

A let kulcsszó és a változók

Rustban a let kulcsszóval hozol létre egy változót (pontosabban: egy bindinget, ami egy nevet köt egy értékhez). A fordító a hozzárendelt értékből következteti ki a típust, neked nem kell mindig kiírnod – ez a type inference.

fn main() {
    let name = "Rustacean";
    let favorite_number = 7;

    println!("Szia, {name}! A kedvenc számod: {favorite_number}.");
}

Ami elsőre furcsa lehet más nyelvekből érkezve: egy sima let-tel deklarált változó alapértelmezetten immutable, azaz nem módosítható. Ha megpróbálod utólag megváltoztatni az értékét, a fordító nem engedi.

fn main() {
    let x = 5;
    println!("x = {x}");
    x = 6; // ide figyelj: ez nem fog fordulni!
    println!("x = {x}");
}
error[E0384]: cannot assign twice to immutable variable `x`
 --> src/main.rs:4:5
  |
2 |     let x = 5;
  |         - first assignment to `x`
3 |     println!("x = {x}");
4 |     x = 6;
  |     ^^^^^ cannot assign twice to immutable variable `x`

Ez nem hiba a nyelvben, hanem szándékos védelem: a fordító biztos akar lenni abban, hogy ha egy változót nem jelöltél kifejezetten módosíthatónak, akkor a kódodban sehol nem is fog véletlenül megváltozni az értéke. Ez sokkal könnyebbé teszi a kód átolvasását és a hibák keresését, főleg nagyobb programokban.

Mutability: a mut kulcsszó

Ha azt szeretnéd, hogy egy változó értéke a program futása során módosulhasson, ezt explicit jelezned kell a mut kulcsszóval, közvetlenül a változó neve előtt.

fn main() {
    let mut x = 5;
    println!("x = {x}");

    x = 6;
    println!("x = {x}");
}

Ez a program most simán lefordul és futtatva ezt írja ki:

x = 5
x = 6

A mut kulcsszó nem csak a fordítónak szól, hanem az olvasónak is: aki később megnyitja a kódodat, egy pillantásból látja, hogy ez a változó a program folyamán módosulni fog, míg egy sima let esetén biztos lehet abban, hogy az érték végig ugyanaz marad.

Tipp

Ha egy let mut-tal deklarált változót valójában sosem módosítasz utána, a cargo clippy figyelmeztetni fog rá, hogy a mut felesleges. Érdemes megszokni a cargo clippy futtatását minden projekten.

Shadowing: új változó, régi név

A shadowing (szó szerint „beárnyékolás”) azt jelenti, hogy ugyanazzal a névvel egy teljesen új változót hozol létre egy újabb let segítségével. Ez nem ugyanaz, mint a mut – itt nem módosítod a régi változót, hanem elrejted (shadow-olod) egy másikkal, amely átveheti a nevét.

fn main() {
    let x = 5;
    println!("x = {x}");

    let x = x + 1;
    let x = x * 2;

    println!("x végül = {x}");
}

Ez kiírja: x = 5, majd x végül = 12. Minden let x = ... sor egy teljesen új bindinget hoz létre, amely felhasználhatja az előző x értékét is, mielőtt elrejtené azt.

A shadowing egyik nagy előnye – szemben a mut-tal –, hogy az új változó akár teljesen más jellegű értéket is tárolhat, mint a régi. Nem kell hozzá mut, mert nem módosítasz semmit, csak egy új nevet foglalsz le.

fn main() {
    let guess = "42";
    println!("Beolvasott szöveg: {guess}");

    let guess = 42;
    println!("Számként feldolgozva: {guess}");
}

Ez különösen hasznos olyan helyzetekben, amikor egy adat több lépésben alakul át (például egy beolvasott szöveget később egy kiszámolt értékre váltasz), és nem szeretnél minden lépéshez külön nevet kitalálni (guess_str, guess_num, guess_final...).

Figyelem

A shadowing után a régi érték nem szűnik meg azonnal a memóriában, csak a neve válik elérhetetlenné az adott ponttól. A pontos részletekről, hogy mikor és hogyan szabadulnak fel az értékek, egy későbbi leckében, az ownership tárgyalásakor lesz szó.

Konstansok: a const kulcsszó

A const kulcsszóval konstansokat definiálhatsz. Ezek két fontos ponton különböznek a let-től:

  1. Egy const soha nem lehet mut – ez a nyelv szintjén tilos, nincs kivétel.
  2. A const típusát mindig explicit ki kell írnod, a fordító itt nem következtet.
const MAX_POINTS: u32 = 100_000;

fn main() {
    println!("A maximális pontszám: {MAX_POINTS}");
}
Megjegyzés

A u32 itt egy típusjelölés – arról, hogy pontosan mit jelentenek ezek a típusnevek, a következő leckében, a „Skalár és összetett típusok”-ban tanulsz majd részletesen. Itt csak annyit kell tudnod, hogy egy const-nál mindig kötelező kiírni valamilyen típust.

A konstansok neve hagyományosan SCREAMING_SNAKE_CASE-ben íródik (csupa nagybetű, aláhúzásokkal), ez megkülönbözteti őket a sima változóktól elsőre is, kódolvasás közben.

Egy másik fontos különbség: egy const értékét a fordítónak már fordítási időben (compile time) ismernie kell, nem lehet például egy futásidőben kiszámolt eredmény. A let-tel deklarált változó értéke ezzel szemben bármi lehet, amit a program futása közben ki lehet számolni.

Próbáld ki!

Hozz létre egy új projektet (cargo new valtozok), és írj egy main függvényt, amely:

  1. deklarál egy mut változót energia néven, valamilyen kezdeti értékkel;
  2. kiírja az energia kezdeti értékét egy println!-lel;
  3. módosítja az értéket (pl. csökkenti egy résszel);
  4. újra kiírja az új értéket;
  5. shadowing segítségével létrehoz egy új energia nevű változót, amely már egy szöveges állapotot tárol (pl. "kimerült" vagy "feltöltve"), és ezt is kiírja;
  6. a fájl elején definiál egy const-ot a maximális energiaértékkel, és ezt is beleveszi valamelyik kiírásba.

Futtasd cargo run-nal, és ellenőrizd, hogy a kimenet sorrendje pontosan azt tükrözi, amit a kódban leírtál!

Önellenőrzés

1. Miért nem enged a Rust fordító újraértékadást egy sima let-tel deklarált változóra?

MegoldásMert a `let` alapértelmezetten immutable bindinget hoz létre – a fordító szándékosan megakadályozza a véletlen módosítást, hacsak explicit nem jelzed a `mut` kulcsszóval, hogy a változó értéke a program során változni fog.

2. Mi a különbség a let mut x = ...; x = ...; és a shadowing (let x = ...; let x = ...;) között?

MegoldásA `mut` esetén ugyanaz a változó kap új értéket ugyanazon a helyen. Shadowing esetén egy teljesen új változó jön létre azonos névvel, amely akár egészen más jellegű értéket is tárolhat – a régi érték elérhetetlenné válik (elrejtve, „shadowed”), de nincs szükség `mut`-ra, mert technikailag nem módosítasz semmit, csak új bindinget hozol létre.

3. Miért kötelező megadni egy const típusát, miközben let-nél ez gyakran elmaradhat?

MegoldásA `let`-nél a fordító ki tudja következtetni a típust az értékadás kontextusából (type inference). A `const` esetén a nyelv szabályai szerint nincs ilyen automatikus következtetés – egyrészt mert a konstansok gyakran a program bármely szintjén léteznek, ahol kevesebb kontextus áll rendelkezésre, másrészt mert a fordítási időben rögzített értékhez pontosan definiált típus kell.

Összefoglalás és merre tovább

  • A let alapból immutable változót hoz létre – ez a Rust egyik biztonsági alapköve, amely megakadályozza a véletlen módosításokat.
  • A mut hozzáadásával engedélyezed egy változó utólagos módosítását, és ezzel egyértelműen jelzed a szándékodat a kód olvasóinak is.
  • A shadowing lehetővé teszi, hogy ugyanazt a nevet újrahasználd egy teljesen új binding létrehozásával, akár más jellegű értékkel is – praktikus, ha egy adat több állapoton megy át.
  • A const mindig, kivétel nélkül immutable, típusa mindig explicit, és értéke fordítási időben ismert kell legyen.
  • A következő leckében, a „Skalár és összetett típusok”-ban pontosan megnézzük, milyen fajta értékeket tárolhatnak ezek a változók – például mi az a u32, amit itt már használtunk a konstansban.

További olvasmány: