C#
CONVERSIONI, CAST, BOXing e UNBOXING

In C# e in .Net in generale dobbiamo tenere presente il discorso delle conversioni di tipo. Esistono quattro branche in questo senso:

conversioni implicite
conversioni esplicite
conversioni definite dall'utente, generalmente su classi dallo stesso create
conversioni con classi di supporto

In questo paragrafo accenneremo ai primi due tipi, quanto basta per i nostri scopi.

Conversioni implicite

Sono conversioni sicure, che non presentano pericolo di perdita di informazione e non hanno una sintassi speciale. L'esempio tipico è quello di un valore numerico che vengano archiviato in una variabile di un tipo più capiente, ad esempio un intero dentro un long. In questo caso non corriamo rischi e la cosa viene eseguita automaticamente senza problemi. A questo proposito di seguito propongo una tabella, presa dal sito MSDN, ma ne troverete a iosa del tutto simili sul web, relativa alle conversioni numeriche permesse:

Da

A

sbyte

short,int,long,float,double o decimal

byte

shortushortintuintlongulongfloatdouble o decimal

short

intlongfloatdouble oppure decimal

ushort

intuintlongulongfloatdouble o decimal

int

longfloatdouble o decimal

uint

longulongfloatdouble oppure decimal

long

floatdouble o decimal

char

ushortint, uint, longulong, float, double o decimal

float

double

ulong

floatdouble o decimal


Può essere utile tenere a portata di mano questa tabella, ripeto, non è un problema trovarne altre in giro se non gradite questo layout.
Per capire la cosa, ovvero la differenza tra conversione implicita ed esplicita, guardiamo il seguente esempio:

  Esempio 14.1
1
2
3
4
5
6
7
8
9
10
11
using System;
class Program
{
  public static void Main()
  {
    int x = 267890;
    long y = x;
    long z = 123456;
    int t = z;
  }
}

Questo programma non compila e l'errore è del tutto chiaro:

(9,17): error CS0266: Non è possibile convertire in modo implicito il tipo 'long' in 'int'. È presente una conversione esplicita. Probabilmente manca un cast.

Vedete? Il compilatore ci avvisa che alla riga 9, posizione 17, viene effettuata una conversione implicita che non va bene in quanto una variabile di tipo long è riversata in una variabile di tipo int. Anche se il valore è ammessibile anche gli int, il passaggio in se non è corretto e il compilatore non fa sconti. Al contrario, come da tabella, la conversione alla riga 7, che è la classica conversione implicita, non origina problemi, la sua sintassi non è nulla di che, un tipico assegnamento. Tra l'altro, per quanto riguarda il problema alla riga 9,  ci viene suggerita una possibile soluzione: Probabilmente manca un cast...  ed è proprio così.
Un altro aspetto da considerare è la possibile perdita di precisione:

int x = int.MaxValue;
float f = x;
Console.WriteLine(f);


queste 3 righe danno come output:

2,147484E+09

che, evidentemente, non è il valore preciso di MaxValue (2147483647). Ciò,è dovuto alla natura dei float, se ci pensate un attimo (se a float sostituite ad esempio decimal, tutto funziona perfettamente). Il compilatore non vi segnala nulla, chi l'ha progettato, evidentemente, ritiene accettabile questo fatto (sono quelle cose che odio dei linguaggi, personalmente). Attenzioni quindi a quando si passa da :

-- da int, uint, long, ulong a float
-- da long, ulong a decimal.

Per i reference types, stanti i rapporti di ereditarietà non è previsto, anche qui, alcun tipo di sintassi particolare.

Conversioni esplicite

In questo caso parliamo di conversioni che potrebbero dare origine a perdita di informazione. In questo caso è richiesta quindi l'interazione diretta del programmatore per chiarire che sono effettivamente quelle le sue intenzioni. Si deve quindi proporre un cast. E' prevista una sintassi apposita nel formato:

variabile-1 = (tipo)variabile-2;

Ad esempio il programma precedente funziona riscrivendo la riga 9 come segue:

int t = (int)z;

in rosso abbiamo il nostro cast. Non è possibile effettuare cast su qualsiasi cosa ci passi per la mente. Anche qui, dal sito MSDN, propongo una tabella per le variabili numeriche:

Da

A

sbyte

byte , ushort, uint, ulong oppure char

byte

Sbyte o char

short

sbyte , byte, ushort, uint, ulong o char

ushort

sbyte , byte, short o char

int

sbyte , byte, short, ushort, uint, ulong, o char

uint

sbyte ,byte, short, ushort, int o char

long

sbyte , byte, short, ushort, int, uint, ulong o char

ulong

sbyte , byte, short, ushort, int, uint, long o char

char

