Capitolo 5

Cicli: ripetere le istruzioni

Un computer è fatto per ripetere: stampare 100 righe, sommare i voti di una classe, controllare tutti i caratteri di una parola. I cicli fanno eseguire lo stesso blocco di codice molte volte, cambiando un valore ogni volta.

In questo capitolo impari

Ripasso lampo: le condizioni

I cicli ripetono, ma decidono le condizioni: ripasso del capitolo 4. Il secondo quiz torna al capitolo 3: come si legge un numero, e che cosa succede se il testo non è un numero.

Che cosa stampa questo programma?

fun main() {
    val x = 5
    val y = if (x > 3) "grande" else "piccolo"
    println(y)
}
grandeEsatto: if restituisce un valore, che finisce nella variabile y.
piccoloLa condizione 5 > 3 è vera, quindi si prende il primo ramo.
Un errore: if non può dare un valoreIn Kotlin if è un'espressione: puoi assegnarlo a una variabile.

In Kotlin if è un'espressione: il ramo che viene eseguito restituisce un valore, e quel valore si può assegnare a una variabile con val y = if (...) a else b.

L'utente scrive 4, poi 7, poi la parola pippo. Che cosa succede?

fun main() {
    val a = readln().toInt()
    val b = readln().toInt()
    val c = readln().toInt()
    println(a + b + c)
}
Il programma si ferma alla terza lettura: toInt() accetta solo cifreEsatto: toInt() non sa convertire pippo e interrompe il programma. Per non fermarsi servirebbe readln().toIntOrNull() ?: 0.
Stampa 11, perché pippo viene saltatoNo: toInt() non salta i dati che non capisce, si ferma con un errore.
Stampa 47pippo, attaccando i tre valoriNo: dopo toInt() hai numeri, non testo. Il problema è la terza riga, che non è un numero.

Ogni readln().toInt() è un punto in cui il programma può fermarsi. Un dato non numerico blocca tutto lì. La forma robusta è readlnOrNull()?.toIntOrNull() ?: 0, spiegata nel capitolo 3.

5.1 for su un intervallo

for (i in 1..5) ripete il blocco facendo valere i uguale a 1, 2, 3, 4 e 5. I due puntini .. creano un intervallo che comprende entrambi gli estremi. La variabile i esiste solo dentro al ciclo.

Gli estremi di .. sono compresi, ma in che ordine? Kotlin va dal primo numero al secondo solo se il primo è più piccolo. Che succede se scrivi il contrario?

Quante volte viene eseguito il corpo di questo ciclo?

fun main() {
    for (i in 3..1) {
        println(i)
    }
}
Tre volteAttenzione: 3..1 è un intervallo vuoto, non un conto alla rovescia.
Nessuna: non stampa nienteEsatto: con .. si va dal più piccolo al più grande. 3 poi 1 non è quella direzione, quindi zero giri.
Dà un erroreNon è un errore: il ciclo semplicemente non ha nessun giro da fare.

Per contare all'indietro c'è un comando nell'esempio dopo: for (i in 3 downTo 1), che stampa 3 2 1.

  1. 0 until 3 comprende 0, 1, 2: il 3 è escluso, per questo until è comodo con gli indici.
  2. 10 downTo 0 step 5 parte da 10 e scende di 5 in 5: 10, 5, 0. Con downTo l'estremo finale è compreso.
  3. println() senza argomenti non stampa testo: manda solo a capo, così le due sequenze finiscono su righe diverse.
