Logo

Marco Cantù
Essential Pascal


Tradotto dall'inglese da: Paolo Rossi

Capitolo 4
Tipi di dato definiti dall'utente

Insieme alla nozione di tipo, una delle grandi idee introdotte dal linguaggio Pascal e' l'abilita' di definire nuovi tipi di dati in un programma. I programmatori possono definire i loro tipi di dati per mezzo dei type constructor, sono un esempio i tipi subrange, i tipi array, i tipi enumerativi, i tipi puntatore ed i tipi set. Il piu' importante tipo di dato definito dall'utente e' la classe, la quale e' parte delle estensione orientate agli oggetti dell'Object Pascal, non descritta in questo libro.

E' giusto pensare che i type contructor sono comuni a diversi linguaggi di programmazione, ma il Pascal e' stato il primo linguaggio ad introdurre l'idea in modo formale e molto preciso. Esistono solo pochi linguaggi con cosi' tanti meccanismi per definire nuovi tipi di dato.

Tipi con nome e senza nome

Questi tipi possono essere usati in seguito con il nome o applicati direttamente ad una variabile. Quando si da un nome ad un tipo, bisogna provvedere una specifica sezione nel codice come la seguente:

type
  // subrange definition
  Uppercase = 'A'..'Z';

  // array definition
  Temperatures = array [1..24] of Integer;

  // record definition
  Date = record
    Month: Byte;
    Day: Byte;
    Year: Integer;
  end;

  // enumerated type definition
  Colors = (Red, Yellow, Green, Cyan, Blue, Violet);

  // set definition
  Letters = set of Char;

Simili definizioni di tipo possono essere usate direttamente per definire una variabile senza un esplicito nome di tipo, come nel seguente codice:

var
  DecemberTemperature: array [1..31] of Byte;
  ColorCode: array [Red..Violet] of Word;
  Palette: set of Colors;

Nota: In generale, bisogna evitare di usare i tipi unnamed come nel codice sopra, visto che non si puo' passarli come parametri alle routine o dichiarare altre variabili dello stesso tipo. Le regole di compatibilita' di tipo del Pascal sono basate di fatto sui nomi del tipo, non sulla definizione corrente del tipo. Due variabili di due identici tipi non sono ancora compatibili, a meno che i loro tipi abbiano esattamente lo stesso nome, e ai tipi unnamed viene attribuito un nome interno dal compilatore. Bisogna abituarsi a definire un tipo di dato ogni volta che si ha bisogno di una variabile complessa, e sicuramente non ci si pentira' del tempo speso.

Ma cosa vogliono dire queste definizioni ? Daro' alcune definizioni per quelli che non hanno familiarita' con le dichiarazioni di tipo del Pascal. Tentero' anche di sottolineare le differenze dagli stessi costrutti negli in altri linguaggi di programmazione, cosi' la lettura di questa sezione sara' interessante anche per chi ha familiarita' con le definizioni mostrate sopra. Finalmente mostrero' alcuni esempi in Delphi ed introdurro' alcuni strumenti che permettaranno l'accesso alle informazioni sul tipo in modo dinamico.

Tipi Subrange

Un tipo subrange definisce un intervallo di valori entro un intervallo di un altro tipo(da qui il nome di subrange)). Si puo' definire un subrange del tipo Integer, da 1 a 10 o da 100 a 1000, oppuresi puo' definireun subrange del tipo Char, come in:

type
  Ten = 1..10;
  OverHundred = 100..1000;
  Uppercase = 'A'..'Z';

Nella definizione di un subange, non serve specificare il nome del tipo di base. Bisogna solo specificare due costanti di questo tipo. Il tipo originale deve essere un tipo ordinale e il tipo risultante deve assere ancora un tipo ordinale.

Una volta definito un subrange, si puo' legalmente assegnare ad esso un valore compreso in questo intervallo. Il codice seguente pertanto risulta valido:

var
  UppLetter: UpperCase;
begin
  UppLetter := 'F';

Il seguente invece non e' valido:

var
  UppLetter: UpperCase;
begin
  UppLetter := 'e'; // compile-time error

Il codice sopra produrra' un errore di run-time, "Constant expression violates subrange bounds." Se si scrive il seguente codice invece:

var
  UppLetter: Uppercase;
  Letter: Char;
begin
  Letter :='e';
  UppLetter := Letter;