sbyte , byte o short

float

sbyte , byte, short, ushort, int, uint, long, ulong, char, o decimal

double

sbyte , byte, short, ushort, int, uint, long, ulong, char, float, o decimal

decimal

sbyte , byte, short, ushort, int, uint, long, ulong, char, float o double

 
ovviamernte bisogna fare un po' di attenzione in questi casi. E' facile prevedere alcuni comportamenti che possono risultare in qualche caso "pericolosi". Ad esempio:

float x = 1.2345F;
int y = (int)x;
Console.WriteLine(y);

dà come risultato il numero 1 perchè passando da un numero con virgola ad uno senza è evidente he si perde quello che sta a destra della virgola stessa.
Anche risultati strani sono sempre in agguato qualora si riversi un valore in di un tipo in un altro di tipo meno capiente. Abbiamo già visto qualche esempio in passato, tipo il seguente:

long x = 999999999999;
int y = (int)x;
Console.WriteLine(y);


che ci restituisce un numero che nulla ha a che vedere con x originale. Un long non può essere contenuto in un int, almeno non sempre. La cosa è insidiosa e bisogna stare attenti. Ricordate il caso del razzo Ariane? Quindi attenzione, come visto anche in altri paragrafi, convertire un int in un byte, ad esempio, può far predere l'informazione originale. Il fatto che certe conversioni debbano essere esplicitate è proprio perchè bisogna essere consapevoli di quel che si fa.
Arrotondamenti e overflow sono sempre in agguato. Il tutto, in maniera esplicita, è spiegato qui.

Se rimanete nell'ambito della tabella tutto funziona, se agite con la dovuta cautela, diversamente il compilatore vi bloccherà; tuttavia se il compilatore stesso non è in grado di comprendere se un cast andrà a buon fine oppure non ve lo lascerà passare e allora potrete incorrere in una InvalidCastException. Ciò è valido soprattutto giocando con le classi. C# ci mette a disposizione due operatori, ovvero is e as, per effettuare dei test di verifica. Ne parleremo quando avremo introdotto le classi.

Boxing e unboxing

Il meccanismo di boxing e unboxing è in qualche modo legato al concetto di cast, visto che di esso si serve esplicitamente l'unboxing. premetto subito che si tratta di un'operazione piuttosto pesante e va usata quando serve veramente. Il framework stesso lo usa si, ma senza esagerare.
Il boxing è sostanzialmente l'operazione di un "inscatolamento" di un value type in un reference type, (un oggetto o un interfaccia supportata), passando quindi nello heap. L'unboxing è l'operazione opposta. e, come detto, necessita di un cast, laddove il boxing è sintatticamente diretto. Il funzionamento della fase di boxing è il seguente:

1) La memoria viene allocata nello heap, in particolare si tratta dello spazio richiesto per accogliere il value type + altri due membri (l’oggetto puntatore e un indice di sincronizzazione)
2) Il valore è copiato nello heap
3) Viene restituito il puntatore all’indirizzo nello heap

L’operazione inversa, l’unboxing, è un po’ meno costosa dato che si riduce a due passaggi fondamentali, ovvero reperire il puntatore al valore contenuto nell’oggetto e poi riversarlo nel value type corrispondente. La cosa appare subito abbastanza pesante, come detto, e ad ogni modo l'introduzione dei generics, di cui riparleremo, ha reso molto meno indispensabili questi meccanismi. Ad ogni modo vediamo un po' di codice:

int x = 3;
object o = x

Ecco fatto, la prima riga definisce un intero, la seconda realizza l'operazione di boxing. Il valore è trasferito nello heap e di qui, stante il fatto che gli oggeti costiuiscono la radice del type system di C#, potrà essere di nuovo trasferito in un value type.

o = 4;
x = (int)o;


Qui sopra abbiamo modificato il valore di o e lo abbiamo ritrasferito, tramite cast, in x.
L'attenzione è d'obbligo:

  Esempio 14.2
1
2
3
4
5
6
7
8
9
10
11
using System;
class Program
{
  public static void Main()
  {
    int x = 1;
    object ob = x;
    int y = (int)ob;
    Console.WriteLine(y);
  }
}

L'esempio 14.2 funziona. Se però provate a modificare la riga 8 come segue:

long y = long(ob);

otterrete un errore a runtime e precisamente un eccezione di cast. Questo è naturale, perchè ob contiene il riferimento ad un intero, non ad un long. Se volete un long avete due soluzioni per la riga 10:

long y = (int)ob;

realizzando un cast esplicito ed uno implicito, oppure, meno bello:

long y = (long)(int)ob;

Quindi è necessario prevedere che la variabile in cui riversare l'unboxing sia compatibile con quello di partenza.