ScriviValori che prende la variabile
1..51, 2, 3, 4, 5 (estremi compresi)
0 until 50, 1, 2, 3, 4 (l'ultimo valore è escluso)
10 downTo 610, 9, 8, 7, 6 (conta all'indietro)
1..10 step 21, 3, 5, 7, 9 (a salti)
until serve per gli indici

Quando un ciclo deve passare su tutti gli elementi di qualcosa che ha dimensione n, si scrive 0 until n: gli indici vanno da 0 a n-1, quindi l'ultimo valore da visitare è proprio n-1. Rivedrai questo schema nel capitolo 6.

Il ciclo interno riparte da capo a ogni giro di quello esterno

Nel laboratorio sopra il primo for sceglie la riga, il secondo genera le colonne. Quando l'esterno passa alla riga successiva, l'interno non riprende da dove era rimasto: ricomincia da 1. Per questo la tavola cresce in due direzioni: lungo la riga (1 2 3) e scendendo di riga in riga (1 2 3, poi 2 4 6, poi 3 6 9).

5.2 while: ripetere finché...

while (condizione) { ... } esegue il blocco finché la condizione resta vera. Serve quando non sai in anticipo quante volte ripetere. Attenzione: dentro al ciclo qualcosa deve cambiare, altrimenti si gira per sempre.

Lo 0 che ferma il ciclo finisce anche nella somma

Il valore che ferma il ciclo viene letto come gli altri: prima arriva in numero e poi viene sommato. Qui non è un problema, perché sommare 0 non cambia il totale. Ma se il segnale di fine fosse stato un numero diverso, per esempio -1, sommarlo avrebbe rovinato il conto: in quel caso la somma va fatta solo quando il valore letto non è il segnale di fine, con un if (numero != -1) dentro al ciclo.

In questo ciclo, che cosa succede se togli la riga secondi = secondi - 1?

fun main() {
    var secondi = 5
    while (secondi > 0) {
        println("Mancano $secondi secondi")
    }
}
Stampa una volta sola e bastaNo: la condizione secondi > 0 resta vera, quindi il ciclo non si ferma.
Continua a girare senza fermarsiEsatto: dentro al while qualcosa deve cambiare, altrimenti la condizione resta sempre vera.
Kotlin dà un erroreNon è un errore di scrittura: il programma parte, ma non finisce. Il manuale lo interrompe dopo troppe istruzioni.

Nel while la condizione si guarda prima di ogni giro. Se non cambi la variabile che la tiene in vita, il blocco si ripete per sempre.

Con while Kotlin guarda la condizione prima. Se è già falsa, il blocco non parte. A volte invece ti serve girare almeno una volta (un menu, una domanda). Per quello c'è do ... while, nella sezione dopo.

5.3 do ... while: almeno una volta

do { ... } while (condizione) esegue il blocco prima e controlla la condizione dopo: serve quando il blocco deve girare almeno una volta (tipico nei menu "ripeti finché non scegli esci").

Devi chiedere un dato almeno una volta, e poi ripetere finché l'utente non decide di smettere. Quale ciclo usi?

whileCon while la condizione è controllata prima: se è subito falsa, il blocco non viene mai eseguito.
do { ... } while (...)Esatto: do ... while esegue il blocco e controlla la condizione dopo, quindi gira almeno una volta.
for (i in 1..10)Con for sai in anticipo quante volte ripetere; qui non lo sai.

Schema tipico, come nell'esempio sopra: il blocco parte subito, poi si ripete finché la condizione resta vera.

I cicli infiniti: errori tipici

Il manuale ti avvisa con "il programma ha eseguito troppe istruzioni".

5.4 break e continue

break esce subito dal ciclo, continue salta al giro successivo. Sono comodi per fermarsi appena hai trovato quello che cercavi o per ignorare i casi che non ti interessano.

Che cosa fa lo zero dentro un ciclo

Nell'esempio 5, 0 significa «ho finito». Se l'utente scrive pippo, ?: 0 mette comunque 0 e il ciclo si ferma. Per quel programma va bene: sommare 0 non cambia il totale.

Nei cicli che girano un numero fisso di volte (esempio 7) lo 0 non ferma niente: entra nei calcoli e il ciclo prosegue. Prima di scrivere ?: 0 chiediti: se il dato non si capisce, il ciclo deve fermarsi o andare avanti?

  1. continue salta il resto del giro: quando i vale 3 non si arriva alla print, e il ciclo riparte con 4.
  2. break esce dal ciclo: quando i vale 5 il ciclo finisce subito, prima di stampare.
  3. Per questo l'output è 1 2 4 : il 3 è saltato, il 5 non è mai stampato.
Extra: fermarsi quando i dati finiscono

L'esempio 5 si ferma quando leggi 0. Questo laboratorio, invece, continua finché c'è una riga. Quando i dati finiscono, premi Ctrl+D (o ⏹ fine dei dati): readlnOrNull() dà null e il while esce.

La variabile deve poter valere null: si scrive String?. Senza il ? Kotlin non accetta null. Alla prima lettura puoi saltare questo laboratorio. Ti basta while (numero != 0).

5.5 I due schemi fondamentali: contatore e accumulatore

Il contatore conta quante volte succede qualcosa; l'accumulatore somma (o moltiplica) dei valori uno dopo l'altro. Sono lo schema di metà degli esercizi sui cicli.

5.6 Cicli su un testo

Lo stesso for scorre anche i caratteri di un testo: for (c in parola). Qui non usi un intervallo di numeri, ma il valore stesso. Nel capitolo 6 vedrai le liste: lì scriverai for (nome in nomi) allo stesso modo.

Stai cercando la prima vocale in una parola e vuoi smettere appena l'hai trovata. Che cosa metti dentro l'if?

break: esce subito dal cicloEsatto: break interrompe il ciclo, quindi non si fanno giri inutili.
continue: salta al giro successivoNo: continue salta questo giro e continua a cercare, ma tu vuoi fermarti.
Niente: il ciclo finisce da soloFunziona, ma continua a girare su tutte le lettere anche quando hai già la risposta.

break = esci dal ciclo; continue = salta questo giro e passa al successivo.

Il ciclo su una collezione si legge così

for (nome in nomi) significa "per ogni nome dentro nomi". Usa il singolare per l'elemento e il plurale per la collezione: il codice diventa una frase leggibile.

listOf in anteprima: lo vedrai al capitolo 6

Qui basta leggere listOf("Anna", "Luca", "Sara") come "l'elenco di questi tre nomi". Come si crea, si modifica e si scorre una lista è spiegato per davvero nel capitolo 6: per adesso non serve ricordarne la sintassi.

5.7 Riepilogo

FormaQuando usarla
for (i in 1..n)sai quante volte ripetere, estremi compresi
for (i in 0 until n)indici di una collezione di dimensione n
for (x in collezione)per ogni elemento della collezione
while (condizione)non sai quante volte: ripeti finché serve
do { ... } while (c)il blocco deve girare almeno una volta
break / continueesci dal ciclo / salta al giro dopo

5.8 Esercizi guidati

Leggi un numero n e calcola la somma di tutti i numeri da 1 a n (per esempio con n = 10 la somma è 55). Stampa anche la media dei numeri. Con 10 le due righe sono Somma da 1 a 10 = 55 e Media = 5.5.

Prima del ciclo dichiara var somma = 0; dentro al ciclo fai somma = somma + i. La media è somma.toDouble() / n.

  1. var somma = 0 è l'accumulatore: parte da zero e cresce a ogni giro.
  2. for (i in 1..n) ripete il blocco per ogni numero da 1 a n compreso.
  3. somma.toDouble() / n evita la divisione intera: senza la conversione la media di 10 numeri sarebbe stata troncata.
  4. Il ciclo è più sicuro della formula matematica n*(n+1)/2? No: è solo più leggibile per un principiante, ed è lo stesso schema che serve per somme di valori letti da input.

Leggi un numero e stampa la sua tabellina da 1 a 10 nel formato 7 x 3 = 21. Alla fine stampa la somma di tutti i risultati: con 7 l'ultima riga è Somma dei risultati: 385.

Il risultato di ogni riga è numero * i: mettilo in una variabile, stampalo e aggiungilo all'accumulatore.

  1. Il ciclo va da 1 a 10 perché 1..10 comprende entrambi gli estremi.
  2. Il risultato viene calcolato una volta sola e salvato in una val: così non si ripete il conto due volte (nella stampa e nella somma).
  3. L'accumulatore somma è fuori dal ciclo: se fosse dentro, verrebbe azzerato a ogni giro.

Leggi un numero n e stampa i numeri da 1 a n con queste regole: se il numero è divisibile per 3 stampa Fizz, se è divisibile per 5 stampa Buzz, se è divisibile per entrambi stampa FizzBuzz, altrimenti il numero stesso. Conta anche quante volte compare FizzBuzz. Con n = 20 l'ultima riga è FizzBuzz: 1 (solo il 15).

Controlla prima il caso "divisibile per 3 e per 5" (i % 3 == 0 && i % 5 == 0), altrimenti gli altri due lo catturano prima.

  1. L'ordine delle condizioni è decisivo: il caso 15 (divisibile per 3 e per 5) deve essere controllato per primo.
  2. Se avessi messo i % 3 == 0 come prima condizione, il 15 sarebbe finito in "Fizz" e "FizzBuzz" non comparirebbe mai.
  3. Il contatore quantiFizzBuzz è dichiarato var fuori dal ciclo e incrementato solo nel primo ramo.
  4. println(i) accetta un numero: Kotlin lo trasforma in testo per stamparlo.

Questo programma parte senza errori, ma stampa Somma: 10 invece di Somma: 15. Deve sommare i numeri da 1 a 5. Trova l'errore logico nell'intervallo e correggilo: il programma deve stampare 15.

until esclude l'ultimo valore: 1 until 5 dà 1, 2, 3, 4. Per includere il 5 serve 1..5 oppure 1 until 6.

  1. Il codice compila e gira: l'errore è logico. 1 until 5 produce 1, 2, 3, 4 e la somma è 10.
  2. Con 1..5 gli estremi sono entrambi compresi: 1, 2, 3, 4, 5 e la somma è 15.
  3. La stessa correzione si poteva scrivere come 1 until 6: until esclude sempre l'ultimo valore.
  4. Quando il risultato è sbagliato ma non c'è nessun messaggio di errore, il sospettato numero uno è l'intervallo del ciclo.

Il programma deve stampare Somma: 15 (numeri da 1 a 5), ma non arriva alla stampa: il ciclo non finisce. Eseguilo. Quando il manuale scrive che ha eseguito troppe istruzioni, fermati e correggi. Dentro al while manca il passo che fa crescere i.

Dentro un while qualcosa deve far prima o poi diventare falsa la condizione: qui manca l'istruzione che fa crescere i.

  1. i resta sempre 1: la condizione i <= 5 è vera per sempre e il ciclo non finisce (ciclo infinito).
  2. Il manuale ti avvisa con "il programma ha eseguito troppe istruzioni", come farebbe un vero programma che si blocca.
  3. Aggiungendo i = i + 1 dentro al ciclo, i diventa 2, 3, 4, 5, 6: quando vale 6 la condizione è falsa e il ciclo si ferma.
  4. Lo stesso schema con for (i in 1..5) non ha questo rischio, perché è il for stesso a far crescere i.
  • so usare for con .., until, downTo e step
  • so scegliere fra for, while e do ... while
  • so usare break e continue e so spiegare la differenza
  • so scrivere un contatore e un accumulatore senza sbagliare il valore iniziale
  • so scorrere una stringa carattere per carattere e una lista elemento per elemento
  • so riconoscere un ciclo infinito e so come evitarlo
  • so leggere un messaggio di errore e correggere il programma da solo