C#

GLI INTERI

Eccoci ora ad affrontare uno dei capitoli più importanti di ogni linguaggio di programmazione che si rispetti, i numeri interi. Inutile parlare della loro utilità e della loro onnipresenza in ambito informatico. A volte si sottovaluta persino la loro "forza". Comunque partiamo subito dicendo che in C#, e in .Net in generale gli interi sono rappresentati attraverso varie struct e relativi alias che trovate nella tabella qui di seguito:

Struct Alias Descrizione
System.Sbyte sbyte Interi con segno su 8 bit.
Range: -128 +127
System.Byte byte Interi senza segno su 8 bit.
Range: 0 +255
System.Int16 short Interi con segno su 16 bit.
Range: -32768 +32767
System.UInt16 ushort Interi senza segno su 16 bit.
Range: 0 +65535
System.Int32 int Interi con segno su 32 bit.
Range: -2147483648 +2147483647
System.UInt32 uint Interi senza segno su 32 bit
Range: 0 +4294967295
System.Int64 long Interi con segno su 64 bit
Range: -9223372036854775808 +9223372036854775808
System.Uint64 ulong Interi senza segno su 64 bit
Range: 0 +18446744073709551615

Evidentemente la prima differenza che salta all'occhio sta nella diversa occupazione di memoria di cui i vari tipi necessitano; è naturale che lavorare con il tipo sbyte sia ben meno oneroso rispetto all'uso dei long e alla lunga, un buon uso di questi formati più ridotti può garantire in programmi complessi un significativo risparmio di risorse; d'altra parte quando si sale nei valori è necessario usare un tipo più espandibile. Tra l'altro, come vedremo, questa è una cosa che va ben calibrata quando programmate perchè, a meno di non ricorrere a tecniche un po' più evolute, con manipolazioni di base si corre qualche rischio. Inoltre si può notare che il diverso range tra tipi con e senza segno anche aventi la medesima ampiezza in termini di byte; questo avviene perchè nei tipi con segno un bit viene utilizzato per il segno stesso. Per quanto a questo punto sia abbastanza scontato è evidente che, come nel caso dei booleani e dei caratteri, anche la definzione di un intero corrisponde all'istanziazione di una struttura. Parimenti, anche per questi tipi è previsto un valore di default che è 0. I tipi uint, long e ulong dispongono di un suffisso per così dire personalizzato che li identifica a vista, rispettivamente U, L e UL (ad esempio 10UL assegnerebbe la variabile a cui attribuiamo il valore 10 ai long senza segno) ma in realtà non si usano quasi mai dal momento che la corretta attribuzione del tipo può avvenire per inferenza o per conversione implicita.
Altra importante considerazione è che sbyte, ushort, uint e ulong non sono CLS compliant. Personalmente cerco di evitarne l'uso.
Tutte le strutture che rappresentano i nostri numeri interi hanno delle proprietà e dei metodi comuni; se ad esempio non vi ricordate i massimi ed i minimi ecco pronte all'uso MaxValue e MinValue, ad esempio:

Console.WriteLine(Int32.MaxValue);
Console.WriteLine(Int64.MinValue);

La dichiarazione di un intero può avvenire in vari modi. Il più semplice è:

int nomeintero [= valore]

la parte tra parentesi quadra è opzionale ma bisognerà attribuire un valore alla variabile prima di usarla. Una forma alternativa è:

System.Int32 nomevariabile = new System.Int32()

