C#

NUMERI IN VIRGOLA

L'argomento in oggetto è interessante anche perchè coinvolge aspetti che esulano dal mondo .Net e si avvicinano agli aspetti generali che legano la rappresentazione dei numeri all'intero degli elaboratori elettronici. I numeri in virgola rappresentano pur sempre un ostacolo e vanno affrontati col dovuto rispetto da parte anche dei language writer, pur se la teoria ormai è consolidata. Il problema è che i computer parlano il loro linguaggio binario che, obbligato ad una certa rappresentazione dei numeri, si accosta concenttualmente in maniera un po' laboriosa a questa tipologia di numeri. In rete troverete innumerevoli esempi sulla questione. Se volete saperne davvero di più questo è un buon documento per imparare qualcosa. Come si vede di materiale ce n'è ;-).

Detto questo vediamo quali sono i tipi numerici in virgola di base  che C# gestisce (più avanti parleremo dei decimal che sono un cosa un po' diversa):

Tipo Struct Range approssimato Precisione
float System.Single da -3.4 × 10^38 a +3.4 × 10^38 7 cifre
double System.Double  da ±5.0 × 10^324 a ±1.7 × 10^308 15-16 cifre

La rappresentazione dei single è definita su 32 bit dei quali:
0-22  - mantissa
23-30 - esponente
31    - segno


La rappresentazione dei double è definita su 64 bit dei quali:
0-51  - mantissa
52-62 - esponente
63    - segno


graficamente spesso vedrete questa rappresentazione con il bit 0 posto all'estrema destra e il bit 31 o il 63 a sinistra.

Per entrambi i tipi il bit relativo al segno è 0 per i positivi e 1 per i negativi. Entrambi seguono le specifiche IEEE-754. Il loro valore di default è 0.

Va specificato che il tipo numerico in virgola di default è il double e che per forzare l'uso dei float dobbiamo ricorrere al suffisso F o f. Quindi ad esempio:

float f1 = 3.3F;

Non è nelle finalità di questo paragrafo tenere un corso completo sui numeri in virgola fissa e tutta la loro teoria; per capire il funzionamento e la differenza sostanziale delle due tipologie ci può bastare sapere che la rappresentazione attraverso la composizione disponibile nei float (e, in forma estesa, nei double) è:

N = (-1)^S * 2^(E-127) * (1.M)

laddove un float è così visto:

S Y X

S = Segno
E = Esponente
M = Mantissa

Ad esempio:

0 10000000 11100000000000000000000

Segno: +
Esponente: 128-127 = 1
Mantissa 1.(0,5 + 0.25 + 0.125) = 1.875

quindi:
+(2 * 1.875) = 3.75

E' evidente che la mantissa arriva fino ad una certa precisione mentre nei double questa sarà maggiore in forza della maggior spettro di rappresentazione. Quanto alla precisione, essa è conseguenza del valore ottenuto da 2^-23 ~  10^-7 per i float e 2^-52 ~ 4 * 10^-16 per i double. Come detto, non mancano le informazioni al riguardo facilmente reperibili su Internet. Il concetto fondamentale da comprendere è che i numeri con virgola mobile vengono quindi rappresentati tramite una approssimazione, che può essere precisissima ma, per sua natura, sempre di approssimazione si tratta.

Fatta questa breve introduzione teorica, ovviamente trasparente durante la vostra attività di programmazione, vediamo di fare conoscenza con quanto possiamo utilizzare con il linguaggio:

  Esempio 8.1
1
2
3
4
5
6
7
8
9
10
11
using System;
class Program
{
  public static void Main()
  {
    var x1 = 8.3;
    var x2 = 8.3F;
    Console.WriteLine(x1.GetType());
    Console.WriteLine(x2.GetType());
  }
}

Che ci fornisce il seguente output

System.Double
System.Single

Un'istruzione tipo

float x1 = 8.3;

non è ammissibile, il compilatore risponderebbe

error CS0664: Literal of type double cannot be implicitly
converted to type 'float'; use an 'F' suffix to create a literal of this
type


molto chiaro.
Che ci conferma quanto detto relativamente al default dei numeri con virgola mobile e a come forzare l'uso dei float. Non badate per ora alla parola var che viene usata per l'inferenza di tipo della quale riparleremo a suo tempo. Concentriamoci sui numeri e vediamo ora un secondo esempio sempre mirato a verificare le differenze tra le due tipologie di numeri:

  Esempio 8.2
