C#

Tipi valore: introduzione + booleani


Ed eccoci quindi a parlare di questo prima parte dei tipi utilizzabili in C#, anche se buona parte di quanto troverete qui è pienamente applicabile a tutti i linguaggi .Net compliant. I tipi valore sono quelli forse più facilmente comprensibili anche se come vedremo c'è una buona quantità di teoria che li accompagna. In gran parte potrete anche dimenticarla mentre programmate... anche se credo invece sia buona cosa tenerla sempre a portata di mano.
Da un punto di vista architetturale, in termini di classi, ci troviamo in questa situazione, come abbiamo già visto:

System.Object
|
System.TypeValue


Object è la radice di tutto e TypeValue è una classe che deriva direttamente da essa. Da buona classe ha i suoi metodi che in pratica coincidono con quelli propri della classe Object e visti nel paragrafo precedente, tranne quelli statici. Ripetiamo quindi la tabella già vista:

Metodo Modificatore Uso
Equals() Pubblico Supporta il confronto accettando uno o due (statico) parametri object
Finalize() Protetto Usato per eliminare l’oggetto dalla memoria, richiamato dal garbage collector
GetHashCode() Pubblico Genera un valore per il tipo nell’ambito di una hash table
GetType() Pubblico Restituisce il tipo dell’oggetto. Lo abbiamo incontrato nell’esempio 4.6
MemberwiseClone() Protetto Crea una shallow copy dell'oggetto (importante, ne riparleremo)
ToString() Pubblico Restituisce una stringa che rappresenta l'oggetto corrente
ReferenceEquals() Pubblico Statico - determina l'identità tra istanze

I metodi in rosso non sono applicabili direttamente a System.TypeValue. Facciamo ora un passo ulteriore:

System.TypeValue
|
System.Enum


L'unica classe direttamente discendente da TypeValue è Enum. Questo è uno dei tipi valore base. Tuttavia i compilatori .Net compliant mettono a disposizione dei costrutti che permettono la creazione di nuovi tipi valore. Per C# questi costrutti sono le strutture (o struct) alle quali quindi viene assegnato questo fondamentale ruolo. Per cui tutti i tipi valore sono in realtà, per loro natura:

Enum oppure Struct.

Il framework stesso lavora in questo modo e ci fornisce una serie di struct che costituiscono i tipi base che utilizziamo nei nostri programmi. Quando definiamo una variabile intera ad esempio, come vedremo in questo stesso capitolo, istanziamo una struct predefinita nella BCL. Una struct che contiene molta informazione. Quindi, ogni qual volta adoperiamo queste entità così importanti creiamo qualche cosa che va a popolare lo stack. E' ovviamente il compilatore che si fa carico della corretta collocazione in memoria.

Ricodiamo, e lo faremo ancora, che i tipi valore sono, tranne in poche specifiche situazioni, sempre allocati sullo stack e che su essi si lavora direttamente, non attraverso dei riferimenti.

In attesa di parlare di System.Enum è a questo punto doveroso ed inevitabile fare un accenno alle struct, autentiche protagoniste di questo paragrafo. Torneremo sull'argomento più avanti quando avremo introdotto le classi, stante le similitudini tra i due costrutti, ma per gli scopi di questo capitolo è comunque bene avere una infarinatura di superficie.
Una struct, per definizione è un tipo valore che racchiude al suo interno un certo contenuto informativo. Formalmente abbiamo:

[attributi][modificatori]struct nome [:interfaccia]
{
corpo
}

Quanto si trova all'interno delle parentesi quadrate, è opzionale e per il momento lo lasciamo da parte. Una struttura prevede un corpo centrale nel quale possiamo definire metodi e proprietà. Inoltre è previsto, si potrebbe dire di default, un costruttore, ovvero un sistema di inizializzazione che può essere invocato in modo preciso anche se qui no ne faremo uso. Detto questo, seguendo la forma base della definizione formale precedente possiamo scrivere come esempio:

struct Punto
{
  int x;
  int y;
  int z;
  public void ChiSei()
  {
    Console.WriteLine("Sono un punto");
  }
}

