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. Un salto indietro al capitolo 3, invece, serve per ricordare come si legge un numero dall'input.

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() {
    var totale = 0
    for (i in 1..3) {
        print("Numero $i: ")
        totale += readln().toInt()
    }
    println("Totale: $totale")
}
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 Totale: 11, perché pippo viene saltatoNo: toInt() non salta i dati che non capisce, si ferma con un errore.
Stampa Totale: 47pippo, attaccando i tre valoriNo: totale è un Int e toInt() restituisce numeri, non testo.

Dentro un ciclo l'errore capita una volta per ogni giro: un dato non numerico ferma tutto al giro in cui compare. 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.

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 .. il conteggio va sempre dal più piccolo al più grande. Per scendere serve downTo.
Dà un erroreNon è un errore: il ciclo semplicemente non ha nessun giro da fare.

Per scendere: 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.

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: do { dato = readln().toInt() } while (dato != 0).

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").

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.

Il valore di riserva: che cosa deve fare il ciclo con un dato sporco?

La forma robusta readlnOrNull()?.toIntOrNull() ?: 0 usa quasi sempre ?: 0, ma dentro un ciclo quello zero fa due cose diverse a seconda del ciclo. Nell'esempio 5 lo 0 è anche la sentinella che ferma i conti: un dato sporco viene letto come 0 e chiude il ciclo, come se l'utente avesse scritto 0 per fermarsi — va bene solo se perdere i dati rimasti è accettabile. Nei cicli che girano un numero fisso di volte (esempio 7) lo 0 non ferma niente: entra nei calcoli come numero qualsiasi e il ciclo prosegue. Prima di scrivere ?: qualcosa dentro un ciclo chiediti: "che cosa deve succedere al ciclo se questo dato non si capisce?" Se deve fermarsi, scegli come valore di riserva proprio la sentinella; se deve andare avanti, qualunque numero va bene.

  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.
Lo stesso ciclo, senza nessuna sentinella

L'esempio 5 si fermava quando l'utente scriveva 0; questo si ferma quando i dati finiscono davvero — premi Ctrl+D (o il pulsante ⏹ fine dei dati) e il ciclo esce da solo, anche senza leggere nessun numero. È lo schema che usano i programmi che leggono un file riga per riga: non c'è un valore magico che significa "basta", c'è la fine dei dati. Due dettagli da notare: la variabile è dichiarata String? — senza il punto interrogativo il tipo dedotto sarebbe una stringa normale, e la lettura di null darebbe l'errore "la variabile è di tipo String e non può valere null"; e dentro il ciclo la lettura sporca usa ?: 0, perché qui un dato non capito non deve fermare niente — il ciclo si ferma solo alla fine dei dati.

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 testo e su collezioni

Lo stesso for serve per scorrere un testo carattere per carattere o una collezione elemento per elemento: in questi casi non si usa l'intervallo, ma direttamente il valore.

Stai cercando una parola in una lista 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 tutti gli elementi 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.

Prima del ciclo dichiara var somma = 0; dentro al ciclo fai 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.

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.

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 la somma dei numeri da 1 a 5 (cioè 15), ma non finisce mai. Interrompilo con ▶ Esegui dopo aver capito il problema e correggilo.

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++ 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