Tipi Ordinali e Interi in Object Pascal

I tipi ordinali sono categorie di tipi per cui è definito un ordine. In pratica, se x e y sono due variabili dello stesso tipo ordinale, è sempre possibile stabilire se x è minore di y, ossia se ha senso l'espressione x < y.

In Object Pascal esistono tre tipi ordinali built-in:

  • I tipi interi
  • il tipo Char
  • il tipo Boolean

In questa lezione ci occuperemo dei tipi interi e vedremo gli altri tipi ordinali nelle prossime lezioni.

Concetti Chiave
  • I tipi ordinali sono tipi di dati che hanno un ordine definito tra i loro valori.
  • In Object Pascal, i tipi ordinali includono tipi interi, Char e Boolean.
  • I tipi interi in Object Pascal variano per dimensione (8, 16, 32, 64 bit) e per la presenza o meno del segno (signed/unsigned).
  • Ogni tipo intero ha un intervallo specifico di valori che può rappresentare, e tentare di assegnare un valore fuori da questo intervallo può causare errori di overflow.
  • Object Pascal fornisce operazioni e funzioni built-in per lavorare con tipi interi, come ToString, Size, Inc, Dec, Low, High, e altre.
  • È possibile abilitare il controllo di overflow a runtime per rilevare errori di overflow durante l'esecuzione del programma.

Tipi Interi

Ogni linguaggio di programmazione che si rispetti permette allo sviluppatore di usare tipi numerici interi. Si parla di tipi interi al plurale in quanto, spesso, esistono diversi tipi di intero a seconda di come i numeri sono internamente rappresentati. Nel dettaglio, ciò che cambia è il numero di bit utilizzati e la presenza o meno del segno.

L'Object Pascal supporta un ampio spettro di tipi interi, riportati nella tabella seguente.

Da notare che oltre al nome, questi tipi hanno un alias, un nome alternativo che può essere usato al posto del nome vero e proprio. Questi alias sono più semplici da ricordare perché presentano nel nome sia il numero di bit usati (ad es. Int16 per un intero a 16 bit e Int32 per un intero a 32), sia se la rappresentazione prevede la presenza del segno o meno (un intero senza segno è indicato da una U anteposta al nome che sta per Unsigned ossia senza segno).

Dimensione Con Segno Nome Alias Valore Minimo Valore Massimo
8 Bit No Byte UInt8 0 255
8 Bit Si ShortInt Int8 -128 127
16 Bit No Word UInt16 0 65,535
16 Bit Si SmallInt Int16 -32,768 32,767
32 Bit No Cardinal UInt32 0 4,294,967,295
32 Bit Si Integer Int32 -2,147,483,648 2,147,483,647
64 Bit No UInt64 0 2^{64}
64 Bit Si Int64 -2^{63} 2^{63} - 1
Tabella 1: Tipi interi in Delphi e range di rappresentazione

Una nota sul tipo Integer.

Nelle versioni a 64 bit di Object Pascal, ci si potrebbe sorprendere che il tipo Integer è a 32 bit. Il fatto che sia a 32 bit è dovuto principalmente alle seguenti ragioni:

  • La maggior parte del codice Object Pascal esistente è stato scritto per piattaforme a 32 bit, e cambiare la dimensione di Integer a 64 bit romperebbe la compatibilità con il codice esistente.
  • Le operazioni sui numeri a 32 bit sono generalmente più veloci delle operazioni sui numeri a 64 bit (anche se questo dipende dalla CPU specifica).
  • La maggior parte delle API di sistema e delle librerie di terze parti con cui interagisce il codice Object Pascal utilizzano ancora tipi a 32 bit per gli interi.

Altri tipi come il tipo Pointer (di cui parleremo più avanti nelle prossime lezioni) e altri tipi di riferimento correlati sono a 64 bit. Questo perché devono essere in grado di indirizzare l'intero spazio di memoria su una piattaforma a 64 bit.

In ogni caso, se si ha la necessità di un tipo numerico che si adatti alla dimensione del puntatore e alla piattaforma CPU nativa, si possono usare i due tipi alias speciali NativeInt e NativeUInt. Questi hanno la stessa dimensione di un puntatore sulla piattaforma specifica (cioè, 32 bit su piattaforme a 32 bit e 64 bit su piattaforme a 64 bit).

La situazione cambia per LargeInt, che viene spesso utilizzato per mappare le funzioni API della piattaforma nativa. Questo è a 32 bit su piattaforme a 32 bit e su Windows 32 bit, mentre è a 64 bit su piattaforma ARM a 64 bit.

Funzioni di ausilio per Interi

I tipi Integer e gli altri tipi interi in Object Pascal sono tipi primitivi, il che significa che non sono oggetti ma tipi di dati nativi del linguaggio. Questa scelta progettuale è stata fatta per motivi di efficienza, in quanto i tipi di dati nativi sono più veloci da manipolare rispetto agli oggetti.