chiaramente meno economica (anche la parola System può essere eliminata tramite l'opportuna adozione di using in capo al programma) che attribuisce direttamente il valore 0 alla variabile associata. Questa forma prevede quindi una istanziazione diretta. Molto spesso peraltro un numero viene immesso come input da tastiera o altra fonte risultando in formato stringa e allora bisogna ricorrere a qualche operazione di trasformazione; come per i booleani e i char possiamo ricorrere a due metodi, ovvero Parse e TryParse. Nell'esempio che segue useremo anche CompareTo, che confronta due interi ed equals, ereditato da Object (ricordiamoci sempre di questa pesante "figura paterna" e dei metodi che mette a disposizione) :

  Esempio 7.1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
using System;
class Program
{
  public static void Main()
  {
    string s1 = "11";
    int i1 = Int32.Parse(s1);
    int i2 = 10;
    Console.WriteLine(i1.CompareTo(i2));
    Console.WriteLine(i1.Equals(i2));
    bool b;
    int i3;
    b = Int32.TryParse("aa", out i3);
    Console.WriteLine(b);
    Console.Write("Inserisci un numero: ");
    int inp = Int32.Parse(Console.ReadLine());
    Console.WriteLine("Numero inserito: " + inp.ToString());
  }
}

Il funzionamento lo abbiamo già visto nei paragrafi precedenti, in questo caso non c'è nulla di diverso. Inversamente,se cioè volete passare da intero a stringa ricordatevi che c'è sempre il comodo metodo ToString() ereditato anche questo da Object. Inoltre, naturalmente, il confronto tra numeri lo potete fare con l'operatore == invece di usare Equals, potete a questo proposito dare uno sguardo al corposo paragrafo dedicato agli operatori. Da ricodare che i metodi visti nell'esempio e gli altri ai quali abbiamo accennato, sono disponibili non solo per Int32 ma anche per tutti i tipi numerici della tabella precedente.

Le operazioni aritmetiche di base sono sempre le solite, diamo giusto uno sguardo veloce tramite un frammento di codice:

int x1 = 10;
int x2 = 3;
Console.WriteLine(x1 + x2);
Console.WriteLine(x1 - x2);
Console.WriteLine(x1 * x2);
Console.WriteLine(x1 / x2);
Console.WriteLine(x1 % x2);

abbiamo, nell'ordine, la somma, la sottrazione, la moltiplicazione, la divisione (si usa l'operatore / ) ed il resto della divisione  (individuato dall'operatore % ) quindi 10 % 3 ci restituisce 1. Come detto, nulla di nuovo. Il risultato della divisione è la parte intera della stessa (non mi piace ma è così) vedremo quando parleremo dei numeri in virgola come ovviare a questa cosa e ottenere il risultato giusto. Il compilatore intercetta le forme più semplici di divisioni per 0, ad esempio cose come:

x2 = x1 / 0;
oppure
x2 = x1 / (5 - 5);


non ve le fa passare, diversamente, se si tratta di espressioni non valutabili a compile time, viene generata un'eccezione (per le eccezioni c'è il paragrafo dedicato).
In coda a questo paragrafo parleremo di come avere disponibili altre funzioni matematiche anche molto avanzate.

Abbiamo accennato a qualche insidia che nasce dal modo in cui i numeri sono rappresentati. Premesso che questa criticità non è peculiare di C# ma la troviamo anche in molti altri linguaggi che presentano diverse tipologie di interi, vediamo quanto segue:

byte b1 = 100;

questa istruzione è normale e corretta. Se però scrivessimo:

byte b2 = 300;

il compilatore si lamenterebbe:

error CS0031: Constant value '300' cannot be converted to a 'byte'

e questo è logico: il tipo byte, essendo definito su 8 bit senza segno, ha un valore massimo di 255 e 300 è al di fuori della sua capacità di rappresentazione. Tuttavia proviamo a scrivere questo frammento di codice:

byte b1 = 100;
b1 += 200;
Console.WriteLine(b1);


Il risultato è 44... mica tanto giusto.... il perchè è presto detto: 100 + 200 fa ovviamente 300 la cui rappresentazione binaria è:
100101100
tuttavia questo numero è composto da 9 bit mentre il nostro povero byte ne gestisce 8 per cui quello che viene memorizzato in b1 è
00101100
ovvero 44 in decimale. In maniera più grossolana 44 è anche il resto della divisione 300 / 256. Insomma un risultato sbagliato che il compilatore, al contrario di quanto visto con l'assegnazione diretta, fa passare senza problemi. La cosa interessante però è che se anzichè usare la forma compatta += usiamo quella esplicita:

b1 = b1 + 200;

il compilatore si arrabbia di nuovo e si arrabbia anche se inserire al posto di 200 un valore che sia valido per resta nell'ambito dei byte, anche se sommato 1, ad esempio. Evidentemente c'è dietro un modo diverso di lavorare. Andiamo a guardare il codice IL tramite ILDASM nel caso in cui si adoperi l'operatore +=:

IL_0000: nop
IL_0001: ldc.i4.s 100
IL_0003: stloc.0
IL_0004: ldloc.0
IL_0005: ldc.i4 0xc8
IL_000a: add
IL_000b: conv.u1
IL_000c: stloc.0
IL_000d: ret


Le righe evidenziate sono quelle critiche:

* viene allocato il valore 100,
* poi il 200 (0xC8 è esadecimale per 200)
* viene effettuata la somma
* l'ultima riga in rosso mette il valore nello stack effettuando una conversione ad Int8 (u1 internamente è Int8)

Il secondo metodo invece (b1 = b1 + 200) per questioni di efficienza esegue una immediata conversione a Int32 o Int64 (vale per qualsiasi operazione) della parte destra dell'espressione ma successivamente non può, in questo modo, esposrtare il risultato in un bytre. Il tutto sarà un po' più chiaro più avanti e comunque è spiegato nelle specifiche profonde del linguaggio. In pratica scrivere:

b1 = b1 + 200 equivale a b1 = (int)b1 + 200 lavorando sugli interi quindi sarebbe necessario scrivere b1 = (byte)(b1 + 200) per far funzionare di nuovo le cose.
Per quanto riguarda l'altra scrittura invece

x += y viene interpretata come x = (tipo)(x + y) in cui (tipo) non è intero generico ma quello di x. Questo vale per byte, sbyte, short, ushort e anche char.

Quando parleremo dei cast tutto questo sarà un po' più chiaro. In linea di massima prediligo la seconda scrittura se devo fare le cose semplici. La scrittura compatta è probabilmente (mai fatto dei test, personalmente) più veloce perchè comporta una sola valutazione di b1, ma è, a mio avviso, meno sicura per quanto visto. Questo fatto è comunque da conoscere e da tenere ben presente ed è importante che abbiate ben scolpite nella memoria le conversioni implicite ed esplicite alle quali è dedicato un paragrafo e sono ad ogni modo ben spiegate e facilmente reperibili su MSDN.

Insomma bisogna fare attenzione, se avete il sentore che i risultati possano essere più ampi della magnitudo del tipo che vorreste usare è meglio stare larghi e sprecare un po' di memoria.

Qualora abbiate dei dubbi sulla possibilità che un'operazione possa dare le problematiche anzi descritte si può marcare la sezione di programma che volete tenere programmata tramite la keyword checked. Di essa riparleremo nel paragrafo dedicato alle eccezioni ma qui possiamo tranquillamente anticiparla, non è nulla difficile. Tale istruzione, che rallenta un po' il programma in senso assoluto anche se in programmi di esempio la cosa è del tutto trascurabile, origina un'eccezione se vengono violati i limiti accettabili per un tipo. Vediamo l'esempio:

  Esempio 7.2
1
2
3
4
5
6
7
8
9
10
11
12
13
using System;
class Program
{
  public static void Main()
  {
    byte s1 = 100;
    checked
    {
      s1 += 200;
    }
    Console.WriteLine(s1);
  }
}

Se eseguite questo programma ottenete una eccezione di tipo

Unhandled Exception: System.OverflowException: Arithmetic operation resulted in
an overflow.
at Program.Main()

La sezione dalla riga 7 alla 10, dove si chiude il checked, è sottoposta quindi ad attenzioni particolari. L'uso di Checked ve lo consiglio caldamente, così come quello dei tipi con segno che il compilatore gestisce meglio. Per inciso, a livello di IL in presenza di una sezione checked viene usata per la somma l'istruzione add.ovf che effettua un controllo sull'overflow di tipo, in luogo di add, vista prima.
Come ulteriore alternativa, se volete che tutto il programma sia sottoposto a rigorose verifiche in questo senso poteve attivare lo switch /checked+ in fase di compilazione per versioni di debug del vostro programma.