1
2
3
4
5
6
7
8
9
10
11
12
13
using System;
class Program
{
  public static void Main()
  {
    float f1 = 7.3F;
    float f2 = 6.6F;
    double d1 = 7.3;
    double d2 = 6.6;
    Console.WriteLine(f1/f2);
    Console.WriteLine(d1/d2);
  }
}

che ci dà:

1,106061
1,10606060606061

che ci conferma la miglior precisione ottenibile con i double. Questo fatto porta ad alcune conseguenze solo apparentemente assurde:

float f1 = 7F;
float f2 = 3f;
double d1 = 7;
double d2 = 3;
Console.WriteLine((f1/f2) == (d1/d2));


Qui l'ultima istruzione stampa false... l'operazione è la stessa, i numeri solo gli stessi ma il risultato soffre per la differenza dei tipi coinvolti.

E' altrettanto vero che i double hanno un maggior peso in termini di occupazione di memoria. In questo senso sta al programmatore vedere se privilegiare la precisione o una maggior leggerezza del software che sta scrivendo. L'uso del tipo double mi sembra un buon compromesso in quanto, in rari casi, il tipo float potrebbe mostrare un po' la corda. Vediamo il seguente esempio:

  Esempio 8.3
1
2
3
4
5
6
7
8
9
10
11
12
using System;
class Program
{
  public static void Main()
  {
    int x = 17;
    float f = 4.99F;
    double d = 4.99;
    Console.WriteLine(x * f);
    Console.WriteLine(x * d);
  }
}

Il risultato è:

84,82999
84,83

ovvero il float introduce un errore, per quanto piccolo, già alla seconda cifra.... in molti casi un se ritenete di potervi imbattere in casi simili e l'errore introdotto vi dà fastidio ripiegate tranquillamente sul tipo double. Evidentemente sarà possibile introdurre dei controlli ma in questa fase e se davvero non avete problemi stringenti di performance e di memoria usando i double correrete meno rischi.... questo è un  consiglio personale, fatevi la vostra esperienza. In ambito finanziario ad esempio, dove esistono dei fattori di moltiplicazione enormi, un piccolo sbaglio numerico può far fallire un'azienda di centinaia di dipendenti... in questi campi spesso si usano i decimal, addirittura. In caso di applicazioni Asp.Net invece, siamo in ambito Web pertanto, possono essere le problematiche relative all'uso della memoria ad essere più sentite. considerate infine che, con le moderne CPU dotate di FPU, dal punto di vista delle prestazioni non vi sono differenze tali da risultare significative nella stragrande maggioranza delle applicazioni. 
L'interazione tra i due tipi è abbastanza pacifica; la regola vuole che se in una espressione sono presenti un float e un double il risultato sia un double. Il principio generale, valido anche in altre interazioni tra tipi numerici, è che si predilige il tipo avente precisione maggiore. Come vedremo nell'esempio 8.5 riga 11 questo vale anche quando un double collabora con un intero. E varrà anche, con qualche attenzione, quando entreranno in scena i decimal.

  Esempio 8.4
1
2
3
4
5
6
7
8
9
10
11
12
using System;
class Test
{
  public static void Main()
  {
    float f1 = 3.3F;
    float f2 = 4.9F;
    double d1 = 4.7;
    Console.WriteLine((f1 * f2).GetType());
    Console.WriteLine((f1 * d1).GetType());
  }
}

l'output conferma la regola:

System.Single
System.Double

A questo proposito diamo una rapida occhiiata alle operazioni applicabili che sono naturalmente quelle di base già incontrate per gli interi:

double d0 = 7.6;
double d1 = 4.7;
Console.WriteLine(d0 + d1);
Console.WriteLine(d0 - d1);
Console.WriteLine(d0 * d1);
Console.WriteLine(d0 / d1);
Console.WriteLine(d0 % d1);


L'ultimo, come ricordete, è il resto della divisione. Per l'elevamento a potenza è disponibile Math.Pow che lavora direttamente sul tipo double.
Parlando degli interi ricorderete come il risultato di una divisione tipo 7 / 3 risultava essere un a mio avviso deludente 2, ovvero la parte intera, il quoziente, dell'operazione invece del risultato esatto, come fanno altri linguaggi tipo Falcon o Python, che sarebbe 2,333333..... a me, personalmente, questo sistema non piace ma tant'è. Per ottenere il risultato che la matematica ci impone possiamo usare vari sistemi:

  Esempio 8.5