Delphi lo compilera'. A run-time, se e' stato abilitato l'opzione del compilatore Range Checking (nella pagina Compiler della finestra Project Options), si otterra' questo messaggio d'errore: Range check error.

Nota: Suggerisco di attivare questa opzione mentre si sta sviluppando un programma, cosi' sara' piu' robusto e piu' facile il debug, visto nel caso di errori si si otterra' uno specifico messaggio e non un comportamento anomalo del programma. Eventualmente si puo' disabilitare questa opzione per la compilazione finale del programma, per generarlo piu' veloce e compatto. Comunque la differenza e' davvero limitata, e per questo suggerisco di lasciare tutte questa opzioni di controllo a run-time abilitate, anche nel programma finito. La stessa cosa vale per le altre opzioni di controllo a run-time tipo il controllo di overflow e dello stack.

Tipi Enumerativi

I tipi enumerativi costituiscono un altro tipo ordinale definibile dall'utente. Invece di indicare un intervallo di un tipo esistente, in un'enumerazione bisogna elencare tutti i possibili valori del tipo. In altre parole, un'enumerazione e' un elenco di valori. Ecco alcuni esempi:

type
  Colors = (Red, Yellow, Green, Cyan, Blue, Violet);
  Suit = (Club, Diamond, Heart, Spade);

Ogni valore nell'elenco ha una ardinalita' associata che parte da zero. Quando si applica la funzione Ord ad un valore di un tipo enumerativo, si ottiene questo valore zero-based. Per esempio Ord (Diamonds) ritorna 1.

Nota: I tipi enumerativi possono avere differenti rappresentazioni interne. Per default, Delphi usa una rappresentazione a 8 bit, a meno che non vi siano piu' di 256 valori, nel qual caso viene usata una rappresentazione a 6 bit. Esiste anche una rappresentazione a 32 bit, la quale puo' essere utile per compatibilita' con le librerie C o C++. Attualmente si puo' comunque cambiare questo comportamento di default, domandando una rappresentazione maggiore usando la direttiva di compilatore $Z.

La VCL (Visual Component Library) di Delphi usa i tipi enumerativi in deversi punti. Per esempio, gli stili del bordo di un form sono definiti come segue:

type
  TFormBorderStyle = (bsNone, bsSingle, bsSizeable,
    bsDialog, bsSizeToolWin, bsToolWindow);

Quando il valore di una proprieta' e' un'enumerazione, si puo' scegliere da una lista di valori visualizzata nell Object Inspector, come visibile in Figura 4.1.

Figura 4.1: Una proprieta' enumerativa nell'Object Inspector

L'help file di Delphi generalmente elenca i possibili valori di un'enumerazione. Come alternativa si puo' usare il programma OrdType, disponibile su www.marcocantu.com, per vedere la lista dei valori di ogni enumerazione, set, subrange e ogni altro tipo ordinale di Delphi. Si puo' vedere un esempio dell'output di questo programma in Figura 4.2.

Figura 4.2: Informazioni dettagliate riguardo i tipi enumerativi, come sono mostrati dal programma.

Tipi Set

I tipi set indicano un gruppo di valori, dove la lista dei valori disponibili e' indicata dal tipo ordinale su cui il tipo set e' basato. Questi tipi ordinali sono usualmente limitati, e abbastanza spesso rappresentati da un'enumerazione o un subrange. Se si prende il subrange 1..3, i possibili valori del set basato su esso includono solo 1, solo 2, solo 3, sia 1 che 3, sia 2 che 3, tutti i tre valori, o nessuno di essi.

Una variabile di solito contiene uno dei possibili valori dell'intervallo di questo tipo. Una variabile di tipo set, invece, puo' contenere nessuno, uno, due, tre o piu' valori dell'intervallo. La variabile set puo' anche includere tutti i valori. Ecco un esempio di un set:

type
  Letters = set of Uppercase;

Adesso posso definire una variabile di questo tipo ed assegnare alcuni valori del tipo originale. Per indicare i valori in un set, si puo' scrivere un elenco separato da virgole, racchiuso tra parentesi quadre. Il seguente codice mostra l'assegnazione ad una variabile di diversi valori, un singolo valore, e un valore vuoto:

var
  Letters1, Letters2, Letters3: Letters;
begin
  Letters1 := ['A', 'B', 'C'];
  Letters2 := ['K'];
  Letters3 := [];