In qualche modo collegata al problema appena visto sorge spontanea un'altra considerazione: ok, posso lavorare su interi definiti fino a 64 bit che mi garantiscono un buon numero di valori... però... se i valori di cui dispongo grazie ai long non fossero sufficienti a coprire le mie necessità? Le possibilità sono due: o passare ai valori in virgola, che garantiscono il supporto ad un range ancora più grande, oppure usare i numeri BigInteger. Non esiste un alias per questa struttura come ad esempio è presente invece in F# (che prevede la keyword bigint) e per sfruttarli bisogna includere il namespace Numerics (potrebbe essere necessario aggiungere un riferimento alla DLL relativa). Una volta fatto questo è possibile dichiarare un valore BigInt in almeno due modi semplici:

BigInteger x1 = new BigInteger(10000);
int i1 = 700;
BigInteger x2 = i1;

I BigInter non hanno limitazione nè per quel che riguarda i valori negativi nè per i positivi. a parte quelli invalicabili imposti dall'hardware sottostante. Essi inoltre possono importare valori partendo da interi o numeri in virgola (in tal caso perdendo la parte decimale), senza problemi. Il loro valore di default è 0, in linea con gli "altri" interi Controindicazioni? Certamente, a dirla tutta, sono elementi molto pesanti in termini di occupazione memoria e di velocità, per cui non c'è nessuno che li usa solo per il gusto di stare tranquillo per quel che riguarda i valori disponibili. Essi hanno una gran numero di metodi che potete consultare sul solito MSDN. Forse nella vostra vita professionale userete raramente i BigInt ma una loro conoscenza di base e qualche prova del loro uso è comunque doverosa.

La rappresentazione degli interi, oltre che nella classica forma decimale, è ammessa anche nel formato esadecimale introdotto dal prefisso 0x.

int x1 = 10;
int x2 = 0xef; // 239
Console.WriteLine(x1 + x2);
Console.WriteLine(x1 * x2);


il trattamento è identico a quello di un normale intero, l'output di deafult sarà in formato decimale. Il formato decimale e quello esadecimale sono gli unici  direttamente supportati. In C# 6.0, non disponibile al momento in cui scrivo, dovrebbe essere previsto il supporto nativo per formato binario.
Gli interi possono essere sottoposti alle opportune formattazioni in fase di output ma di quelle parleremo in un apposito paragrafo.

Parlando delle operazioni abbiamo detto che è possibile avere a disposizione altre opportunità operative. Il framework ci mette infatti a disposizione all'interno del namespace System, la classe Math ovviamente utilizzabile su tutte le tipologie di numero, non solo sugli interi, che contiene numerosi metodi pronti all'uso. Su MSDN trovate tutti i riferimenti, di seguito fornisco un esempio di base:

  Esempio 7.3
1
2
3
4
5
6
7
8
9
10
11
12
13
14
using System;
class Program
{
  public static void Main()
  {
    int x1 = -2;
    int x2 = 6;
    int x3 = 8;
    Console.WriteLine(Math.Abs(x1));      // valore assoluto
    Console.WriteLine(Math.Pow(x2, x3));  // elevamento a potenza
    Console.WriteLine(Math.Sqrt(x2));     // radice quadrata
    Console.WriteLine(Math.Min(x1,x2));   // minimo tra due numeri interi
  }
}

Come potete vedere l'uso è del tutto simmetrico a quanto mettiamo in pratica con la classe Console, in effetti Math.Pow è del tutto analoga a Console.WriteLine(), quindi non ci sono difficoltà. Math è come detto pensata come utilizzo generale, alcuni metodi si applicano solo su alcune tipologie di numero, naturalmente. L'elevamento a potenza, metodo Pow, funziona con la sintassi Pow(base, esponente). Anche per l'uso di queste funzioni bisogna fare un po' di attenzione... se ad esempio x1 valesse 1000 e x2 valesse 50 l'elevamento a potenza resituire un risultato probabilmente poco utile. In casi come questi un cast a BigInteger potrebbe aiutare:

Console.WriteLine((BigInteger)Math.Pow(x1,x2));

In rete troverete poi molte altre librerie dedicate al mondo .Net per quel che riguarda funzioni matematiche evolute, segnalo ad esempio di progetto Math.Net.

Non c'è molto altro da dire relativamente agli interi, categoria di numeri concettualmente banale ma, come visto, non priva di interesse e di qualche insidia. Fate le vostre prove per testare le varie particolarità che potrebbero presentarsi e che, di solito, capitano a tradimento....