1
2
3
4
5
6
7
8
9
10
11
12
13
using System;
class Test
{
  public static void Main()
  {
    int x = 7;
    int y = 3;
    Console.WriteLine(x / 3.0);
    Console.WriteLine((double)x / y);
    Console.WriteLine(x / (double)y);
    Console.WriteLine((0.0 + x) / y);
 }
}

Quindi, in un modo o nell'altro, uno dei due membri dell'operazione deve essere un double, che sia esplicito (riga 8), ottenuto attraverso un cast esplicito (righr 9 e 10) o implicito (riga 11... si poteva anche impostare la moltiplicazione di uno dei due per 1.1, ad esempio) alla fin fine quella è la strada. Vi piace? A me no. Ad ogni modo scegliete la strada che vi piace di più.

Parlando delle singole strutture e delle loro peculiarità, cominciamo dai float; questa keyword, come visto nella prima tabella, è un alias per System.Single che è la solita struct che viene istanziata ogni qual volta creiamo un float. Le proprietà di tale struct sono le seguenti:

Proprietà Descrizione
Epsilon Costante che rappresenta il positivo più piccolo
MaxValue Costante che rappresenta il Single più grande
MinValue Costante che rappresenta il Single più piccolo
NaN Costante acronimo di Not a Number
NegativeInfinity Costante - infinito negativo
PositiveInfinity Costante - infinito positivo

Se eseguite per esempio in un programma queste 3 righe:

Console.WriteLine(Single.Epsilon);
Console.WriteLine(Single.MaxValue);
Console.WriteLine(Single.MinValue);


ottenete:

1,401298E-45
3,402823E+38
-3,402823E+38

Più interessanti sono invece i numerosi metodi che arricchiscono la struct, vediamo le più significative:

CompareTo - permette il confronto tra due numeri single restituendo come risultato
-1 se il secondo termine di paragone è maggiore,
0 se i due elementi sono uguali
1 se il primo è il maggiore.
Console.WriteLine(3.0.CompareTo(2.9)); restituisce il valore 1

IsInfinity, IsNan, IsNegativeInfinity, IsPositiveInfinity - evidentemente riferiscono ad eventuali valori particolari del numero valutato. Restituisce True o False
Console.WriteLine(Single.IsInfinity(2.0f/0)); restituisce True.

Parse - Importante, riceve in input una stringa, ad esempio tramite tastiera o file, e lo attribuisce ad una variabile di tipo float o solleva una eccezione se l'input non è compatibile.
float f = Single.Parse("3.3");
da notare che non è necessario il suffisso F o f la cui presenza anzi darebbe origine ad un errore.

TryParse - Analogamente a quanto già visto con questa istruzione in altri costrutti, questo metodo permete di tentare un'operazione di parsing della stringa di input restituendo true in caso di successo della conversione o false in caso di insuccesso senza dare origine ad eccezioni. Vediamo un esempio completo:
using System;
class Test
{
  public static void Main()
  {
    string s = Console.ReadLine();
    float f1;
    Console.WriteLine(Single.TryParse(s, out f1));
    Console.WriteLine(f1);
  }
}


Un problema comune relativamente ai numeri in virgola è quello del loro arrotondamento. E' importante notare che la funzione principale devoluta a questo scopo, Math.Round, può produrre risultati apparentemente non congrui:

double d1 = 1.5;
Console.WriteLine(Math.Round(d1));
double d2 = 2.5;
Console.WriteLine(Math.Round(d2));

Questo frammento di codice, se eseguito, restituisce il risultato 2 e il motivo è che Round (vi invito comunque a consultare la guida relativa a questo interessante metodo reperibile come sempre su MSDN) se si trova per così dire in mezzo tra un numero pari e un dispari restituisce sempre il numero pari. Si può pensare di utilizzare Math.Floor o Math.Ceiling al fine di andare sempre, rispettivamente, verso l'intero più piccolo o verso quello di più grande più vicini al nostro numero di partenza.

Un'ultima osservazione interessante ci viene da ILDASM, grande utility, su questo semplice codice:

using System;
class Test
{
  public static void Main()
  {
    float f1 = 3.4F;
  }
}

Ecco qua cosa ne risulta per il Main:

.method public hidebysig static void Main() cil managed
{
.entrypoint
// Code size 8 (0x8)
.maxstack 1
.locals init (float32 V_0)
IL_0000: nop
IL_0001: ldc.r4 3.4000001
IL_0006: stloc.0
IL_0007: ret
} // end of method Test::Main