L’esempio riguarda proprio la definizione nelle sue parti necessarie; c’è la keyword struct alla quale segue il nome della struttura stessa e il corpo che contiene 3 proprietà(x, y, z) ed un metodo (ChiSei). Se provate ad inizializzare le 3 proprietà in fase di definizione attribuendo loro un valore il compilatore se la prende. L’inizializzazione è possibile solo dopo la fase di istanziazione. Come abbiamo visto nel capitolo precedente istanziare significa definire una variabile basata su un certo tipo; qui il "tipo" è la struttura stessa. Vediamo quindi un esempio che si basa sulla struttura appena definita:

  Esempio 5.1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
using System;
struct Punto
{
  public int x;
  public int y;
  public int z;
  public void ChiSei()
  {
    Console.WriteLine("Sono un punto");
  }
}
class Program
{
  public static void Main()
  {
    Punto p1 = new Punto();
    p1.ChiSei();
    p1.x = 3;
    p1.y = 4;
    p1.z = 0;
  }
}

Il programma 5.1 molto semplicemente espone a video un messaggio e inizializza tre variabili. Analizzandolo:
riga 2 - riga 11 - abbiamo la dfinizione della struttura. Essa presenta 3 variabili pubbliche, come vedremo questo significa, come d'altronde dice la parola, che sono liberamente accessbili e,dalla riga 7 alla 10 un metodo, anch'esso pubblico. Le tre variabili costituiscono le proprietà della struttura.
riga 16 - qui viene istanziata la struttura
riga 17 - viene richiamato il metodo
riga 18 - 19 - 20 - vengono assegnati dei valori a x, y e z. 
L'accesso ai metodi ed alle proprietà avviene, come di consueto, attravero l'operatore . (punto). Da notare che il compilatore attribuisce comunque alle proprietà il valore di default del loro tipo, per gli interi è 0, quindi se eliminaste le istruzioni alle righe 18. 19 e 20, x,y, e z varrebbero 0. Questo avviene perchè abbiamo utilizzato l'operatore new che richiama il costruttore di default; le struct potrebbero essere istanziate anche senza tale operatore ma, almeno in linea di principio, vi consiglio di usarlo a meno di indicazioni contrarie. Per quanto possa essere ridondante anche ricordarsi di inizializzare i suoi membri interni, le proprietà, è cosa a mio avviso consigliabile.
Le strutture sono elementi molto importanti e il loro studio non può essere limitato a queste poche righe. Come detto, su di esse ritorneremo diffusamente ma per il momento i concetti cardine che dovete afferrare sono pochi e semplici, oltre a quanto già detto, in particolare quello che torna utile è ricordare che le strutture contengono variabili interne (proprietà) e funzioni interne (metodi) accessibili tramite l'operatore punto. In pratica in questo paragrafo è di questo che faremo uso.

Ed eccoci ora ad analizzare finalmente i tipi valori che .Net ci mette a disposizione. Iniziamo con una tabella riepilogativa, nella quale mettiamo in evidenza anche quali sono i tipi CLS compliant. I nomi dei tipi stessi vedremo essere in realtà degli alias.

Tipo CLS Descrizione
bool si Booleano, assume valore true o false
byte si Intero con segno su 8 bit - valori da -128 a 127
sbyte no Intero su 8 bit senza segno - valori da 0 a 255
char si Carateeri Unicode 16 bit - range da U+000 a U+ffff
decimal si Numeri con virgola, Vanno da 1.0 x 10^-28 fino a ±7.9 x 10^28 con una precisione di 28 / 29 numeri significativi. Necessitano del suffisso m o M
double si Qui si va da ±5.0 × 10−324 a ±1.7 × 10308 con una precisione di 15-16 cifre. Un intero può essere trattato come double usando il suffisso d oppure D. Ad.es: double x = 3d. Double costituisce il default per i numeri con virgola.
float si Il range va da ±1.5x10^-28 a ±3.402823x10^38 con una precisione di 7 cifre. Dal momento che, come detto, il default per i numeri con virgola è il tipo double, per trattare un numero reale come float è necessario aggiungendo un suffisso f o F.
int si Classici interi con segno 32 bit quindi da -2,147,483,648 a 2,147,483,647
uint no Intero senza segno 32 bit range da 0 a 4.294.967.295
long si Intero con segno 64 bit quindi da –9.223.372.036.854.775.808 a 9.223.372.036.854.775.807. Definiti con la parola chiave long. Ad es. long l1 = 9999999999999999; Per forzare l'uso di long su un numero che ricadrebbe ad esempio negli interi si deve usare il suffisso l o L. Meglio in questo caso usare L (maiuscolo) in quanto la l minuscola si confonde col numero 1
ulong no Intero senza segno 64 bit, si va da 0 a 18,446,744,073,709,551,615
short si CLS – SI. Intero con segno 16 bit. Va da -32.767 a + 32768.
ushort no Interi senza segno su 16 bit. Range da 0 a 65535

