Struttura di controllo
Da Wikipedia, l'enciclopedia libera.
In tutti i paradigmi di programmazione imperativa, le strutture di controllo sono costrutti sintattici di un linguaggio di programmazione la cui semantica afferisce al controllo del flusso di esecuzione di un programma, ovvero che servono a specificare se, quando, in quale ordine e quante volte devono essere eseguite le istruzioni che lo compongono.
Indice |
[modifica] Strutture di controllo fondamentali
[modifica] Sequenza
Vedi articolo principale
La sequenza è la struttura di controllo fondamentale di qualsiasi linguaggio imperativo, inclusi i linguaggi macchina. Essa si applica quando si vuole, semplicemente, che le istruzioni di un programma siano eseguite nell'ordine in cui compaiono nel testo del programma stesso. Di norma, essa non ha una espressione sintattica esplicita; in altre parole, laddove non vengano utilizzate esplicitamente strutture di controllo di altro genere, per "default" la macchina virtuale di un linguaggio esegue le istruzioni del programma sequenzialmente.
[modifica] Goto
Insieme alla sequenza, il goto (vai a) è la struttura di controllo più semplice; anch'essa appare, in qualche forma, in tutti i linguaggi macchina. Il significato generale del goto è quello di far "passare" il controllo a una istruzione specificata, che può trovarsi in un punto qualsiasi del programma. Il goto ammette sempre anche (o solo) una forma condizionata, il cui significato può essere parafrasato come segue: "se è vera una condizione C, vai alla istruzione I". Nei linguaggi macchina, la condizione C deve solitamente assumere una delle due seguenti forme: "il contenuto della cella di memoria M è 0" oppure "il contenuto della cella di memoria M è diverso da 0"; l'istruzione I viene inoltre identificata dall'indirizzo di memoria in cui I è memorizzata. Nei Linguaggi ad alto livello come il Basic, la condizione C può essere espressa come una qualsiasi espressione booleana (come il risultato della moltiplicazione di A per B è diverso da X), e l'istruzione I può essere identificata da un nome o da un codice numerico esplicitamente associato dal programmatore all'istruzione stessa (indipendentemente dalla sua collocazione in memoria).
A partire dagli anni settanta, la struttura di controllo goto è stata soggetta a forti critiche in quanto essa consente (o favorisce) lo sviluppo di programmi potenzialmente molto poco leggibili e modificabili (il cosiddetto spaghetti code). Sebbene essa resti una struttura di controllo fondamentale dei linguaggi macchina moderni, nei linguaggi di programmazione ad alto livello, a seconda dei casi, il goto non viene fornito oppure viene fornito ma se ne sconsiglia l'uso.
[modifica] Bibliografia
E. Dijkstra (1968), Goto statement considered harmful, «Communications of the ACM» 11 (3), pp.147-148. La più celebre critica della struttura di controllo goto. Oggi disponibile anche online
[modifica] Strutture di controllo della programmazione strutturata
Nel 1966, con un celebre teorema, Corrado Boehm e Giuseppe Jacopini introdussero i fondamenti teorici del paradigma della programmazione strutturata dimostrando che qualsiasi programma scritto usando la struttura di controllo goto poteva essere riscritto usando solo strutture di controllo di tipo sequenza (vedi sopra), iterazione e alternativa. Unitamente alle critiche di cui si è detto sopra, questo risultato contribuì a decretare la fine della programmazione basata sul goto. Tutti i linguaggi moderni offrono un insieme di strutture di controllo dei tre tipi introdotti da Boehm e Jacopini, sebbene molti mantengano anche il goto (pur sconsigliandone l'uso indiscriminato).
[modifica] Alternativa
Le strutture di controllo "alternative" consentono di specificare che una data istruzione o un dato blocco di istruzioni devono essere eseguiti "(solo) se" vale una certa condizione.
[modifica] Alternativa if-then e if-then-else
L'alternativa if-then (se-allora) è la più semplice forma di alternativa. Il suo significato può essere parafrasato con la frase "se vale la condizione C, esegui l'istruzione (blocco) I". La maggior parte dei linguaggi di programmazione ammette anche (come variante) la forma più articolata if-then-else (se-allora-altrimenti), che si può parafrasare come: "se vale la condizione C esegui l'istruzione (blocco) I1; altrimenti esegui l'istruzione (blocco) I2".
[modifica] L'alternativa case
L'alternativa case può essere assimilata a una catena di if-then-else con certe restrizioni. In questo caso, la scelta di uno fra N istruzioni o blocchi alternativi viene fatta sulla base del valore di una determinata variabile o espressione, normalmente di tipo intero. Essa può essere parafrasata come segue: "valuta il valore N; nel caso in cui il suo valore sia V1, esegui I1; nel caso in cui sia V2, esegui I2 (ecc.)". Il seguente frammento di codice pseudo-C illustra il concetto:
case sceltaMenu of 1 : apriFile(); 2 : chiudiFile(); 3 : salvaFile(); 4 : esci(); end;
Il fatto che il valore N debba (spesso) essere di tipo intero è legato a considerazioni di efficienza nell'implementazione del meccanismo. Il case si presta infatti a essere trasformato, a livello di linguaggio macchina, in un goto con indirizzamento indiretto, che potrebbe essere basato su una tabella in memoria di cui N seleziona una particolare entry, in cui è specificato l'indirizzo delle istruzioni da eseguire per quel valore di N.
[modifica] Iterazione
Vedi articolo principale
Le strutture di controllo "iterative" consentono di specificare che una data istruzione o un dato blocco di istruzioni devono essere eseguiti ripetutamente. Esse vengono anche dette cicli. Ogni struttura di controllo di questo tipo deve consentire di specificare sotto quali condizioni l'iterazione (ripetizione) di tali istruzioni debba terminare, ovvero la condizione di terminazione del ciclo oppure, equivalentemente, la condizione di permanenza nel ciclo. Di seguito si esaminano le strutture di controllo più note di questa categoria.
[modifica] Ciclo for
Vedi articolo principale
Il ciclo for è indicato quando il modo più naturale per esprimere la condizione di permanenza in un ciclo consiste nello specificare quante volte debbano essere ripetuti l'istruzione o il blocco controllati dal ciclo. Le forme tradizionali di ciclo for possono essere parafrasate come "ripeti (il codice controllato) per i che va da un certo valore iniziale a un certo valore finale, con un certo passo". i è in generale una variabile di tipo intero, detta contatore. Il seguente frammento di codice Basic illustra il concetto:
FOR I=1 TO 9 STEP 2 PRINT I
Questo frammento ripete l'istruzione di stampa a video della variabile contatore I. Essa parte dal valore iniziale 1 per arrivare al valore finale 10. L'indicazione del "passo" (STEP) specifica come varia il valore di I da una iterazione alla successiva. Il frammento dunque stamperà la sequenza 1, 3, 5, 7, 9.
[modifica] Ciclo while
Vedi articolo principale
Il ciclo while (mentre, o fintantoché) è indicato quando la condizione di permanenza in un ciclo è una generica condizione booleana, indipendente dal numero di iterazioni eseguite. Le forme tradizionali di ciclo while possono essere parafrasate come "ripeti (il codice controllato) fintantoché resta vera la condizione C". Un esempio tipico è la lettura di dati da un file di cui non si conosca a priori la dimensione; esso potrebbe assumere la forma "leggi il prossimo dato finché non incontri la fine del file". Il seguente frammento di codice pseudo-C mostra un esempio di while:
passwordUtente = leggiPassword(); while(passwordUtente <> passwordCorretta) { segnalaErrorePassword(); passwordUtente = leggiPassword(); }
Nel while del C (e di molti altri linguaggi) la condizione di permanenza nel ciclo viene controllata prima di eseguire la prima iterazione del ciclo stesso; se essa risulta immediatamente falsa, le istruzioni nel ciclo non vengono eseguite. Nell'esempio riportato sopra, se la password letta è corretta, il blocco di codice che segnala l'errore di immissione e chiede di inserire nuovamente la password non viene (ovviamente) eseguito.
[modifica] Ciclo repeat-until
Il ciclo repeat-until (ripeti finché) differisce dal while per due aspetti. Innanzitutto, esso garantisce che venga eseguita sempre almeno una iterazione del ciclo (la condizione che controlla il ciclo viene verificata dopo aver concluso la prima iterazione). In secondo luogo, viene espressa la condizione di terminazione del ciclo anziché quella di permanenza nel ciclo (quest'ultima differenza non ha comunque un impatto concettuale molto importante, essendo le due condizioni esprimibili semplicemente come negazione l'una dell'altra). Il seguente frammento pseudo-Pascal illustra il concetto:
passwordUtente := leggiPassword(); repeat passwordUtente := leggiPassword(); until (passwordUtente = passwordCorretta)
[modifica] Varianti di while e repeat-until
Le due differenze citate sopra fra while e repeat-until sono in effetti indipendenti l'una dall'altra, per cui sono facilmente immaginabili (benché meno diffuse per motivi storici) anche altre due combinazioni: cicli che garantiscono una iterazione ma in cui si specifica la condizione di permanenza nel ciclo, e cicli che ammettono 0 iterazioni ma in cui si specifica la condizione di terminazione del ciclo. Un esempio del primo caso è la struttura do-while (fai fintantoché) del C e dei suoi derivati, esemplificata in questo frammento di pseudo-codice:
do passwordUtente = leggiPassword(); while(passwordUtente<>passwordCorretta);
[modifica] Iterazione basata su collezioni
Alcuni linguaggi (per esempio Smalltalk, Perl, C#) forniscono varianti del ciclo for in cui il "contatore" è una variabile generica (non necessariamente un numero intero) che assume una sequenza di valori del tipo corrispondente, per esempio tutti i valori contenuti in un array o in una collezione. Il seguente frammento di codice C# illustra il concetto:
foreach (string s in unaCollezioneDiStringhe) { stampa(s); }
[modifica] Terminazione anticipata di cicli e iterazioni
Molti linguaggi forniscono una istruzione specifica per terminare "prematuramente" un ciclo, ovvero causarne la terminazione in un modo alternativo rispetto a quello principale del ciclo (per esempio basato sul valore del contatore nel for o sulla verità/falsità di una condizione nei cicli while o repeat-until). Il seguente frammento illustra il concetto in un ciclo Java:
// cerco un valore N in un array boolean trovato = false; for(int i=0; i<100; i++) if(v[i]==N) { trovato = true; break; } }
In questo frammento Java, il programma deve stabilire se un dato numero N è contenuto in un array. Non appena N viene trovato, diventa inutile proseguire l'attraversamento dell'array stesso: il break termina quindi il ciclo for.
L'uso indiscriminato di meccanismi come il break (fornito da linguaggi come C, C++ e Java) è spesso criticato sconsigliato perché si viene a perdere una utile proprietà di leggibilità dei cicli della programmazione strutturata: infatti, di fronte a un ciclo while con una condizione C, il lettore tende ad assumere che, al termine del ciclo, C sia falsa. L'uso di break o costrutti simili, introducendo "un altro" possibile motivo di terminazione del ciclo (oltretutto potenzialmente "nascosto" all'interno del blocco controllato dal while) fa sì che questa assunzione non del tutto sicura. Tuttavia, vi sono anche casi in cui lo stato del programma alla fine del ciclo, con o senza break, è lo stesso, per cui il suddetto problema non si pone. Questo è il caso dell'esempio Java riportato sopra (il contatore i è locale al for, e quindi viene deallocato al termine del ciclo).
Un meccanismo simile (in generale considerato meno pericoloso) è quello che consente di terminare anticipatamente una determinata iterazione di un ciclo, passando immediatamente alla successiva. Si veda il seguente programma:
for(int i=1; i<=100; i++) if(numeroPrimo(i)) { stampa(i); continue; } // calcola e stampa i fattori di i }
Questo programma stampa i fattori di tutti i numeri interi da 1 a 100. Se i è un numero primo, è sufficiente stampare il numero stesso; altrimenti sono necessarie altre operazioni per scomporlo. L'istruzione continue indica che l'iterazione corrente è conclusa e fa sì che il ciclo passi immediatamente alla successiva.
[modifica] Bibliografia
C. Boehm, G. Jacopini (1966). Flow Diagrams, Turing Machines, and Languages with Only Two Formation Rules, <<Communications of the ACM>> 9(5), pp. 366-371. La formulazione originale del teorema di Boehm-Jacopini, oggi disponibile anche online
[modifica] Strutture di controllo non locali
Nella maggior parte dei linguaggi di programmazione ad alto livello, l'esecuzione di un programma evolve attraverso diversi contesti, in ciascuno dei quali sono possibili alcune azioni e non altre. Per esempio, l'esecuzione di una subroutine avviene in un contesto solitamente descrivibile in termini di record di attivazione, che comprende dati locali a cui solo quella attivazione di subroutine può accedere. Inoltre, vengono poste regole ben precise che stabiliscono come e quando l'esecuzione del programma transita da un contesto all'altro (nel caso delle subroutine, queste regole sono generalmente coerenti con il modello dello stack dei record di attivazione. Per strutture di controllo non locali si intendono quelle strutture di controllo che causano un salto del flusso di controllo che prescinde da (può costituire un'eccezione a) tali regole. Il goto è il caso estremo, e in effetti i linguaggi fortemente basati sul goto senza restrizione (come i linguaggi macchina) sono spesso caratterizzati da nozioni di contesto molto deboli o del tutto assenti.
Nei linguaggi strutturati esistono talvolta strutture di controllo non locali che aggirano le normali restrizioni sull'evoluzione del contesto di un programma in esecuzione, senza essere però del tutto incoerenti con esse; queste si possono definire strutture di controllo non locali strutturate. Gli esempi più noti di strutture di controllo non locali (sia non strutturate che strutturate) sono riportati nel seguito e afferiscono al problema generale della gestione delle eccezioni.
[modifica] Condizioni in PL/1
Il linguaggio PL/1 ha un meccanismo di gestione delle eccezioni piuttosto semplice. Sono previste un certo numero di condizioni (leggi: eccezioni, situazioni anomale; per esempio, tentativi di dividere un numero per 0 o di accedere a un array con un valore di indice illegale) che vengono "sollevate" (RAISE) automaticamente dal linguaggio. Il programmatore può indicare cosa fare quando viene sollevata una condizione attraverso una clausola della forma "ON <condizione> <azione>". Nella maggior parte dei casi, l'<azione> da eseguire quando si verifica la <condizione> viene specificata nella forma di un goto. Il meccanismo di PL/1 si può considerare come una versione primitiva (e poco o per niente "strutturata") della gestione delle eccezioni in C++ e linguaggi derivati.
[modifica] Le eccezioni in C++ e linguaggi derivati
C++, D, Java, e C# gestiscono le eccezioni con una struttura di controllo non locale strutturata apposita, solitamente detta struttura try-catch, la cui sintassi è illustrata di seguito:
try { ... ... // codice che può causare eccezioni di vari tipi ... } catch (UnTipoDiEccezione e) { ... // gestisce il problema } catch (UnAltroTipoDiEccezione e) { ... // gestisce il problema } finally { ... // codice che va eseguito comunque }
In questo schema, il blocco di codice controllato da try contiene una o più istruzioni che possono causare un'eccezione. Se tale evenienza si verifica, il controllo salta fuori dal contesto-blocco associato a try passando a un blocco controllato da una catch (come l'esempio suggerisce, si possono avere più catch associate a diversi tipi di eccezioni). Il blocco catch è il gestore dell'eccezione, ovvero contiene quelle operazioni che costituiscono, in senso ampio, la "contromisura" prevista dal programmatore nel caso si verifichi quella particolare eccezione. Il blocco controllato da finally (presente in D, Java, e C#) contiene istruzioni che devono essere eseguite comunque, che si verifichi o no una eccezione (normalmente si collocano nel blocco controllato da finally operazioni di rilascio risorse come chiusura di file o di connessioni di rete; per situazioni di questo genere C# ha anche un altro costrutto ad hoc, la clausola using).
Se in un blocco try viene sollevata un'eccezione per la quale non è stata prevista alcuna catch (oppure se si verifica un'eccezione in un punto del codice non controllato da una try), la subroutine o il metodo corrente terminano e l'eccezione viene propagata alla subroutine o metodo chiamante, che la intercetterà se l'istruzione di chiamata alla subroutine fallita è inclusa in un blocco try con associata una catch appropriata per quel tipo di eccezione; viceversa, il chiamante stesso terminerà e l'eccezione verrà ulteriormente propagata verso "l'alto" (al chiamante del chiamante). Su questo modello di gestione si possono fare due osservazioni:
- esso può essere definito strutturato nel senso che pur saltando da un contesto all'altro secondo regole diverse da quelle che "normalmente" regolano il cambiamento di contesto di un programma strutturato, non ne viola i princìpi fondamentali: il controllo non può passare a un punto qualsiasi del programma (come nel goto), bensì rispetta il modello dello stack dei record di attivazione (passaggio dal contesto del chiamato a quello del chiamante);
- è giustificato dal fatto che, nella pratica della programmazione, non tutte le eccezioni possono essere efficacemente gestite "localmente"; spesso, prendere contromisure per un certo problema richiede informazioni aggiuntive che sono disponibili solo in un contesto più ampio. Per esempio, se un fallimento nell'apertura di un file dev'essere segnalato all'utente con un messaggio in una finestra pop-up, non è ragionevole attendersi che questo possa essere fatto da una routine generica di accesso a file (la cui progettazione, per motivi di riusabilità probabilmente non "assume" che l'applicazione corrente sia dotata di GUI piuttosto che avere un'interfaccia testuale).
Un modello analogo a quello appena descritto si trova anche nei linguaggi Python, Ruby, Objective C e altri.
[modifica] Voci correlate
Gestione delle eccezioni in Java
gestione delle eccezioni in C++
[modifica] Strutture di controllo concorrenti
Nel contesto della programmazione concorrente e parallela, sono state introdotte strutture di controllo specifiche che specificano o implicano l'esecuzione concettualmente contemporanea di determinati insiemi di operazioni o istruzioni. Il linguaggio più rappresentativo in questo senso è probabilmente il linguaggio di programmazione parallela linguaggio di programmazione Occam. Questo linguaggio fornisce almeno due strutture di controllo innovative, rispettivamente per l'esecuzione parallela di istruzioni e una forma speciale di alternativa che implica la valutazione parallela delle condizioni che la governano.
[modifica] La struttura PAR di Occam
La struttura di controllo PAR specifica che un certo insieme di istruzioni devono essere eseguite in parallelo. Nella sua forma più semplice, la struttura PAR ha la seguente sintassi:
PAR x := x+1 y := y+1
In questo frammento di codice, l'incremento delle due variabili avviene contemporaneamente. Il PAR ammette anche una forma più complessa che presenta alcune analogie con un ciclo for, e viene coerentemente indicata con le parole chiave PAR-FOR. Il seguente frammento di codice acquisce un dato intero da quattro canali in parallelo.
PAR i=0 FOR 4 INT n c[i] ? n[i]
L'analogia con il ciclo for riguarda l'uso del "contatore" i. Come un ciclo for tradizionale, il frammento di codice riportato esegue le operazioni indicate cinque volte, "per i che va da 0 a 4"; tuttavia, le cinque operazioni di input non sono svolte sequenzialmente, bensì in parallelo.
[modifica] La struttura ALT di Occam
Il costrutto ALT consente di definire un insieme di "comandi con guardia". Un comando con guardia è costituito da una condizione detta guardia e una istruzione, con qualche analogia con una struttura if. Tuttavia, il significato del comando con guardia non è che l'istruzione sarà eseguita se la condizione è vera; piuttosto, l'istruzione potrà essere eseguita quando la condizione diventerà vera. Il costrutto ALT raggruppa un certo numero di comandi con guardia; il primo comando per cui la guardia diventa vera viene eseguito. Se ci sono più comandi la cui guardia è vera, uno di essi viene selezionato (arbitrariamente) dalla macchina virtuale del linguaggio.
ALT in.a ? v out ! v in.b ? v out ! v
Il costrutto ALT qui riportato comprende due comandi con guardia. Le guardie sono in questo caso istruzioni (sospensive) che attendono in input un valore da uno di due canali (in.a e in.b). Non appena il dato diventa disponibile su uno qualsiasi dei due canali, esso verrà acquisito, la guardia corrispondente si considererà "vera", e l'istruzione (di output) associata sarà eseguita, ponendo fine all'esecuzione del blocco ALT.