In Delphi, un set e' generalmente usato per indicare un flag non esclusivo. Per esempio, le seguenti due linee di codice (che sono parte della libreria di Delphi) dichiarano un'enumerazione di icone per il bordo di una finestra e il corrispondente tipo set:

type
  TBorderIcon = (biSystemMenu, biMinimize, biMaximize, biHelp);
  TBorderIcons = set of TBorderIcon;

Di fatto, una data finestra puo' avere nessuna di queste icone, una, o piu' di una. Quando si lavora con l'Object Inspector (vedi Figura 4.3), si possono provvedere i valori di un set espandendo la selezione (doppio click sul nome della proprieta' o click sul segno di piu' sulla sinistra) e mettere on oppure off la presenza di ogni valore.

Figura 4.3: Una proprieta' di tipo set nell'Object Inspector

Un'altra proprieta' basata su un tipo set e' lo stile di un font. I possibili valori indicano un font in grassetto, in corsivo, sottolineato o barrato. Naturalmente lo stesso font puo' essere corsivo e grassetto, non avere nessun attributo oppure tutti gli attributi. Per questa ragione e' dichiarato come un set. Si possono assegnare valori a questo set nel codice del programma come segue:

Font.Style := []; // no style
Font.Style := [fsBold]; // bold style only
Font.Style := [fsBold, fsItalic]; // two styles

Si puo' anche operare su di un set in diversi modi, incluso aggiungere due variabili dello stesso tipo set (o, per essere piu' precisi, calcolare l'unione delle due variabili set):

Font.Style := OldStyle + [fsUnderline]; // two sets

Ancora, si puo' usare l'esempio OrdType per vedere la lista dei possibili valori di diversi set definiti nelle librerie di Delphi.

Tipi Array

I tipi array definiscono un elenco di un numero prefissato di elementi di uno specifico tipo. Generalmente si puo' usare un indice all'interno di parentesi quadre per accedere ad un elemento dell'array. Le parentesi quadre sono usate anche per specificare i possibili valori dell'indice quando l'array e' definito. Ad esempio, si puo' definire un gruppo di 24 numeri interi con il seguente codice:

type
  DayTemperatures = array [1..24] of Integer;

Nella definizione dell'array, bisogna passare un tipo subrange nelle parentesi quadre oppure definire un nuovo typo subrange usando due costanti di un tipo ordinale. Questo subrange specifica gli indici validi dell'array. Siccome si specifica sia l'estremo inferiore che quello superiore, l'indice non deve per forza essere zero-based, come invece necessario in C, C++, Java e altri linguaggi di programmazione.

Siccome gli indici dell'array sono basati su di un subrange, Delphi puo' controllare il loro intervallo come abbiamo gia' visto. Una costante subrange non valida produrra' un errore in compilazione e usare un indice fuori dai valori consentiti a run-time produrra' un errore di run-time se la corrispondente opzione del compilatore e' attivata.

Usando la definizione di array vista sopra, si puo' settare il valore di una variabile DayTemp1del tipo DayTemperatures come segue:

type
  DayTemperatures = array [1..24] of Integer;

var  
  DayTemp1: DayTemperatures;
  
procedure AssignTemp;  
begin  
  DayTemp1 [1] := 54;
  DayTemp1 [2] := 52;
  ...
  DayTemp1 [24] := 66;
  DayTemp1 [25] := 67; // compile-time error

Un array puo' avere piu' di una dimensione, come nel seguente esempio:

type
  MonthTemps = array [1..24, 1..31] of Integer;
  YearTemps = array [1..24, 1..31, Jan..Dec] of Integer;

Questi due tipi di array sono costruiti sugli stessi tipi base. In questo modo si possono dichiarare usando il precedente tipo di dato, come nel seguente codice:

type
  MonthTemps = array [1..31] of DayTemperatures;
  YearTemps = array [Jan..Dec] of MonthTemps;

Questa dichiarazione inverte l'ordine degli indici come mostrato sopra, ma permette anche assegnamenti di interi blocchi tra variabili. Ad esempio, la seguente istruzione copia la temperatura di gennaio in febbraio:

var
  ThisYear: YearTemps;
begin
  ...
  ThisYear[Feb] := ThisYear[Jan];