Iniziamo studiando il tipo probabilmente più semplice:

I BOOLEANI

La parola che introduce questo tipo, come da tabella, è bool; si tratta di un alias per System.Boolean che, di fatto è una struct, come avrete immaginato. Questa struttura ha al suo interno molti metodi ma iniziamo dalle sue proprietà che sono solo due:

1)  FalseString - proprietà readonly, ovvero non modificabile, che è in pratica la stringa "False"
2)  TrueString - proprietà readonly che è in pratica la stringa "True"

Si tratta di proprietà statiche (per i dettagli su questo modificatore date uno sguardo al paragrafo successivo) che quindi non possono richiamati da istanze ma solo tramite invocazione del tipo. Vediamo l'esempio:

  Esempio 5.2
1
2
3
4
5
6
7
8
9
using System;

class Program
{
  public static void Main()
  {
    Console.WriteLine(bool.TrueString);
    Console.WriteLine(bool.FalseString):
  }
}

Come detto questi valori non sono modificabili. Contrariamente quindi a quanto accade in C/C++ non sono ammessi valori numerici, come 0 e 1 in luogo di False e True. Questa mi sembra una scelta logica e molto più sicura anche dal punto di vista dei possibili bug.  Tra l'altro, conseguentemente, non sono permesse conversioni da valori numerici a valori booleani a meno di utilizzare la classe Convert, anch'essa appartenente al namespace System; di quest'ultima potente classe parleremo a suo tempo per il momento tenete presente che non è possibile passare direttamente da intero a booleano e viceversa. Istanziare una variabile booleana vuol dire inizializzare le sue proprietà quindi i valori accettabili sono solo le due stringhe appena viste. Come nota va detto che internamente, come vedremo alla fine di questa sezione, l'Intermediate Language fa uso di 0 e 1 per distinguere true da false; ma, come detto, è una cosa interna al framework per il programmatore e l'utente finale la cosa è del tutto ininfluente.
L'assegnazione di un valore ad una variabile booleana avviene attraverso un cosiddetto "literal" o "valore letterale" che è appunto una rappresentazione di valori a livello di codice sorgente. Nel caso dei booleani i letterali ammessi sono "true" e false". L'esempio seguente illustra in maniera semplice la cosa:

  Esempio 5.3
1
2
3
4
5
6
7
8
9
10
using System;

class Program
{
  public static void Main()
  {
    bool b1 = true;
    bool b2 = false;
  }
}

Le parole true e false, alle righe 7 e 8 sono i valori letterali per vero e falso. Le semplici espressioni alle stesse righe inizializzano due istanze, aventi nome b1 e b2, del tipo bool rispettivamente a vero o falso; non è il solo metodo di istanziare una variabile booleana, come vedremo nell'esempio 5.4. Ricordatevi che una variabile booleana non inizializzata ha valore false. . Ovviamente in condizioni normale prima di usarla dovrete attribuirgli un valore. Il sistema dell'esempio precedente è un buon metodo per inizializzare delle variabili booleane ma non è il solo. Si possono usare anche espressioni che abbiano lo stesso significato logico:

b1 = (1 > 2); // questo è equivalente a b1 = false, perchè 1 non è maggiore di 2.
b1 = (3 * 3 > 5); // 3 * 3 = 9 > 5
b1 = metodo che restituisce true o false

E tanti altri esempi si possono fare. Ne vedremo ancora tra poco.

Interessante a questo punto, concentrarci sui tanti metodi che ci vengono messi a disposizione quando lavoriamo coi nostri valori booleani. Alcuni sono ereditati da Object e non vale pena ripeterli: nuovi e interessanti invece sono:

CompareTo   Confronta due elementi e restituisce 0 se uguali 1 se il primo è maggiore del secondo -1 se il secondo è maggiore del primo. True è > di false.
Parse            Trasforma una stringa nel suo equivalente booleano
TryParse       Simile al precedente ma restituisce un valore che indica se la conversione è andata o no a buon fine oltre ad effettuare un assegnamento se la conversione è ok. Personalmente trovo molto potente e comoda questa istruzione.
Vediamo l'esempio  che sarà molto istruttivo:

  Esempio 5.4
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
using System;
class Program
{
  public static void Main()
  {
    bool b1 = true;
    Console.WriteLine(b1);
    System.Boolean b2 = new System.Boolean();
    b2 = (2 < 1);
    Console.WriteLine(b2);
    Console.WriteLine(b1.CompareTo(b2));
    bool b3;
    b3 = Boolean.Parse("false");
    Console.WriteLine(b3);
    bool b4;
    bool b5;
    b4 = Boolean.TryParse("false", out b5);
    Console.WriteLine(b4);
  }
}

che ha il seguente output:

True
False
1
False
True

Prima di illustrare i metodi osserviamo la riga 8 che illustra un metodo alternativo di dichiarare un valore booleano tramite istanziazione piena della struct. Più verboso ma certamente molto consono alla natura delle cose.... ovviamente il sistema alla riga 6 è più comune.
La riga 9 assegna il valore false tramite una istruzione logica.
La riga 11 effettua il confronto restituendo 1 in quanto b1 è true che è considerato maggiore di false, come abbiamo detto. Se invece volete semplicemente testare se due variabili booleane sono uguali, ottenendo da questo confronto un valore true o false come risposta, potrete tranquillamente usare l'operatore = ad esempio in una istruzione come Console.WriteLine(b1 = b2). Invece non si possono usare gli operatori di confronto < e >.
La riga 13 effettua il parsing della stringa false e la attribuisce a b3 (è un altro metodo di inizializzazione, se volete, anche se piuttosto raro). Peraltro il programma va in crash se la stringa non è  true o false (vanno bene anche con la maiuscola iniziale) e per il momento non abbiamo gli strumenti per gestire tale situazione (per maggiori informazioni vedere il capitolo riferito alle eccezioni)
La riga 17 fa la stessa cosa, ovvero il parsing di una stringa e cerca di attribuire il risultato di questa operazione a b5. Invece b4 risulta true o false a seconda che il parsing precedente sia andato a buon fine oppure no. In questo caso il programma regge se la stringa non è congrua e a b5 e in questo caso a b5 resterebbe assegnato il valore di default ovvero false. Fino a quando non conosceremo la gestione delle eccezioni è forse meglio usare TryParse, anche se "sprecate" la variabile d'appoggio (b4, in questo caso). La parolina out che vedete sempre alla riga 17 la spiegheremo quando parleremo dei metodi.

Il tipo booleano, nella sua semplicità nasconde una certa inefficienza in termini di uso della memoria; infatti per la sua rappresentazione in memoria è sufficiente un solo bit ma viene comunque "sprecato" un byte, che è l'unità minima gestibile; questo piccolo problema è noto a chi utilizza .Net in generale. Più avanti impareremo a conoscere la classe BitArray appartenente al namespace System.Collections, che risolve questo problema.

Prima di concludere vediamo come un semplicissimo programma viene tradotto internamente dal compilatore:

using System;
class Program
{
  public static void Main()
  {
    bool b1 = true;
    bool b2 = false;
    b1 = false;
  }
}

dà origine a:

.method public hidebysig static void Main() cil managed
{
.entrypoint
// Code size 8 (0x8)
.maxstack 1
.locals init (bool V_0,
bool V_1)
IL_0000: nop
IL_0001: ldc.i4.1
IL_0002: stloc.0
IL_0003: ldc.i4.0
IL_0004: stloc.1
IL_0005: ldc.i4.0
IL_0006: stloc.0
IL_0007: ret
} // end of method Program::Main

dove è evidente la corrispondenza a 1 e 0 rispettivamente di true e false. Ma, come detto, è una cosa interna al framework. Non preoccupatevi naturalmente di altri dettagli di questo codice.

In linea di massima questo è tutto quanto possiamo dire al momento sulle variabili booleane. Come vedremo il concetto di "vero" e "falso", nella sua semplicità sarà utilissimo e molto utilizzato nel prosieguo di tutto il nostro percorso.