|
Le variabili in un programma servono, in buona sostanza, per contenere dei valori. Fisicamente questi valori sono contenuti in una certa locazione di memoria e vengono individuati tramite dei "nomi", in realtà detti identificatori, scelti dal programmatore. Questi nomi devono seguire certe regole formali. La prima e ovvia è che gli identificatori prescelti non devono essere uguali alle keyword del linguaggio. Esiste un modo per aggirare questo ostacolo, come vedremo, che può essere utile in caso si debba far cooperare il nostro programma scritto in Rust con altri moduli scritti magari in C e che contengono identificatori identici alle keyword di Rust. Oppure nel caso in cui in versione future vengano introdotte delle keyword che tali non sono adesso e vi troviate a cooperare con programmi scritti con vecchie versioni. A parte questi casi che devono essere previsti in fase di design del linguaggio ma sono decisamente rari, tenete presente questa limitazione. Da un punto di vista formale le regole che gestiscono i nomi da attribuire alle variabili, possono essere riassunte come segue:
Il carattere _ ha però anche un paio di usi pratici peculiari che possono risultare utilie in qualche caso. Vediamo il seguente esempio, anticipando l'uso della keyword let che serve, in breve, per definire una variabile, ma lo vedremo in dettaglio tra breve, qui concentriamoci sul concetto di identificatore:
Questo programma è formalmente corretto ma il compilatore emette un warning: warning: unused variable: `x` --> r014.rs:2:9 | 2 | let x = "ciao"; | ^ help: if this is intentional, prefix it with an underscore: `_x` | = note: `#[warn(unused_variables)]` on by default Ho evidenziato le due righe di interesse che indicano: 1) che la variabile x non è utilizzata per alcuno scopo e in effetti è solo definita 2) e qui si vede la bontà del compilatore, vi dice come fare per evitare il warning (un altro avviso interessante è nell'ultima riga del messaggio ma di esso riparleremo a suo tempo) Se provate a modificare il programma indicando nella riga 2 la variabile non x ma _x ovvero con il carattere _ in prima posizione, il messaggio viene silenziato. Ovviamente l'utilità di questa soluzione ha un senso ad esempio quando "parcheggiate" un identificatore per un uso futuro. Sempre a proposito di _ abbiamo detto che, definito da solo come identificatore, non è realmente utilizzabile. In effetti, in questa veste, esso significa "ignora questo valore" e vedremo quando questa particolarità possa risultare utile. Caso particolare, come detto, sono gli identificatori raw. Essi permettono utilizzare le keyword del linguaggio come identificatori. La forma che li distingue è la seguente: r#identificatore vediamo un esempio:
Per capire come il compilatore vede realmente la variabile r#type (che è keyword in altri linguaggi) basta togliere la riga 3 e, compilando otterrete un warning, di cui ormai sapete l'origine, che inizia così: warning: unused variable: `type` Ci sono però 4 keyword che non possono essere usate come identificatori nemmeno se precedute dal prefisso r#; esse sono: crate, self, Self, super. Come detto, le variabili raw le dovrete usare solo in casi abbastanza particolari. DICHIARAZIONE DELLE VARIABILI Abbiamo anticipato poco sopra la keyword let. Essa serve, nella pratica finalizzata ai nostri scopi, per creare una variabile, come abbiamo fatto nell'esempio precedente, ovvero in realtà introduce il binding tra un identificatore ed un valore e quindi permette di attribuire un valore ad una nuova variabile tramite il successivo operatore di assegnazione =. Da un punto di vista logico, let permette di creare quindi un legame tra un identificatore ed un valore. Formalmente: let identificatore = valore In realtà la definizione più corretta dovrebbe essere let pattern = espressione; in quanto a sinistra può esserci qualcosa di più di un singolo identificatore e a destra qualcosa di più di un semplice valore, come vedremo poco più avanti. Ma, per i nostri scopi iniziali, ci rifacciamo alla via più comoda. Quindi, per esempio: let x = "ciao"; let y = 3; sono dichiarazioni valide di variabili. La dichiarazione può avvenire anche per gruppi di variabili (si parla di tuple, ne parleremo tra qualche paragrafo) appunto un pattern sintattico invece di una singola voce. let (var-1, var-2, var-3... var-n) = (valore-1, valore-2, valore-3,... valore-2) esempio let(x, y, x) = (1, 2, "ciao") possiamo evidenziare un altro uso di _ let (_, x) = (1,2); in questo caso il primo valore (ricorderete quanto detto poc'anzi relativamente all'uso di _ da solo come nome di un identificatore) è ignorato e viene preso in considerazione solo il secondo. a destra possiamo trovare espressioni più complesse di varia natura let x = ((4 /2) * 3) o anche tramite valori di ritorno di funzioni, come vedremo in molti esempi futuri. Non si può invece scrivere cose come: let x = (let y = 6); perchè l'assegnazione non è una espressione che restituisce un valore, quindi il compilatore non sa cosa attribuire a x. Il termine "variabili" che abbiamo usato finora per semplicità espositiva, in questi casi è in realtà poco appropriato, almeno nell'accezione comune, perchè le definizioni precedenti attribuiscono certamente, legano, il valore di destra agli elementi di sinistra ma tale valore è immutabile, quindi non possiamo attribuire agli elementi inizializzati un valore diverso da quello iniziale. Quindi una volta attribuito ad esempio il valore 3 a y se scriviamo una ulteriore assegnazione y = 4; riceveremo un chiaro messaggio di errore: error[E0384]: cannot assign twice to immutable variable `y` --> r016.rs:3:5 | 2 | let y = 3; | - | | | first assignment to `y` | help: consider making this binding mutable: `mut y` 3 | y = 4; | ^^^^^ cannot assign twice to immutable variable Anche qui il compilatore non solo è chiaro ma ci propone anche una soluzione, come evidenziato. Si tratta di legare a let un'altra keyword ovvero mut. Essa, come suggerisce il suo stesso nome, serve a rendere modificabile una variabile. let mut identificatore = valore; variabile = nuovovalore; Grazie a mut la nostra variabile diventa a tutti gli effetti modificabile (anche più volte ovviamente).
che stampa il valore 1 a video. Va da sè, che il nuovo valore deve essere coerente per tipologia con quello già attribuito alla variabile, vedremo qui di seguito un chiarimento sull'aspetto relativo ai tipi. Rust ci permette anche di scrivere sequenze di questo genere, che non sono banali in realtà nell'economia del linguaggio: let x = 1; let x = "aaa"; ovviamente le due dichiarazioni possono non essere consecutive, ovvero possono esserci quante istruzioni volete tra di esse, ma quello che è importante notare è che in questa sequenza il valore finale di x è "aaa" mentre il valore 1 precedentemente attribuito non è più raggiungibile. Questo sembra in contrasto con quanto detto relativamente alla immutabilità delle variabili non precedute da mut. In realtà la seconda definizione crea una nuova variabile in un nuovo spazio di memoria, (il precedente può essere riutilizzato), variabile che è completamente indipendente dall'altra. E' una scelta di design del linguaggio che ha i suoi pro e i suoi contro, a mio avviso, nell'ambito di un discorso complesso. Esistono tecniche per ora al di fuori della nostra portata per blindare un valore, in fondo al paragrafo troveremo un primo metodo, valido a certe condizioni. Ogni variabile, come noto, ha un valore che appartiene ad un tipo ben preciso e definito. Questo può essere un intero, un carattere, un valore booleano, ecc, li vedremo tutti. Per la gestione dei tipi Rust prevede l'inferenza di tipo, un potente meccanismo che, sulla base del valore attribuito alla variabile, si incarica di assegnare alla variabile stessa il tipo corretto per quel valore. In pratica se scrivo: let x = 3; il meccanismo di inferenza assegnerà a x il tipo i32, che è il default per gli interi. Sulla base di questo si capisce come l'inizializzazione di una variabile sia importante. Se scriviamo infatti qualcosa come: let x; e basta, il compilatore si fa sentire: error[E0282]: type annotations needed --> r025.rs:2:9 | 2 | let x; | ^ | help: consider giving `x` an explicit type | 2 | let x: /* Type */; | ++++++++++++ sulla base del help incluso nel messaggio potremmo pensare di aggiungere noi il tipo (cosa che è perfettamente lecita, come vedremo di seguito) let x: i32; che il compilatore accetta, pur col solito warning relativo alle variabili non utilizzate. Se però proviamo ad usare la nostra x ad esempio cercando di stamparla a video avremo questo ulteriore messaggio: error[E0381]: used binding `x` isn't initialized --> r027.rs:3:18 | 2 | let x: i32; | - binding declared here but left uninitialized 3 | print!("{}", x); | ^ `x` used here but it isn't initialized anche questo direi piuttosto chiaro, leggendo con attenzione. Si capisce quindi anche che non esiste di norma una inizializzazione di default. Se successivamente alle due forma si aggiunge poi esplicitamente un valore, allora le cose funzionano: let x: i32; // definizione x = 0; // attribuzione del valore iniziale let x; // definizione x = 0; // definizione del valore iniziale e del tipo via inferenza. così va bene. Queste assegnazioni possono essere fatte anche non immediatamente di seguito alla creazione della variabile ma comunque prima di qualsiasi utilizzo. Abbiamo accennato alla possibilità di attribuire manualmente un tipo alle variabili senza lasciar fare alla inferenza. Come visto negli esempi presentati appena sopra, questo è possibile tramite l'operatore : . Quindi formalmente la cosa funziona così: let identificatore: tipo = espressione. l'espressione ovviamente deve essere di tipo coerente col tipo dichiarato. Alcuni tipi permettono di inserire "a forza" un valore di default per il tipo come segue: let x1 : i32 = Default::default(); questo mi pare possa avere senso solo se non vi ricordate quale possa essere tale valore.... Un breve ragionamento, per chi non lo conoscesse, lo dobbiamo fare sul significato di ambito di visibilità (o scope, nella dizione inglese), delle variabili che in questo linguaggio non è dissimile da quanto si incontra in altri. Sostanzialmente si tratta del perimetro entro il quale una variabile esiste e si può interagire con essa. Non è mia intenzione sviscerare un argomento piuttosto intuitivo e non peculiare di Rust ma mostrerò come le cose funzionino secondo logica anche in questo linguaggio (tra quelli che conosco solo i linguaggi V e Zig non ammettono questo meccanismo, forse anche Elm ma non sono sicuro). L'esempio seguente chiarirà un po' le cose:
Questo programma dà come output
a seguito delle istruzioni di stampa che troviamo alle righe 6 e 8 La cosa funziona così in quanto la parentesi alla riga 4 apre un perimetro interno a quello aperto dalla graffa alla riga 2. All'intero del perimetro aperto alla riga 4 abbiamo la definizione di una variabile x che non copre quella definita alla 3 in quanto, appunto, si trova in uno scope, in un ambito, diverso (evidenziato da un diverso colore) che termina alla riga 7 laddove la x definita alla riga 5 cessa di esistere. Anche se si chiamano allo stesso modo le due "x" sono totalmente separate. Eliminando le graffe alle righe 4 e 7 invece, la x ora definita alla riga 5 ricoprirebbe (si parla di "shadowing") quella alla riga 3 ed il risultato dell'esecuzione del programma sarebbe
Abbiamo accennato alla possibilità di blindare un valore ad un certo identificatore senza che questo possa essere toccato. Un modo è quello che uso della keyword const che, come intuibile, introduce un valore costante. Le condizioni per l'uso delle costanti sono: 1) bisogna sapere in fase di scrittura del codice il valore da attribuire alla costante 2) bisogna esplicitare il tipo 3) seppure opzionalmente, è proposto tra le linee guida, l'identificatore della costante dovrebbe essere scritto con lettere tutte maiuscole (come in Python, solo che in quel linguaggio è l'unico sistema per identificare in modo non stringente una costante). Se non lo fate il compilatore emette un warning ma fa il suo lavoro lo stesso. Quindi formalmente l'ideale sarebbe: const NOMECOSTANTE: tipo = valore; esempio: const X: i32 = 4; const ST: String = "ciao" Il tentativo di modificare un valore costante tramite assegnazione diretta o tramite let dà origine ad un errore, come è facile constatare. E' consequenziale che cercare di usare mut su elemento definito costante, vista la sua finalità, originerà un errore. La attribuzione di un valore ad una costante può essere effettuato solo tramite valori costanti, anche sotto forma di espressioni, non tramite variabili pur se inizializzate mentre sono utilizzabili altre costanti const X:i32 = 6 * 3; // va bene let y = 9; const X:i32 = 9 * y; // non compila const Y: i32 = 9; const X: i32 = 9 * Y // va bene Non è possibile effettuare shadowing su una costante quindi una volta definita tramite const non potrete nemmeno usare let per dichiarare una variabile con lo stesso nome. L'errore che ne ricavereste è piuttosto singolare e merita una discussione approfondita che non è oggetto di questo paragrafo. Per cultura, se volete anticipare i tempi, il riferimento è qui: Refutability: Whether a Pattern Might Fail to Match - The Rust Programming Language. |