Nonostante ciò, è possibile operare su variabili (e valori costanti) di questi tipi con operazioni che si applicano usando la notazione punto. Questa è la notazione generalmente utilizzata per applicare metodi agli oggetti.

Come anticipazione, la notazione punto ha una sintassi di questo tipo:

variabile.operazione(parametri)

Dove variabile è una variabile di un certo tipo, operazione è il nome dell'operazione che si vuole eseguire su di essa, e parametri sono eventuali parametri richiesti dall'operazione.

Per chiarire meglio, osserviamo il codice seguente:

var
    N: Integer;
begin
    // Assegna un valore alla variabile
    N := 10;

    // Trasforma il valore in stringa e lo mostra a video
    WriteLn(N.ToString);

    // Mostra i byte richiesti per memorizzare il tipo
    WriteLn(Integer.Size.ToString);
end.

L'output di questo codice sarà:

10
4

Di seguito sono elencate le operazioni che è possibile eseguire sui tipi interi:

Operazione Descrizione
ToString Converte il numero in una stringa, usando il formato decimale
ToBoolean Converte nel tipo Boolean
ToHexString Converte in una stringa, usando il formato esadecimale
ToSingle Converte nel tipo di dati a virgola mobile single
ToDouble Converte nel tipo di dati a virgola mobile double
ToExtended Converte nel tipo di dati a virgola mobile extended
Tabella 2: Operazioni sui tipi interi in Object Pascal

La prima e la terza operazione convertono il numero in una stringa, usando una rappresentazione decimale o esadecimale. La seconda è una conversione a Boolean, mentre le ultime tre sono conversioni ai tipi a virgola mobile che vedremo nelle prossime lezioni.

Ci sono altre operazioni che si possono applicare al tipo Integer (e alla maggior parte degli altri tipi numerici), come:

Operazione Descrizione
Size Il numero di byte richiesti per memorizzare una variabile di questo tipo
Parse Converte una stringa nel valore numerico che rappresenta, sollevando un'eccezione se la stringa non rappresenta un numero
TryParse Tenta di convertire la stringa in un numero
Tabella 3: Altre operazioni sui tipi interi in Object Pascal

Procedure standard per i tipi interi

Object Pascal fornisce tutta una serie di routine standard built-in per lavorare sui tipi interi oltre alle operazioni viste sopra. Queste routine sono definite per tutti i tipi ordinali, non solo per i tipi interi.

Alcuni esempi di queste routine sono:

  • SizeOf che restituisce il numero di byte richiesti per memorizzare una variabile del tipo di dati;
  • High e Low che restituiscono rispettivamente il valore più alto e più basso nell'intervallo del tipo di dati.

Proviamo ad applicare alcune di queste routine nel codice seguente in cui verifichiamo la dimensione in byte di alcuni tipi interi e ne stampiamo il valore più alto e più basso possibile:

program TestInteri;

var
    I: Integer;
    B: Byte;
    S: SmallInt;
    C: Cardinal;
    L: Int64;
begin
    WriteLn('Integer:  Size = ', SizeOf(I), ' bytes, Low = ', Low(I), ', High = ', High(I));
    WriteLn('Byte:     Size = ', SizeOf(B), ' bytes, Low = ', Low(B), ', High = ', High(B));
    WriteLn('SmallInt: Size = ', SizeOf(S), ' bytes, Low = ', Low(S), ', High = ', High(S));
    WriteLn('Cardinal: Size = ', SizeOf(C), ' bytes, Low = ', Low(C), ', High = ', High(C));
    WriteLn('Int64:    Size = ', SizeOf(L), ' bytes, Low = ', Low(L), ', High = ', High(L));
end.

Eseguendo questo codice, otterremo un output simile al seguente (i valori di Low e High possono variare a seconda della piattaforma):

Integer:  Size = 2 bytes, Low = -32768, High = 32767
Byte:     Size = 1 bytes, Low = 0, High = 255
SmallInt: Size = 2 bytes, Low = -32768, High = 32767
Cardinal: Size = 4 bytes, Low = 0, High = 4294967295
Int64:    Size = 8 bytes, Low = -9223372036854775808, High = 9223372036854775807

Le routine di sistema che funzionano sui tipi ordinali sono mostrate nella seguente tabella:

Nome Descrizione
Dec Decrementa la variabile passata come parametro, di uno o del valore specificato come secondo parametro opzionale
Inc Incrementa la variabile passata come parametro, di uno o del valore specificato come secondo parametro opzionale
Odd Restituisce True se l'argomento è un numero dispari. Per testare numeri pari, dovresti usare un'espressione not (not Odd)
Pred Restituisce il valore prima dell'argomento, cioè il predecessore, nell'ordine determinato dal tipo di dati
Succ Restituisce il valore dopo l'argomento, cioè il successore
Ord Restituisce un numero che indica l'ordine dell'argomento all'interno dell'insieme di valori del tipo di dati (usato per tipi ordinali non numerici)
Low Restituisce il valore più basso nell'intervallo del tipo ordinale
High Restituisce il valore più alto nell'intervallo del tipo di dati ordinale
SizeOf Restituisce il numero di byte richiesti per memorizzare una variabile del tipo di dati
Tabella 4: Routine di sistema per tipi ordinali

Vediamo alcune osservazioni sulle routine Inc e Dec. Entrambe le routine accettano uno o due parametri. Il primo parametro è sempre la variabile da incrementare o decrementare. Il secondo parametro, opzionale, specifica di quanto incrementare o decrementare la variabile. Se il secondo parametro non è specificato, la variabile viene incrementata o decrementata di uno. In ogni caso, le routine non restituiscono alcun valore.

Ad esempio, se si ha una variabile N di tipo Integer, le seguenti chiamate sono equivalenti:

Inc(N);    // Incrementa N di 1
Inc(N, 1); // Incrementa N di 1
Dec(N);    // Decrementa N di 1
Dec(N, 1); // Decrementa N di 1

Le chiamate di sopra sono equivalenti a:

N := N + 1; // Incrementa N di 1
N := N - 1; // Decrementa N di 1
Nota

Similarità con C/C++

Per chi proviene da C o C++, è importante notare che le routine Inc e Dec in Delphi non sono esattamente equivalenti agli operatori di incremento (++) e decremento (--) in C/C++. In particolare, Inc e Dec non supportano le versioni post-incremento e post-decremento, e non restituiscono valori. Inoltre, l'uso di Inc e Dec può essere più chiaro in alcuni contesti, specialmente quando si lavora con tipi ordinali non numerici.

Un'altra osservazione riguarda il fatto che tali routine sono ottimizzate per i tipi ordinali. Questo significa che il compilatore può generare codice più efficiente per queste operazioni rispetto a un'operazione aritmetica standard come N := N + 1. Ad esempio, se usiamo la routine High su una variabile di tipo Integer, il compilatore può sostituire la chiamata con il valore costante 32767 (o 2147483647 su piattaforme a 64 bit), evitando così una chiamata di funzione. Analogamente, se il compilatore vede una chiamata a Inc(N) dove N è di tipo Integer, e il valore di N è noto in fase di compilazione, può sostituire la chiamata con un'operazione di incremento diretta.

Overflow e tipi interi

Una variabile intera o ordinale ha solo un intervallo limitato di valori validi.

Se il valore che le viene assegnato è troppo grande, questo risulta in un errore. Analogamente, se viene assegnato un valore negativo a un tipo senza segno, questo risulta in un errore. Tali condizioni sono chiamate overflow (o underflow per i valori troppo piccoli).

Ci sono in realtà tre diversi tipi di errori che si possono incontrare con operazioni fuori intervallo.

Il primo tipo di errore è un errore del compilatore, che si verifica se si assegna un valore costante (o un'espressione costante) che è fuori intervallo. Per esempio, se scriviamo il codice che segue:

var
    N: Byte;
begin
    N := 100 + High(N);
end;

il compilatore genererà l'errore:

E1012 Constant expression violates subrange bounds

Il secondo scenario si verifica quando il compilatore non può anticipare la condizione di errore, perché dipende dai valori a runtime e quindi dallo stato del programma. Ad esempio, consideriamo il seguente codice:

Inc(N, High(N));

Il compilatore non genererà un errore perché c'è una chiamata di funzione, e il compilatore non conosce il suo effetto in anticipo (e l'errore dipenderebbe anche dal valore iniziale di N). In questo caso ci sono due possibilità. Per default, se si compila ed esegue questa applicazione, si ottiene un valore completamente illogico nella variabile (in questo caso l'operazione risulterà nel sottrarre 1). Questo è il peggior scenario possibile, perché non si ottiene alcun errore, ma il programma non è corretto.

Tuttavia, sia Delphi che FreePascal offrono un'opzione del compilatore chiamata "Overflow checking" che si attiva con le direttive:

{$Q+}

oppure

{$OVERFLOWCHECKS ON}

Attivando questa opzione, il compilatore inserisce del codice di controllo per rilevare le condizioni di overflow a runtime. Se si verifica un overflow, viene sollevata un'eccezione EIntOverflow, che può essere gestita con un blocco try..except. Studieremo la gestione delle eccezioni in una lezione successiva.

Bisogna tenere presente, però, che l'abilitazione del controllo di overflow può rallentare leggermente l'esecuzione del programma a causa del codice di controllo aggiuntivo.