Vedete in particolare le righe evidenziate in rosso... se avessimo usato un double avremmo ottenuto:

.locals init (float64 V_0)
IL_0000: nop
IL_0001: ldc.r8 3.3999999999999999


Come si vede double e float, internamente condividono la stessa natura, pur nelle altrettanto evidenti differenze.
Passando appunto ai double siamo di fronte ad un alias per System.Double, che non presenta particolarità, in termini di proprietà e metodi, rispetto a quanto appena visto per i float. Anzi esiste una identità completa. Vediamo un esempio indicativo:

  Esempio 8.6
1
2
3
4
5
6
7
8
9
10
11
12
13
using System;
class Test
{
  public static void Main()
  {
    string s1 = Console.ReadLine();
    double d1;
    double.TryParse(s1, out d1);
    Console.WriteLine(d1 + 1);
    Console.WriteLine(double.IsInfinity(d1));
    Console.WriteLine(double.MaxValue);
  }
}

Con il seguente output (ovviamente dipendente dall'input digitato, nel caso 5,679):

5,679
6,679
False
1,79769313486232E+308

Come si vede tutto uguale a quanto già visto e potete fare altri esempi per conto vostro che confermeranno la cosa.
Vi sono anche alcune regole comuni:
  • se il risultato di un'operazione è troppo piccolo (ricordate che c'è un limite in questo senso) esso viene posto a 0
  • se il risultato è troppo grande esso viene posto a +∞ o -∞
  • se un'operazione fallisce il suo risultato è NaN
  • se uno dei due operandi è NaN il risultato è sicuramente NaN
Altra caratteristica comune ai due tipi è quella di mantenere internamente un paio di cifre in più (quindi m9 per i float e 17 per i double) rispetto allo standard. Questo fatto è del tutto trasparente al programmatore.
Infine avrete notato che la notazione che abbiamo adottato finora è quella più consueta; tuttavia entrambi, float e double, accettano anche il formato esponenziale. Quindi

  Esempio 8.7
1
2
3
4
5
6
7
8
9
10
11
12
13
14
using System;
class Test
{
  public static void Main()
  {
    double d1 = 7e-4;
    Console.WriteLine(d1);
    float f1 = 3.567e-8F;
    Console.WriteLine(f1);
    int x1 = 50;
    double f2 = 3e-2;
    Console.WriteLine(x1 + f2);
  }
}

che produce:

0,0007
3,567E-08
50,03


E' il momento di affrontare i decimal, alias dietro il quale si nasconde la potente struttura System.Decimal. I decimal, in pratica floating point definiti su 128 bit,  hanno il loro punto di forza nella grande precisione, tale da raggiungere le 28-29 cifre. Potete facilmente costruirvi qualche esempio in cui effettuate le divisioni tra single, double e decimal con gli stessi valori di partenza per comprendere la differenza. Tutto ciò rende i decimal perfetti per gli ambiti finanziari ma anche per quelli scientifici ad alta precisione. Di contro sono grassi e grossi, quindi decisamente pesanti ed ingombranti ed hanno un range piuttosto limitato che si estende da:
minimo = -79228162514264337593543950335
massimo = 79228162514264337593543950335


quindi più ristretto anche di quello dei float.
Da un punto di vista pratico, un decimal è composto da:

- 1 bit per il segno
- 96 bit per rappresentare un numero intero (coefficente)
- 31 come fattore di dimensionamento che specifica la parte del coefficiente che è una frazione decimale. In realtà di questi si usano solo 8 bit.

Piano piano capiremo come funziona questa cosa.
I decimal devono essere specificati, se dichiarati direttamente, tramite il suffisso m o M.

decimal m1 = 5.7M;

oppure possiamo costruirlo in maniera formale con la seguente sintassi:

decimal nomedecimal = new decimal(valore);

laddove il valore può essere di tipo intero o con virgola.

Esiste anche un costruttore siffatto:

decimal nomedecimal = new decimal(int-1, int,-2 int-3, bool, byte);

dove i parametri rappresentano:

int-1 è un integer32 rappresentante i 32 bit iniziali
int-2 è un integer32 rappresentante i 32 bit mediani
int-3 è un integer32 rappresentante i 32 bit finali
bool è un valore booleano che indica se il numero è negativo (true) o positivo (false)
byte è una potenza di 10 da 0 a 28 (viene sollevata una eccezione per valori maggiori)

ad esempio se scrivete una cosa così:

decimal d1 = new decimal(7, 8, 9, true, 14);
Console.WriteLine(d1);

ottenete:
-1660206,96697745702919

potete fare altre prove per comprendere di più di questo modo un po' inconsueto di costruzione di un numero. Tenete però presenete che è così che lavora IL al suo interno, con quest'ultima, apparentemente bizzarra rappresentazione e lo vedremo al termine del paragrafo.

Le proprietà esposte dalla struct decimal sono le seguenti, le vediamo tramite un esempio:

  Esempio 8.8
1
2
3
4
5
6
7
8
9
10
11
12
using System;
class Test
{
  public static void Main()
  {
    Console.WriteLine(decimal.MaxValue);
    Console.WriteLine(decimal.MinusOne);
    Console.WriteLine(decimal.MinValue);
    Console.WriteLine(decimal.One);
    Console.WriteLine(decimal.Zero);
  }
}

Direi che sono abbastanza autoesplicative, l'output è ovvio:

79228162514264337593543950335
-1
-79228162514264337593543950335
1
0

Ci si può chiedere perchè decimal esponga proprio i valori 1, 0 e -1 quando potrebbe tranquillamente farne a meno. Sinceramente non ho approfondito più di tanto l'argomento e anche su testi piuttosto corposi non se ne fa il minimo accenno. Da quel che ho capito, per quel poco che risulta in base alle scarne ricerche che ho fatto sulla rete, si tratta di una peculiarità del tipo che vine gestita dal framework per garantire compatibilità con linguaggi che non supportano il tipo decimal. Non è un gran che come risposta, lo so, se avete suggerimenti migliori mandatemi due righe, le pubblicherò a vostro nome. Se ricordo bene anche in Java il tipo decimal espone queste proprietà. Per i nostri scopi attuali la questione è irrilevante.
Prima di vedere i numerosi metodi legati a questa struct, occupiamoci della sua interazione con altri tipi di numero. Il seguente codice, non compila:

double dc1 = 8.9;
decimal dc2 = 7.33m;
Console.WriteLine(dc1 / dc2);


e l'errore è il seguente:

error CS0019: Operator '/' cannot be applied to operands of type
'double' and 'decimal'


La cosa non funziona nemmeno con le altre operazioni di base mentre va tutto bene se usate un intero al posto del double oppure, ovviamente, un altro decimal. Perchè la cosa funzioni dovrete ricorrere ad un cast esplicito.

Console.WriteLine((decimal)dc1 / dc2);

E passiamo ora ai numerosi metodi che potrete usare sui decimal, dando uno sguardo ai più significativi e ricordando, come sempre, che sul sito MSDN trovate di tutto e di più in merito a questi metodi:

Add - statico - permette la somma di due decimal. Svolge in pratica la stessa funzione del consueto +.
decimal dc1 = 0.8123m;
decimal dc2 = 3.457m;
Console.WriteLine(decimal.Add(dc1,dc2));


Ceiling - statico - restituisce il più piccolo intero maggiore del decimal dato

Compare, CompareTo - la prima è statica la seconda no, restituiscono -1,0+1 a seconda che nel confronto tra due decimale siano più grande il secondo, siano uguali o sia più grande il primo.
decimal dc1 = 0.8123m;
decimal dc2 = 0.812m;
Console.WriteLine(dc1.CompareTo(dc2));

Il risultato è 1.

Divide - statico - divide due decimali ma forza anche il risultato di una divisione tra interi (non single e nemmeno double) ad entrare nel campo dei decimali. Eseguendo il seguente codice:

decimal dc1 = 0.8123m;
decimal dc2 = 0.712m;
Console.WriteLine(decimal.Divide(dc1,dc2));
int x = 7;
int y = 6;
var z = decimal.Divide(x,y);
Console.WriteLine(z.GetType());

otteniamo
1,1408707865168539325842696629
System.Decimal

Floor - static - al contrario di ceiling ci restituisce il più grande intero minore del decimal dato.

GetBits - statico - interessante metodo che permette di esporre le parti costitutive di un decimal. In particolare, restituisce un array di 4 elementi che rappresentano nell'ordine, i primi 32 bit, i 32 bit mediani, i 32 bit finali ed il fattore di scala. Utile per capirne di più su come sono fatti questi numeri. Esempio pratico, facilmente modificabile a vostro piacimento:

using System;
class Test
{
  public static void Main()
  {
    decimal dc1 = 0.8123m;
    int[] x = decimal.GetBits(dc1);
    Console.WriteLine(x.Length);
    Console.WriteLine(x[0]);
    Console.WriteLine(x[1]);
    Console.WriteLine(x[2]);
    Console.WriteLine(x[3]);
  }
}

Multiply - statico - identico a Divide, visto prima, con l'unica evidente differenza di effettuare una moltiplicazione invece di una divisione. Applicabile anch'esso sugli interi ma non sui single e sui double.

Negate - statico - resituisce il decimal moltiplicato per -1.

Parse - statico - converte da stringa a decimal. In caso di errore viene sollevata una eccezione.
decimal dc1 = decimal.Parse("3,14");
Ovviamente l'input può essere una stringa qualunque, proveniente da file, db, tastiera ecc...

Remainder - statico - resituisce il resto della divisione di due decimal

Round - statico - arrotonda all'intero più vicino. Di questo metodo esistono alcune interessanti varianti che suggerisco di guardare su MSDN.

Subtract - statico - simile ad Add ma effettua una sottrazione invece di una addizione.

Abbiamo poi una serie di possibilità di conversione abbastanza chiaramente indicate dal nome dei rispettivi metodi:
ToByte, ToDouble, ToInt16, ToInt32, ToInt64, ToSByte, ToSingle, ToUint16, ToUint32, ToUint64.
Si tratta di metodi statici per i quali bisogna fare attenzione a possibili overflow.

TryParse - come per i single e i double effettua una conversione da stringa a decimal senza sollevare eccezione in caso di fallimento dell'operazione.

Attenzione che anche i decimal hanno le loro insidie, l'esempio arriva diretto da MSDN:

decimal dc1 = decimal.One;
Console.WriteLine((dc1 / 3) * 3);


Il risultato di questo frammento di codice è
0,9999999999999999999999999999
e non 1, come sarebbe logico attendersi. Questo non accade se usate i float e i double. Il tipo decimal è adatto per quelle computazione che necessitano di minimizzare gli errori di arrotondamento. In casi come quello dell'esempio e tipicamente è quello finanziario l'ambito ideale, i decimal andrebbero utilizzati in congiunzione con Round. Come detto anche in precedenza, i double sono nella stragrande maggioranza dei casi, il tipo in virgola mobile ideale.
Diamo ora uno sguardo a come IL gestisce i nostri decimal:

using System;
class Test
{
  public static void Main()
  {
    decimal dc1 = 22.45M;
  }
}

ed ecco come il semplice programma precedente viene gestito passato al runtime, per la parte riguardante il Main

.method public hidebysig static void Main() cil managed
{
.entrypoint
// Code size 17 (0x11)
.maxstack 5
.locals init (valuetype [mscorlib]System.Decimal V_0)
IL_0000: nop
IL_0001: ldc.i4 0x8c5
IL_0006: ldc.i4.0
IL_0007: ldc.i4.0
IL_0008: ldc.i4.0
IL_0009: ldc.i4.2
IL_000a: newobj instance void [mscorlib]System.Decimal::.ctor(int32,
int32,
int32,
bool,
uint8)
IL_000f: stloc.0
IL_0010: ret
} // end of method Test::Main


In rosso ho evidenziato le parti secondo me più interessanti. Noterete subito come 0x8c5 non sia altro che 2245 in esadecimale e come l'ultima istruzione in rosso suggerisca di lo spostamento di due cifre decimali. L'istruzione relativa al segno è quella che inizia con IL_0008. Provate a vedere,se vi interessano questi dettagli interni, cosa esce con i numeri negativi. Da qui tuttavia si evince che in realtà il framework gestisce in maniera piuttosto complicata al suo stesso interno i decimal che non sono un tipo nativo. La loro lentezza è quindi determinata anche da questa gestione un po' macchinosa. Inoltre, il fatto di non essere nativi, determina che eventuali azioni sezioni cheked non impediranno la generazione di eccezioni.

Questi tipi di numero, come è evidente anche dai semplici esempi che abbiamo visto, sono potenti, importanti ma non scevri di qualche sorpresa o insidia. Un capitolo importante è quello della conversione tra di essi con relativo discorso riguardante la possibile perdita di informazione ad esempio passando da un double ad un float. Ci ritorneremo sopra, per intanto, e vi incoraggio fortemente in questo senso, la lettura di esempi, ce ne sono a vagonate on line e le prove personali sono a mio avviso necessari per avere un po' di dimestichezza con questo fondamentale argomento.