Si puo' anche definire un array zero-based , un array con il limite inferione uguale a zero. Generalmente, l'uso di limiti piu' logici e' un vamtaggio, siccome non serve usare l'indice 2 per acceder al terzo elemento, e cosi' via. Windows, tuttavia, usa invariabilmente array zero-based (visto che windows e' basato sul linguaggio C), e la libreria dei componenti di Delphi tende a fare lo stesso.

Se serve lavorare con un array, si puo' comunque testare i limiti con le funzioni standard Low e High, le quali ritornano il limite inferiore e superiore dell'array. Usare Low e High quando si opera su un array e' altamente consigliato, specialmente nei cicli, siccome rende il codice indipendente dall'intervallo dell'array. Successivamente, si puo' cambiare l'intervallo dell'array e il codice che usa Low e High funzionera' ancora. Se si scrive un ciclo con i valori correnti dell'intervallo, bisognera' aggiornare il codice quando la dimensione dell'array cambia. L'uso di Low e High rende il codice facile da mantenere e piu' affidabile.

Nota: A proposito, non c'e' una penalizzazione delle prestazioni a run-time quando si usano le funzioni Low e High con gli array. Esse sono risolte in costanti al momento della compilazione. Questa conversione da funzione a costante durante compilazione accade anche per altre semplici funzioni di sistema.

Delphi usa gli array principalmente nella forma di array di proprieta'. Si e' gia' visto un esempio di queste proprieta' nell'esempio TimeNow, per accedere alla proprieta' Items di un componente ListBox. Mostrero' ulteriori esempi di proprieta' array nel prossimo capitolo, quando si discuteranno i cicli.

Nota: delphi 4 introduce i dynamic array, che sono array che possono essere ridimensionati a run-time allocando una giusta quantita' di memoria. Usando i dynamic array e' facile, ma in questa discussione di Pascal ritengo che non siano un argomento adatto. Si puo' trovare una descrizione dei dynamic array di Delphi nel capitolo 8.

Tipi Record

I tipi record definiscono una collezione fissa di elementi di tipi differenti. Ogni elemento, o campo, ha il proprio tipo. La definizione di un tipo record elenca tutti questi campi, dando ad ognumo un nome che verra' usato piu' tardi per accedervi.

Ecco un piccolo listato con la definizione di un record, la dichiarazione di una variabile di questo tipo, e qualche istruzione che usa questa variabile:

type
  Date = record
    Year: Integer;
    Month: Byte;
    Day: Byte;
  end;
  
var
  BirthDay: Date;
  
begin
  BirthDay.Year := 1997;
  BirthDay.Month := 2;
  BirthDay.Day := 14;

Le classi e gli oggetti possono essere considerati un'estensione del tipo record. Le librerie di Delphi tendono ad usare i tipi classe piuttosto che i tipi record, ma ci sono diversi tipi record definiti nelle API di Windows.

I tipi record possono avere anche una parte variabile, cioe': campi multipli possono essere mappati sulla stessa area di memoria, anche se hanno differenti tipi (Questo corrisponde al tipi union nel linguaggio C). Alternativamente, si possono usare questi campi varianti o gruppi di campi per accedere alla stessa locazione di memoria dentro ad un record, ma considerando questi valori da una differente prospettiva. L'uso principale di questo tipo e' di archiviare simili ma differenti dati e di ottenere effetti sinili a quelli del typecasting (meno utili adesso che il type casting e' stato introdotto anche in Pascal). L'uso dei record variant e' stato rimpiazzato da tecniche object oriented e altre tecniche moderne, benche' Delphi li usa in alcuni casi peculiari.

L'uso di un record variant non e' type-safe e non e' raccomandato come tecnica di programmazione, particolarmente per i principianti. I programmatori esperti possono invece usare i variant record e nel nucleo delle librerie di Delphi ci sono esempi d'uso. Ad ogni modo, non serve affrontarli finche' non ci si sente esperti di Delphi.

Puntatori

Un tipo puntatore definisce una variabile che contiene l'indirizzo di memoria di un'altra variabile di un dato tipo (o di un tipo indefinito). Cosi' una variabile puntatore indirettamente punta ad una variabile. La definizione di un tipo puntatore non e' basata su una specifica keyword, ma usa invece uno speciale carattere. Questo simbolo speciale e' il carattere (^):

type
  PointerToInt = ^Integer;

Una volta che e' stata definita una variabile puntatore, si puo' assegnare ad essa l'indirizzo di un'altra varabile dello stesso tipo, usandol'operatore @:

