Come
accennato in altre lezioni, Visual Basic mette a disposizione
del programmatore vari tipi di cicli iterativi; nell'ultima
lezione abbiamo visto il ciclo principale (For
Next),
ora vedremo un altro ciclo molto importante: il Do
Loop.
Come il ciclo For
Next, anche il ciclo Do
Loop esegue
un gruppo di istruzioni finché risulta verificata (o
falsificata, a seconda del punto di vista) una determinata
condizione: questa condizione deve essere preceduta da una
parola chiave che indica se il proseguimento del ciclo è
subordinato alla verità o falsità della condizione.
Le due parole chiave che possono essere utilizzate sono While
e Until: la prima indica che la condizione specificata deve
risultare vera affinché il ciclo prosegua, la seconda
indica che la condizione deve risultare falsa affinché
il ciclo prosegua.
Ecco un esempio di ciclo Do
Loop:
Dim
i
As Integer
i
= 1
Do While i<10
i = i + 1
Debug.Print i
Loop
Debug.Print "i vale "+cstr(i)+": il ciclo
è terminato"
|
Questo
ciclo incrementa e stampa il valore della variabile i fintantoché
essa resta minore di 10: quando i assume il valore 10 la condizione
i<10 risulterà falsa, e poiché nel ciclo
è specificata la parola chiave While, il ciclo si interromperà
e il controllo passerà all'istruzione successiva all'istruzione
Loop.
Lo stesso identico ciclo può essere espresso anche
utilizzando la parola chiave Until:
Dim
i
As Integer
i
= 1
Do Until i>=10
i = i+1
Debug.Print i
Loop
Debug.Print "i vale "+cstr(i)+": il ciclo
è terminato"
|
Avendo
utilizzato Until, è necessario invertire la condizione
di controllo affinché i due cicli siano equivalenti:
nel primo esempio il ciclo era eseguito finché i<10
è vera, nel secondo esempio il ciclo è eseguito
finché i>=10 è falsa, poiché il valore
iniziale di i è sempre 1. In termini più generali,
dato un ciclo Do While:
Do
While condizione
[istruzioni]
Loop
|
il
ciclo Do Until equivalente sarà:
Do
Until Not condizione
[istruzioni]
Loop |
Da
questi esempi appare con evidenza una forte analogia con il
ciclo For visto sopra: in effetti le differenze sono ben poche,
tant'è vero che qualunque ciclo For può essere
espresso come un ciclo Do e viceversa.
L'uso di un ciclo al posto di un altro va deciso in base a
questioni di convenienza: si sceglie l'istruzione più
semplice, più immediata, più facilmente comprensibile:
negli esempi appena considerati il ciclo For è leggermente
preferibile al ciclo Do, perché è più
intuitivo e consente di evitare l'istruzione di incremento
della variabile i, che in questi casi funge da "contatore":
è proprio l'istruzione For
Next che si occupa
di inizializzare e incrementare la variabile, e così
il programmatore deve solo preoccuparsi di stamparne il valore.
In generale, il ciclo For è preferibile quando si conosce
con esattezza il numero di iterazioni, ossia quando si sa
per quante volte il ciclo deve essere eseguito; il ciclo Do
è preferibile invece quando è necessaria più
flessibilità, quando il numero di iterazioni non può
essere conosciuto a priori.
Ad esempio, si supponga di dover eseguire ciclicamente un
gruppo di istruzioni finché l'utente non dice "basta!":
non è possibile sapere in anticipo di quanta pazienza
disponga l'utente, perciò è opportuno ricorrere
a un ciclo Do che controlli il verificarsi di una condizione.
In casi come questi, solitamente si fa ricorso all'utilizzo
di una variabile flag di tipo booleano, che assuma il valore
vero (o falso, a seconda della convenienza) quando l'utente
decide di interrompere il ciclo:
Questo
semplice ciclo continua a visualizzare numeri casuali finché
la variabile blnFlag è vera: il ciclo si interromperà
quando il flag, a seguito di un'azione dell'utente o al verificarsi
di altre condizioni, assumerà il valore False. Per
esempio, si inserisca in un Form un pulsante di comando e
un'etichetta di testo: si imposti la Caption del CommandButton
a "Avvia" e nella routine Click si scriva:
Private
Sub
Command1_Click()
If blnFlag Then
Command1.Caption = "Avvia"
blnFlag = False
Else
Command1.Caption = "Interrompi"
blnFlag = True
End If
Do While blnFlag
Label1.Caption = CStr(Rnd())
DoEvents
Loop
End Sub |
La
subroutine controlla innanzitutto il valore del flag (che
naturalmente deve essere dichiarato a livello di modulo):
se è falso (impostazione predefinita iniziale per tutte
le variabili booleane), esso diventa vero e il ciclo Do successivo
può essere eseguito dando il via alla visualizzazione
dei numeri casuali; altrimenti diventa falso e quindi il ciclo
Do si interrompe; inoltre viene modificata la Caption del
pulsante per far capire all'utente l'effetto della pressione
del pulsante stesso.
Nel ciclo Do compare l'istruzione DoEvents, che restituisce
temporaneamente il controllo al sistema operativo per consentirgli
di gestire altre operazioni accumulatesi durante l'esecuzione
del nostro programma: senza questa istruzione, a causa del
modo con cui Windows gestisce il multitasking, il programma
si bloccherebbe nell'esecuzione del ciclo, perché la
pressione del pulsante non avrebbe alcun effetto.
Detto con parole poco precise, il programma è così
impegnato ad eseguire il ciclo Do che non ha il tempo di gestire
gli altri input forniti dall'utente (come la pressione del
pulsante), né di aggiornare l'aspetto degli altri componenti
del Form (l'etichetta non mostra nulla), a meno che non ci
sia un'istruzione esplicita che obblighi il nostro programma
a cedere momentaneamente il controllo al sistema operativo:
questo è appunto il compito dell'istruzione DoEvents.
Senza di essa l'unico modo per interrompere il programma è
premere la combinazione di tasti ctrl+alt+canc se il programma
è stato compilato; se invece è eseguito all'interno
dell'IDE di Visual Basic basta premere ctrl+pausa.
L'eliminazione dell'istruzione DoEvents impedisce di modificare
il flag blnFlag una volta che il ciclo Do è cominciato:
la variabile booleana pertanto resta sempre True, e di conseguenza
il ciclo non si interromperà mai; questo è un
semplice esempio di ciclo infinito.
La presenza di cicli infiniti è uno dei classici bug
commessi da programmatori alle prime armi, ma anche da programmatori
un po' più esperti: quando la condizione di controllo
del ciclo dipende da diversi fattori può essere difficile
rendersi conto dell'errore commesso, anche perché di
norma l'errore sarà tanto più "nascosto"
quanto più si presenta raramente nel corso dell'esecuzione.
Per questo motivo è necessario stare bene attenti a
fornire una condizione di uscita dal ciclo: se c'è
il rischio che la condizione di controllo resti sempre vera
(se si usa While) o sempre falsa (se si usa Until), può
essere opportuno utilizzare ancora l'istruzione Exit; nel
caso del ciclo For l'istruzione corretta era Exit For, nel
caso del ciclo Do l'istruzione corretta è Exit Do.
L'esempio precedente potrebbe quindi essere riscritto in questo
modo:
Private
Sub
Command1_Click()
If blnFlag Then
Command1.Caption = "Avvia"
blnFlag = False
Else
Command1.Caption = "Interrompi"
blnFlag = True
End If
Do While True 'ciclo
virtualmente infinito
Label1.Caption = CStr(Rnd())
DoEvents
If Not blnFlag Then
Exit Do 'condizione di uscita dal ciclo
Loop
End Sub |
Il
ciclo è potenzialmente infinito, perché la condizione
di controllo non è una vera e propria condizione, ma
è un valore letterale costante (True) che in quanto
tale non potrà mai diventare False: la condizione di
uscita è quindi ottenuta tramite un controllo interno
al ciclo, che esamina il valore del flag: se questo è
falso, significa che l'utente vuole interrompere il ciclo,
pertanto Not blnFlag sarà True e sarà eseguita
l'istruzione Exit Do che passa il controllo all'istruzione
successiva al ciclo (in questo caso End Sub).
La clausola While o Until può trovarsi alternativamente
dopo l'istruzione Do o dopo l'istruzione Loop:
Do
While|Until condizione
[istruzioni]
Loop
|
oppure:
Do
[istruzioni]
Loop While|Until condizione |
La
differenza consiste nel momento in cui è controllato
il valore di condizione: nel primo caso il controllo avviene
all'inizio di ogni iterazione, nel secondo caso alla fine
di ogni iterazione.
Ciò significa che se per ipotesi la condizione risulta
subito falsa (o vera, se si usa Until), nel primo caso il
ciclo non sarà eseguito mai, mentre nel secondo caso
sarà eseguito solo una volta, perché la condizione
è controllata dopo l'esecuzione delle istruzioni.
La condizione di controllo del ciclo può naturalmente
essere rappresentata da un'espressione booleana anche complessa,
come la seguente:
Do
While
blnFlag And (intUsers >
1 Or intUsers = -1) And
((lngCount Xor _ lngMax)
= 20)
'istruzioni
Loop |
In
questi casi bisogna tenere presente che la condizione è
sempre e soltanto una, che viene valutata tenendo conto complessivamente
del valore delle singole parti e degli operatori logici utilizzati:
quando l'intera espressione è vera, il ciclo prosegue,
quando diventa falsa il ciclo si interrompe.
Trasformare un ciclo del genere nel corrispondente Do Until
Loop
può diventare molto complicato se si cerca di convertire
ogni parte della condizione nel corrispondente opposto: in
questo caso, l'equivalente sarebbe:
Do
Until Not
blnFlag Or (intUsers <=
1 And intUsers <> -1)
Or ((lngCount Xor
_ lngMax) <> 20)
'istruzioni
Loop |
Ma
la cosa più semplice è premettere un Not all'intera
condizione:
Do
Until Not
(blnFlag And (intUsers >
1 Or intUsers = -1) And
((lngCount Xor _ lngMax)
= 20))
'istruzioni
Loop |
Infine,
come per i cicli For, anche i cicli Do possono essere nidificati
a più livelli: ogni istruzione Loop corrisponderà
all'istruzione Do immediatamente precedente, in modo che ogni
ciclo sia interamente contenuto nel ciclo più esterno,
come nell'esempio seguente:
Do
While condizione1
Do
[istruzioni2]
Loop Until condizione2
[istruzioni1]
Loop |
Ora
che conoscete anche il ciclo Do
Loop, potete riprendere
in mano il progetto del Campo minato delle lezioni precedenti:
ricordate lo stratagemma utilizzato per assicurarsi che le
tre mine fossero disposte sotto tre differenti pulsanti? La
soluzione escogitata consisteva nel simulare più estrazioni
casuali tramite l'uso di etichette e dell'istruzione GoTo.
In realtà una soluzione molto più efficiente
e flessibile (oltre che più "bella" stilisticamente)
è data dall'uso di un ciclo Do simile a questo:
Dim
t As Integer 'variabile
temporanea per eseguire i controlli
Dim
i As Integer, k As
Integer 'contatori per i
cicli
Dim blnUguali As
Boolean 'flag
Randomize Timer
For i = 0 To
2
Do
PosMine(i) = Int(Rnd * 16)
blnUguali = False
For k = 0 To
i - 1
If (PosMine(i) = PosMine(k))
Or blnUguali Then
blnUguali = True
Else
blnUguali = False
End If
Next k
Loop While blnUguali
Mina(PosMine(i)).Tag = True
Next i
|
L'algoritmo
è solo apparentemente complicato: il ciclo For esterno
conta le mine da piazzare, e contiene un ciclo Do che continua
a estrarre valori casuali finché l'ultimo valore estratto
risulta uguale a uno di quelli estratti nelle iterazioni precedenti.
L'uguaglianza di due valori è segnalato dal flag blnUguali,
il cui valore è impostato nel terzo ciclo, quello più
interno che ha come contatore k: questo ciclo controlla se
la posizione calcolata della mina considerata coincide con
la posizione delle mine precedenti; se due mine coincidono,
il flag è impostato a True, altrimenti è impostato
a False.
Si noti che la condizione controllata non è semplicemente
(PosMine(i)=PosMine(k)), ma (PosMine(i)=PosMine(k)) Or blnUguali,
per evitare che blnUguali diventi False quando è già
True, cioè quando è già stata trovata
una coincidenza da eliminare.
Per lo stesso motivo, blnUguali è reinizializzato a
False prima di controllare la coincidenza di due mine col
ciclo For interno. Una volta che si è calcolata una
posizione della mina i-esima che è diversa dalle altre,
il ciclo Do si interromperà e si potrà aggiornare
la proprietà Tag di quella mina, e così via
per tutte le tre mine.
Questo è un algoritmo semplice ma potente, perché
consente di estrarre valori casuali certamente diversi l'uno
dall'altro qualunque sia il numero di iterazioni da effettuare
(ovvero il numero di mine da piazzare): se avessimo dovuto
cavarcela con le etichette la situazione sarebbe diventata
ingestibile anche solo con 10 mine.
|