Rust - I booleani
I valori di tipo booleano, nella loro semplicità, sono
tra i grandi protagonisti della programmazione. Si tratta tra l'altro,
come evidenziato nel capitolo precedente, di uno dei tipi primitivi,
built-in, del linguaggio. In Rust i valori ammessi sono due, come in molti
altri linguaggi:
* true
*
false
vero e falso. Una variabile
booleana si dichiara nel solito modo:
let b1 = true:
let b2: bool = false;
let mut b3 = true;
non sono ammessi altri valori ad esempio valori 0 e 1 che in
altri linguaggi possono sostituirsi a true e false non sono consentiti. Questo è
un bene. Vedremo comunque un esempio in fine paragrafo che chiarirà
ulteriormente il rapporto tra interi e booleani. Come sempre, affinchè dette
variabili siano modificabili è necessario che sia presente la solita keyword
mut. Annotiamo che dal punto di vista tecnico una variabile booleana
occupa lo spazio di un solo byte. In pratica basterebbe un bit (e in
qualche condizione particolare il compilatore è in grado di ottimizzare usando
un solo bit, come nei tipi complessi, potete informarvi relativamente alle
tecniche di "niche optimization") ma in questo modo è possibile creare dei
puntatori anche verso variabili booleane.
Ora, una variabile dichiarata come
negli esempi precedenti piuttosto raramente vi tornerà utile. Invece, nella
maggioranza dei casi il valore booleano che tratterete sarà il risultato di una
espressione ovvero una operazione di confronto. Vediamo quali
sono gli operatori usati e chiariamo il concetto con un esempio:
| == | uguaglianza |
| > | maggiore |
| < | minore |
| >= | maggiore o uguale |
| <= | minore o uguale |
| != | diverso |
Questi operatori danno origine ad espressioni il cui valore deve essere necessariamente vero o falso. Vediamo il seguente esempio:
Esempio 5.1
fn main() {
println!("{}", 3 > 0);
println!("{}", 2 == 2);
println!("{}", 'a' == 'b');
println!("{}", 3 != 7);
}
che restituisce il seguente output:
| true true false true |
Quando ci occuperemo delle istruzioni di selezione, che possono
presentare naturalmente le più svariate casistiche, vedremo quanto
importanti siano questi operatori.
Sui valori booleani, true e
false, è possibile compiere delle operazioni di carattere logico.
Gli operatori principali sono i seguenti:
- and espresso come
&&
- or espresso come
||
- not espresso come
!
- xor espresso come
^
- uguale espresso come
==
l'ultimo lo abbiamo già incontrato ma ora lo vediamo in una luce diversa. Il comportamento di questi operatori può essere espresso attraverso tabelle specifiche:
and
true and true -> true
true and false -> false
false and true -> false
false and false -> false
or
true or true -> true
true or false -> true
false or true -> true
false or false -> false
not
not true -> false
not false -> true
xor
true xor true -> false
true xor false -> true
false xor true -> true
false xor false -> false
==
true == true -> true
false == true -> false
true == false -> false
false == false -> true
Il tipo bool essendo, come detto, primitivo in Rust implementa 5 trait fondamentali (oltre ad altri ancora): Clone, Copy, Sized, Send, Sync. Cosa questo significhi in dettaglio è materia molto più avanzata, per il momento diciamo che grazie al primo, siamo in grado di effetturare delle copie bit a bit. Il seguente codice crea due variabili booleane la proma è b1 e la seconda b2 che è copia di b1 ma da essa indipendente.
fn main() {
let mut b1 = true;
let mut b2 = b1;
b1 = false;
print!("{}", b2);
}
b2 continua valere "true" mentre b1 è passato a false. Quindi b2 è del
tutto staccato e indipendente da b1.
Abbiamo detto che non è ammesso
che valori numerici siano direttamente usati come booleani. E'
tuttavia possibile effettuare una conversione da booleano ad intero mentre
non è ammesso il contrario:
Esempio 5.2
fn main() {
println!("{}", false as i32);
println!("{}", true as i32);
// println!("{}", 1 as bool); questa non compila
}
se escludiamo la riga commentata abbiamo
| 0 1 |
mentre reintroducendo la 5 si otterrebbe:
error[E0054]: cannot
cast `i32` as `bool`
chiaro e forte. Quindi possiamo
andare dai booleani verso gli interi ma non viceversa.
Per
quanto non utile nel nostro contesto segnalo anche la presenza di un crate
che si chiama as_bool e fornisce un modo
per rappresentare vari tipi in un contesto booleano. Esso si basa su una
serie di regole:
1) I booleani si comportano come
previsto (true è vero, false è falso).
2) Tutti i numeri
non zero sono veri.
3) Le stringhe non vuote sono vere,
mentre le stringhe vuote sono false.
4) Le collezioni
non vuote sono vere, mentre le collezioni vuote sono false.
5)
None è sempre falso, mentre Some è valutato in base al contenuto seguendo le
regole precedenti.
Il suo uso, che necessita di qualche manipolazione
un po' particolare, che fa necessariamente uso del file cargo.toml,
esula un po' da questo momento del nostro percorso ma vediamo comunque un
esempio.
1) lanciamo il comando cargo new asbool
2) il file cargo.toml può venire editato come segue:
[package]
name = "asbool"
version = "0.1.0"
edition = "2024"
[dependencies]
as_bool = "0.1.3"
la parte in rosso è quella che ho inserito manualmente
infine ecco il
codice:
use as_bool::AsBool;
fn main() {
println!("{}", 1.as_bool()); //
true
println!("{}", 0.as_bool()); // false
println!("{}",
true.as_bool());
println!("{}", Some(42).as_bool());
println!("{}",
None::<i32>.as_bool());
}
che, in base alle regole precedenti, restituisce:
| true false true true false |
I booleani internamente sono più complessi di quanto sembri e su di essi è possibile effettuare ulteriori manipolazioni (per fare un esempio i classici BitAnd e BitOr grazie ai trait omologhi che questo tipo implementa e che sui booleani agiscono come operatori logici) che però esulano dalla finalità di base di questo paragrafo. Vale però la pena fare una osservazione interessante:
Esempio 5.3
fn main() {
let v = vec![1,2,3];
let i = 10;
if (i < v.len()) && (v[i] == 3) {
println!("ok");
}
else {
println!("not ok");
}
}
Questo programma compila tranquillamente e
restituisce un "not ok" in quanto le condizioni non sono verificate. Se
però al posto dell'operatore && mettete un BitAnd ovvero & ne otterrete:
thread 'main' (25288) panicked at
r064.rs:5:26:
index out of bounds: the len is 3 but the
index is 10
note: run with `RUST_BACKTRACE=1` environment
variable to display a backtrace
questo perchè && verifica la prima
condizione e trovandola non verificata non valuta la seconda
mentre & le valuta entrambe e a quel punto risulta che
v[i] ovvero v[10] non esiste perchè l'indice è fuori range. Questo tipo
di valutazione "totale" vale anche per BitOr.