var
  P: ^Integer;
  X: Integer;
begin
  P := @X;
  // change the value in two different ways
  X := 10;
  P^ := 20;  

Quando si ha un puntatore P, con l'espressione P ci si riferisce all'indirizzo di memoria cui punta P, con l'espressione P^ si indica il contenuto di questa locazione di memoria. Per questa ragione nel frammento di codice sopra P^ corrisponde a X.

Invece di puntare ad una locazione di memoria esistente, un puntatore puo' indirizzare un nuovo blocco di memoria allocata dinamicamente (nell'area di memoria heap) con la procedura New. In questo caso non serve piu' il puntatore, bisogna anche ricordarsi di liberarsi della memoria dinamicamente allocata chiamando laprocedura Dispose.

var
  P: ^Integer;
begin
  // initialization
  New (P);
  // operations
  P^ := 20;
  ShowMessage (IntToStr (P^));
  // termination
  Dispose (P);
end;

Se un puntatore non ha valore, si puo' assegnare il valore nil ad esso. Si puo' quindi testare quando un puntatore e' nil per vedere se attualmente referenzia qualche valore. Questo metodo e' spesso usato, visto che dereferenziare un puntatore non valido causa una violazione d'accesso (conosciuta anche come General Protection Fault, GPF):

procedure TFormGPF.BtnGpfClick(Sender: TObject);
var
  P: ^Integer;
begin
  P := nil;
  ShowMessage (IntToStr (P^));
end;
Si puo' vedere un esempio dell'effetto di questo codice eseguendo l'esempio GPF (o guardando la corrispondente Figura 4.4). L'esempio contiene anche il frammento di codice mostrato sopra.

Figura 4.4: L'errore di sistema risultante dall'accesso ad un puntatore nil, dall'esempio GPF

Nello stesso programma si puo' trovare un esempio di accesso ai dati sicuro. In questo secondo caso il puntatore e' assegnato ad una variabile locale esistente, e puo' essere usato senza rischi, ma ho aggiunto un controllo per sicurezza:

procedure TFormGPF.BtnSafeClick(Sender: TObject);
var
  P: ^Integer;
  X: Integer;
begin
  P := @X;
  X := 100;
  if P <> nil then
    ShowMessage (IntToStr (P^));
end;

Delphi definisce anche un tipo di dato Pointer, che indica un puntatore senza tipo (come void* nel linguaggio C). Se si usa un puntatore senza tipo bisogna usare GetMem invece di New. La procedura GetMem e' richiesta ogni volta che la dimensione della variabile di memoria da allocare non e' definita.

Il fatto che i puntatori sono raramente necessari in Delphi costituisce un interessante vantaggio di questo ambiente. Nonostante cio', capire i puntatori e' importante per la programmazione avanzata e per capire completamente il modello a oggetti di Delphi, che usa i puntatori "dietro le quinte."

Nota: Sebbene non si usino i puntatori spesso in Delphi, si usa frequentemente un costrutto similare, le references. Ogni istanza di oggetto e' in realta' un puntatore o una referenza al suo dato corrente. Comunque, questo e' completamente trasparente al programmatore, che usa le variabili oggetto come qualsiasi altro tipo di dato.

Tipi File

Un altro tipo specifico del Pascal e' il tipo File. Il tipo file rappresenta i file fisici su disco, di sicuramente una peculiarita del linguaggio Pascal. Si puo' definire un nuovo tipo file come segue:

type
  IntFile = file of Integer;

Adesso si puo' aprire un file fisico associato con questa struttura e scrivere in esso valori interi o leggerne il valore corrente.

Nota dell'autore: Gli esempi relativi ai file sono parte delle vecchie edizioni di Mastering Delphi e progetto di aggiungerli al piu' presto.

l'uso dei file in Pascal e' abbastanza semplice, ma in Delphi ci sono anche altria componenti che sono capaci di memorizzare o caricare il proprio contenuto su o da un file. C'e' un supporto per la serializzazione, nella forma di stream, e c'e' anche il supporto per i database.

Conclusioni

Questo capitolo che tratta dei tipi di dati definibili dall'utente completa la copertura del sistema del Pascal. Adesso si puo' guardare alle istruzioni che il linguaggio provvede per operare sulle variabili che abbiamo definito.

Prossimo Capitolo: Le istruzioni

© Copyright Marco Cantù, Wintech Italia Srl 1995-2000