Tipo di dato

Un tipo di dato, in informatica, indica l'insieme di valori che una variabile, o il risultato di un'espressione, possono assumere e le operazioni che su tali valori si possono effettuare. Dire per esempio che la variabile X è di tipo "numero intero" significa affermare che X può assumere come valori solo numeri interi (appartenenti ad un certo intervallo) e che su tali valori sono ammesse solo certe operazioni (ad esempio le operazioni aritmetiche elementari).

Ogni linguaggio di programmazione consente di usare, in modo più o meno esplicito, un certo numero di tipi di dati predefiniti di uso generale, e di solito fornisce strumenti per definire nuovi tipi sulla base delle necessità specifiche di un programma.

Può anche accadere, durante la scrittura di un programma, che sia utile o necessario "tradurre" una variabile di un certo tipo in una variabile di un altro tipo (l'operazione è detta type casting): alcuni linguaggi mettono a disposizione costrutti sintattici per questo scopo, ma in altri casi è necessario scrivere una funzione che associ i valori di un tipo a quelli dell'altro.

Tipizzazione statica e dinamica 

Si parla di "tipizzazione statica" quando a una variabile viene associato rigidamente un tipo che rimane lo stesso per tutto il programma e di "tipizzazione dinamica" quando una variabile può cambiare tipo durante l'esecuzione del programma.

Ad esempio, il C è un linguaggio con tipizzazione statica; C++ e Java permettono sia tipizzazione statica che dinamica; Lisp, Visual Basic e Python sono linguaggi con tipizzazione dinamica.

Per vedere come funziona il controllo sui tipi, si può considerare il seguente esempio in pseudocodice:

  var x;        // (1)
  x := 5;       // (2)
  x := "ciao";  // (3)

In questo esempio: (1) dichiara la variabile x, (2) associa a x il valore di tipo intero 5, (3) associa a x il valore di tipo stringa "ciao" (qui si suppone che "intero" e "stringa" siano due tipi). Nella maggior parte dei linguaggi con tipizzazione statica un codice di questo tipo sarebbe illegale, poiché (2) e (3) associano alla variabile x valori appartenenti a tipi diversi; al contrario un linguaggio con tipizzazione totalmente dinamica troverebbe questo codice perfettamente legale. In quest'ultimo caso, ovviamente, la dichiarazione iniziale in (1) avrebbe dovuto specificare, con una qualche sintassi, il tipo da associare a x. Un esempio in Java potrebbe essere come segue:

  int x;       // (1)
  x = 5;       // (2)
  x = "ciao";  // (3) → rifiutata dal compilatore

Un linguaggio con tipizzazione dinamica consente di catturare "errori di tipo" (cioè errori dovuti a un uso scorretto dei valori che una variabile può assumere) solo durante l'esecuzione del programma.

Si consideri ad esempio il seguente pseudocodice:

  var x = 5;       // (1)
  var y = "ciao";  // (2)
  var z = x + y;   // (3)

In questo esempio: (1) associa a x il valore 5, (2) associa a y il valore "ciao" e (3) cerca di sommare x e y. In un linguaggio con tipizzazione dinamica, durante l'esecuzione del frammento di pseudocodice indicato, la variabile x risulterebbe (in quel momento) di tipo intero con valore 5, mentre la variabile y risulterebbe di tipo stringa con valore "ciao" (qui si suppone che "intero" e "stringa" siano due tipi). Se la definizione del linguaggio non ammette l'operazione di addizione fra un intero e una stringa, durante l'esecuzione del programma verrà segnalato un errore.

Tipi di dati 
I tipi di dati possono essere classificati secondo la struttura in tipi atomici o primitivi e tipi derivati. I tipi primitivi sono i tipi semplici che non possono essere decomposti, come ad esempio numeri interi o booleani; ogni linguaggio tipizzato ne possiede. I tipi derivati si ottengono dai tipi atomici mediante opportuni operatori forniti dal linguaggio: essi includono i tipi strutturati (record) o gli array, ma anche i puntatori di un tipo fissato (in linguaggi come il C), i tipi funzione (specialmente nei linguaggi funzionali), le classi dei linguaggi object-oriented e così via.

Un'altra classificazione suddivide i tipi di dati in predefiniti e definiti dall'utente. Si potrebbe pensare che i tipi predefiniti coincidano con i tipi atomici, mentre i tipi definiti dall'utente siano essenzialmente quelli derivati: in realtà le due classificazioni non sono perfettamente sovrapponibili – ad esempio le enumerazioni in linguaggi come il C sono tipi atomici definiti dall'utente, mentre sempre in C le stringhe sono tipi derivati (dal tipo carattere) ma predefinite dal linguaggio. D'altra parte è quasi sempre vero che i tipi definiti dal programmatore sono necessariamente tipi derivati.

Alcuni dei tipi di dati più comuni nei linguaggi di programmazione sono i seguenti.

Booleani 

Il tipo booleano ha due soli valori: true ("vero") e false ("falso"). Essi vengono utilizzati in modo speciale nelle espressioni condizionali per controllare il flusso di esecuzione e possono essere manipolati con gli operatori booleani AND, OR, NOT e così via.

Anche se in teoria basterebbe un solo bit per memorizzare un valore booleano, per motivi di efficienza si usa in genere un'intera parola di memoria, come per i numeri interi "piccoli" (una parola di memoria a 8 bit, per esempio, può memorizzare numeri da 0 a 255, ma il tipo booleano utilizza solo i valori 0 e 1).

Numeri 
I tipi di dati numerici includono i numeri interi e i numeri razionali in virgola mobile, che sono astrazioni dei corrispondenti insiemi di numeri della matematica. Quasi tutti i linguaggi includono tipi di dati numerici come tipi predefiniti e forniscono un certo numero di operatori aritmetici e di confronto su di essi.

A differenza degli insiemi numerici della matematica, i tipi di dati numerici sono spesso limitati (includono cioè un massimo e un minimo numero rappresentabile), dovendo essere contenuti in una singola parola (word) di memoria.

Caratteri e stringhe 
Il tipo carattere contiene un carattere, generalmente ASCII, memorizzato in un byte. Tuttavia in questi anni si sta affermando il nuovo standard Unicode per i caratteri, che prevede 16 bit (che generalmente corrisponde a una parola di memoria) per la rappresentazione di un singolo carattere. Molti linguaggi tradizionali si sono adattati a questo standard emergente introducendo, in aggiunta al tipo "carattere a 8 bit", un nuovo tipo "carattere a 16 bit", talvolta detto wide char (il linguaggio Java è invece un esempio di linguaggio moderno che gestisce direttamente tutti i caratteri nel formato Unicode).

Le stringhe sono sequenze di caratteri di lunghezza finita. I linguaggi possono fornire operazioni per la concatenazione di stringhe, la selezione di sottostringhe di una stringa data, ecc.

Enumerazioni 
Le enumerazioni sono insiemi finiti di identificatori, generalmente specificati dal programmatore. In linguaggi come C e C++ è possibile definire dei tipi enumerazione con una sintassi simile alla seguente:

  enum Color { RED, GREEN, BLUE };

Una variabile di tipo "Color" potrà in tal caso assumere solo i valori "RED", "GREEN" e "BLUE". Rispetto alle tecniche tradizionali per gestire analoghi generi di dati, che prevedevano semplicemente di adottare una convenzione numerica implicita (per esempio scrivo "1" per intendere "rosso", "2" per intendere "verde" e così via), i tipi enumerati forniscono una maggiore leggibilità e una migliore astrazione sui dati.

Puntatori 

I valori di tipo puntatore sono indirizzi di memoria di variabili, oggetti (o altri elementi di programma). L'operatore con cui, dato un puntatore, si accede all'oggetto puntato viene detto operatore di dereferenziazione (dereferencing). Molti linguaggi offrono anche un operatore "inverso", spesso detto operatore indirizzo-di, che data una variabile consente di ricavarne l'indirizzo. Un insieme esteso di operazioni sui puntatori viene fornito dai linguaggi dotati di aritmetica dei puntatori.

L'uso di puntatori è spesso necessario per costruire strutture dati complesse e dalla forma non prevedibile a priori e/o variabile nel tempo come grafi, alberi, liste e così via; inoltre, i puntatori possono essere usati per realizzare il passaggio di parametri per riferimento nei linguaggi che non lo offrono come meccanismo nativo. Se usati in modo indiscriminato, tuttavia, i puntatori possono portare allo sviluppo di software molto complesso e, soprattutto, condurre a errori di programmazione difficili da individuare. Per questo motivo alcuni linguaggi, tra cui Java, tentano di limitarne l'uso.

Riferimenti

Alcuni linguaggi forniscono un meccanismo simile ai puntatori, ma caratterizzato da dereferenziazione implicita; le variabili di questo tipo sono dette riferimenti.

Le caratteristiche dei riferimenti sono diverse nei diversi linguaggi, ma in generale hanno una caratteristica comune: sintatticamente, i tipi riferimento non richiedono l'uso di un operatore di dereferenziazione, e non prevedono un operatore "indirizzo di"; di conseguenza, sui riferimenti non è possibile l'aritmetica dei puntatori.

Esempi di linguaggi che ne fanno uso, con caratteristiche diverse, sono il C++ (vedi anche riferimento (C++)) e Java.

Array 

Un array è una sequenza finita di elementi appartenenti a un determinato tipo, indicizzata mediante un numero intero.

Record 

I record, detti anche tuple o strutture, sono aggregati di tipi di dati più semplici, la cui composizione può essere definita dall'utente. Un record è necessario per mantenere informazioni eterogenee correlate: potrebbero ad esempio essere usati per modellare le schede dell'archivio di una biblioteca, che devono contenere stringhe per il titolo di un libro e il nome del suo autore, ma anche un valore numerico indicante la collocazione; ciascuna di queste informazioni (campi del record) può essere acceduta in modo indipendente specificandone il nome: in molti linguaggi, specialmente quelli derivati dal C, questa operazione è specificata mediante l'operatore . (punto):

cout << scheda.autore; // stampa il nome dell'autore
cout << scheda.titolo; // stampa il titolo del libro

Poiché in molti casi è necessario imporre una coerenza ai dati contenuti in un record, certi linguaggi consentono di definire tipi di dati astratti, che sono essenzialmente record la cui struttura fisica non è visibile e che possono essere manipolati soltanto mediante certe operazioni fidate(non necessariamente in ogni caso) specificate in un'interfaccia. Sui tipi di dati astratti si basano anche i tipi classe tipici della programmazione orientata agli oggetti.

Tipi funzione 

In molti linguaggi, una variabile può contenere un puntatore a funzione. Dereferenziando la variabile sarà quindi possibile invocare una funzione definita a runtime.

Per garantire che la funzione sia invocata con argomenti di tipo corretto, il tipo di una variabile funzione è definito dalla signature della funzione, ovvero dal tipo del valore restituito e dal tipo e ordine degli argomenti della funzione.

Di conseguenza, le funzioni possono restituire funzioni così come gli altri tipi di dati, e possono ricevere funzioni come argomenti, al pari degli altri tipi di dati. Un esempio in Perl:

sub generaAccumulatore {
  my $accumulator=0;
  return sub { 
      $inc = shift;
      $accumulator += $inc;
      return $accumulator; 
  }
}

$accumulatore1=generaAccumulatore();
$accumulatore2=generaAccumulatore();
$accumulatore1->(4);
print $accumulatore2->($accumulatore1->(1)) . " e " . $accumulatore1->(3);

Tipi generici o template 

I costruttori di tipo visti finora sono costrutti definiti dal linguaggio di programmazione, e possono essere utilizzati dal programmatore per costruire nuovi tipi di dato sulla base di tipi esistenti.

In alcuni linguaggi è anche possibile specificare anche tipi generici (detti anche parametrici o template), che sono in effetti costruttori di tipo definiti dal programmatore. Essi non possono essere direttamente usati nei programmi, ma devono essere applicati ad uno o più tipi esistenti (che sono quindi i parametri del costruttore) per generare nuovi tipi di dato. La relazione tra costruttore di tipo e tipo concreto può quindi essere vista in analogia a quella tra classe ed oggetto nella programmazione orientata agli oggetti.

Sarà quindi possibile definire il template lista, che può quindi essere istanziato sugli interi per ottenere il tipo lista di interi, o su booleani per ottenere lista di booleani.

Il principale beneficio è quello di non essere costretti a dare una definizione diversa delle liste per ogni possibile tipo contenuto, in modo tale da ridurre il codice replicato.

Nei linguaggi orientati agli oggetti, i template sono costruttori di classi parametrici rispetto ad uno o più tipi, e quindi includono anche il codice per operare sulle classi stesse. In quanto tali, sono particolarmente adatti alla definizione di tipi "contenitore" (come lista, insieme, albero), che comprendono sia la struttura dati per rappresentare in memoria il contenitore che le operazioni per accedere ai suoi membri.

In questo modo, il codice per la definizione e l'accesso alle strutture dati viene reso ortogonale sia rispetto ai tipi di dato memorizzati che rispetto agli algoritmi che operano sulle strutture dati. Questo approccio è stato seguito in modo organico dalla libreria STL, inclusa nel linguaggio C++.

Tipologie di linguaggi 

 Non tipizzati: I linguaggi di programmazione più semplici, come i linguaggi macchina della maggior parte degli elaboratori o il lambda calcolo puro sono detti non tipizzati in quanto non prevedono tipi o, secondo un altro punto di vista, consentono l'utilizzo dell'unico tipo contenente tutti i valori possibili. Ad esempio i linguaggi macchina manipolano configurazioni di bit, per le quali l'onere dell'interpretazione (ossia stabilire quali operazioni sono sensate su un valore e quali no) spetta completamente al programmatore: la stessa configurazione di bit nella memoria di un computer potrebbe indicare valori concettualmente diversi per tipo, come ad esempio il numero intero 0, il puntatore nullo o l'istruzione vuota NOP (no operation). Nel lambda calcolo si manipolano funzioni mediante le stesse funzioni e uno stesso termine può rappresentare il valore intero 0, il booleano false ("falso") o la funzione che dati due valori scarta il primo e restituisce il secondo.

 Tipizzati: Nei linguaggi tipizzati è necessario associare alle variabili, alle espressioni e più in generale ai termini dei programmi delle annotazioni o dichiarazioni di tipo. Queste annotazioni di tipo, a seconda del linguaggio, devono essere specificate esplicitamente dal programmatore, oppure possono essere generate in modo automatico dall'interprete o dal compilatore. Assegnare un tipo di dato ad una variabile permette di risolvere le ambiguità viste per i linguaggi non tipizzati: se si cercasse di utilizzare un valore intero laddove è richiesto un puntatore, si otterrebbe normalmente un errore di tipo o violazione di tipo.

Controllo sui tipi: linguaggi fortemente e debolmente tipizzati 

Il "controllo sui tipi" (type checking) è il procedimento che permette di verificare se i vincoli imposti dai tipi sono soddisfatti. Tale verifica può avvenire sia durante la compilazione, e si parla in tal caso di "controllo statico" (in inglese "static check"), sia durante l'esecuzione del programma, e si parla in tal caso di "controllo dinamico" (in inglese "dynamic check"). Alcuni linguaggi operano alcuni controlli di tipo in fase di compilazione e altri durante l'esecuzione.

Se un linguaggio impone regole rigide sui tipi, impedendo qualsiasi uso dei dati incoerente col tipo specificato in fase di dichiarazione, si dice che esso è "fortemente tipizzato"; in caso contrario, che è "debolmente tipizzato".

Voci correlate 
 Astrazione (informatica)
 Struttura dati
 Type checking
 Linguaggio di programmazione
 Linguaggio macchina
 Linguaggio di programmazione ad alto livello
 NULL
 Forte tipizzazione
 Tipo di dato astratto
 Programmazione orientata agli oggetti
 classe (informatica)
 firma (programmazione)
 Sottotipo (informatica)

Altri progetti