Benvenuti nella quarta parte Flusso di controllo in Kotlin della serie Introduzione a Kotlin. Potete trovare qui l’articolo precedente, qui la prima parte, mentre invece in questo articolo uno sguardo generale al linguaggio per i programmatori Java/OO.
Flusso di controllo in Kotlin
Il flusso di controllo in un linguaggio di programmazione è l’ordine in cui vengono eseguite le istruzioni. Con le strutture di controllo si definisce quale dev’essere il flusso di controllo delle istruzioni, chiamate a funzione o dichiarazioni nel programma.
Normalmente, le istruzioni sono eseguite in sequenza, una dopo l’altra, all’interno di una funzione o in un metodo.
fun main(args: Array<String>) { eseguiTask1() eseguiTask2() eseguiTask3() }
Tuttavia, in un programma più complesso, si può prevedere un flusso che non sia sempre statico ma dinamico ovvero che cambia in base a determinate circostanze.
O semplicemente, si può voler eseguire più volte lo stesso blocco di istruzioni.
Vediamo adesso tutte le strutture di controllo base disponibili in Kotlin, incluse quelle accennate in precedenza.
Ciclo For
Questo era stato già visto nel primo articolo della serie nel paragrafo “Cicli”.
Il for
generalmente è utilizzato o per eseguire un numero prefissate di volte un blocco di istruzioni, oppure per iterare gli elementi di una collezione (ad es. una lista o un array), e farci qualcosa.
Adesso entreremo più nel dettaglio vedendo differenti modi di eseguire un ciclo for per differenti utilizzi.
Metodo #1: Classica ripetizione
Già affrontato all’inizio della guida.
fun main(args: Array<String>) { for (i in 1..5) { println("Istruzione eseguita 5 volte") } }
Utile o per eseguire un numero prefissato di volte un blocco di istruzioni, oppure per iterare un array sfruttando la i
come indice.
Metodo #2: Come il metodo #1, ma con una progressione personalizzata
Se per qualche motivo vi serve che la i
faccia dei “salti”, o vi interessa che scorra in maniera decrescente anziché crescente, ad esempio per operazioni matematiche o particolari su array, ci sono le funzioni infisse downTo
e step
. Le funzioni infisse saranno spiegate nel prossimo capitolo che riprenderà le funzioni per spiegarle più analiticamente. Per adesso basti sapere che una funzione infissa non ha bisogno degli operatori .
e ()
per essere denotata.
for (i in 10 downTo 0 step 2) { println(i) } /* Output: 10 8 6 4 2 0 */
Metodo #3: Iterare una collezione
Questo forse è il metodo più comune in cui vedrete impiegati i for
nella maggior parte dei contesti nello sviluppo software classico. Eppure, anche questi sono soppiantati il più delle volte nella programmazione funzionale, anch’essa che sarà approfondita in futuro.
val myArray = intArrayOf(1, 2, 3, 4, 5, 6, 7, 8, 9, 10) for (item in myArray) { print("$item ") // 1 2 3 4 5 6 7 8 9 10 }
Espressione If
Struttura di controllo fondamentale in qualsiasi linguaggio di programmazione, anche se non la vedete esplicitamente nel codice.
L’espressione if o if-else (“se” o “se-altrimenti”) consiste nel verificare che una determinata condizione sia o no vera. Se è vera, viene eseguito il blocco posto dopo il controllo; se è falsa, non viene eseguito il blocco, ma se è presente in un caso else
(“altrimenti”), allora verrà eseguito quello.
Immaginiamo dato un array arbitrario, di voler stampare tutti i numeri pari:
for (item in myArray) { // Se item è divisibile per 2 <=> Se item è pari if (item % 2 == 0) { println(item) } } /* 2 4 6 8 10 */
Proviamo ora un esempio più complicato. Diciamo che vogliamo fare un programma che dato in input un anno, ci dice se è bisestile oppure no.
Un anno è bisestile se è divisibile per 4, ma non se è divisibile per 100, a meno che, non è divisibile anche per 400.
Un po’ complessa questa definizione di bisestile, no? Normalmente si usa dire “divisibile per 4”, tuttavia gli anni come 1900 e 2100 non sono bisestili, pur essendo divisibili per 4. Il 2000 invece lo è perché pur essendo divisibile per 100, è divisibile anche per 400.
Vediamo come risolveremmo un simile problema con gli if-else.
val year = 2050 val bisestile: Boolean // Se l'anno è divisibile per 4 if (year % 4 == 0) { // Se è divisibile per 400 if (year % 400 == 0) { bisestile = true // Allora è bisestile // La selezione di questo if finisce qui } else if (year % 100 == 0) { // Altrimenti se è divisibile per 100 (quindi non per 400), non bisestile bisestile = false } else { // Altrimenti, se è divisibile per 4 (premessa primo if) // Ma né divisibile per 400 o per 100 (premessa if e else-if precedenti) // l'anno è bisestile bisestile = true } } else { // Altrimenti (l'anno non è divisibile per 4) // l'anno non è bisestile bisestile = false } println(bisestile) // false
Generalizzabile in una funzione in questo modo:
fun main(args: Array<String>) { println("2100: " + annoBisestile(2100)) // 2100: false println("2000: " + annoBisestile(2000)) // 2000: true println("2016: " + annoBisestile(2016)) // 2016: true println("2018: " + annoBisestile(2018)) // 2018: false } fun annoBisestile(anno: Int): Boolean { val bisestile: Boolean if (anno % 4 == 0) { if (anno % 400 == 0) { bisestile = true } else if (anno % 100 == 0) { bisestile = false } else { bisestile = true } } else { bisestile = false } return bisestile }
Espressione When
L’espressione when
è molto interessante in Kotlin. Negli altri linguaggi è comunemente conosciuta come switch
, ma in Kotlin, oltre ad avere un nome diverso, presenta una sintassi molto leggera e un uso leggermente più potente.
When (o Switch) classica
Quando si usano if
a catena come prima ad esempio, per confrontare diverse volte il valore di una variabile, è possibile utilizzare una when
per alleggerire e chiarire il flusso generale.
Nella forma classica dopo la when
si pone la variabile per cui si vuole controllare i valori, e a cascata i valori per cui in caso di equivalenza, eseguire l’istruzione o il blocco di istruzioni corrispondente.
when (x) { 0 -> print("x == 0") 1 -> print("x == 1") else -> { // Porre attenzione al blocco print("x non è né 0 né 1") } }
Equivalente a
if (x == 0) { print("x == 0") } else if (x == 1) { print("x == 1") } else { print("x non è né 0 né 1") }
Quale sintassi delle due si capisce più al volo?
Per dimostrare l’utilità e la potenza della when
, rivediamola nel caso d’uso di prima dell’anno bisestile.
When senza argomento
Quando alla when non si fornisce un argomento, vengono valutate tutte le espressioni booleane (cioè che restituiscono un Boolean
, true
o true
) finché non si ottiene un valore true
.
fun annoBisestile(anno: Int): Boolean { when { anno % 400 == 0 -> return true anno % 100 == 0 -> return false anno % 4 == 0 -> return true else -> return false } }
Molto più corto di quello con gli if
, non è vero?
È ulteriormente semplificabile con la forma semplificata equivalente sicché non si hanno più istruzioni in successione:
fun annoBisestile(anno: Int) = when { anno % 400 == 0 -> true anno % 100 == 0 -> false anno % 4 == 0 -> true else -> false }
Se conoscete Java o un altro linguaggio simile, in quanto riuscite a scrivere nella forma più breve e chiara questa funzione?
Ciclo While e do-while
Il ciclo while
è un altro costrutto molto comune e classico già dal linguaggio C, è caratterizzato da una condizione, così come un if
, e un blocco di istruzioni eseguito in verità di quella condizione, così come un if
. A differenza di un if
però, il ciclo while
ripete quel blocco di istruzioni finché il valore non diventa falso.
Un esempio molto comune e quando si vuole che un determinato input sia corretto, ad es. input di un numero maggiore o uguale a 0.
fun main(args: Array<String>) val n: Int var input: String? // Ciclo do-while do { print("Inserisci il numero di elementi da sommare (>0): ") input = readLine() } while ((!isNumeric(input) || input!!.toInt() < 0) && // input non è un numero oppure è un numero <0 { println("Non è un numero valido!"); true }()) // pre-condizioni fallite n = input!!.toInt() var somma: Double = 0.0 var i = 0 while (i < n) { // Ciclo while print("[$i] Inserisci un numero: ") input = readLine() if(isNumeric(input)) { somma += input!!.toDouble() i += 1 } else { println("Non è un numero!") } } println("somma = $somma") } fun isNumeric(string: String?): Boolean { string ?: return false if(string.isEmpty()) return false var dot = false var beginning = true for (character in string) { when (character) { in '0'..'9' -> {} // OK '-' -> if (!beginning) return false // Solo all'inizio '.' -> if(dot) return false else dot = true // Solo una volta else -> return false } beginning = false } return true }
Esempio di utilizzo (input e output):
Inserisci il numero di elementi da sommare (>0): a Non è un numero valido! Inserisci il numero di elementi da sommare (>0): 5 [0] Inserisci un numero: b Non è un numero! [0] Inserisci un numero: -5 [1] Inserisci un numero: 10 [2] Inserisci un numero: 20.0 [3] Inserisci un numero: 31.5 [4] Inserisci un numero: .4 somma = 56.9
Interruzioni nelle iterazioni
Spiegate tutte le strutture di controllo tipiche in Kotlin, può essere inutile introdurre delle istruzioni che anch’esse hanno il potere modificare il flusso di controllo, nella fattispecie, alle strutture di controllo cicliche, ovvero nel for
e nel while
.
break
serve a uscire forzatamente dal loop in cui ci si trova. In caso di loop innestati, da quello più innestato.continue
interrompe come break l’iterazione, ma anziché concluderla, si torna all’inizio.
Ecco un esempio alternativo per l’input di un numero maggiore di zero, a voi la scelta se meno o più chiaro di prima.
fun main(args: Array<String>) { var n: Int while (true) { println("Inserisci un numero intero maggiore di zero: ") val input = readLine() // input è di tipo String?, quindi nullabile input ?: continue // Concludi l'iterazione e torna all'inizio del while se input == null if(!isNumeric(input)) continue // input dev'essere numerico altrimenti continua n = input.toInt() if(n <= 0) continue // n dev'essere maggiore di zero altrimenti continua break // Esci dal ciclo while } }
Esempio di esecuzione:
Inserisci un numero intero maggiore di zero: a Inserisci un numero intero maggiore di zero: -5 Inserisci un numero intero maggiore di zero: 10 Process finished with exit code 0
Etichette a cicli e lambda
Si potrebbe fare un approfondimento di un intero articolo in merito alle etichette in Kotlin, e forse sarà fatto in un’appendice, ma per finire, in Kotlin è anche possibile etichettare i loop o le funzioni lambda per decidere da quale uscire esattamente. Infatti,
- Nei cicli, un
break
o uncontinue
classico, affligge solo l’ultimo loop in cui ci si trova - Nelle funzioni lambda, un
return
porta ad uscire dalla funzione principale, non dalla funzione lambda.
Ecco un esempio sui loop.
loop@ for (i in 1..100) { for (j in 1..100) { if (condizione) break@loop // Esce dal primo loop } }
L’esempio sulle funzioni lambda sarà fatto quando … saranno spiegate nel dettaglio le funzioni lambda! Per chi volesse approfondire comunque, c’è la KotlinDocs.
Esercizio e soluzione articolo precedente
Scrivere una classe Piece (astratta), e una classe Board. La classe Piece deve presentare una funzione (anche astratta) moves(). Deve anche avere una proprietà placing che restituisce la sua posizione sulla scacchiera, se è presente.
La classe Board invece, deve fornire due metodi place, uno che accetta due parametri, uno per il pezzo e l’altro per la posizione, e piazza il pezzo in quella posizione. L’altro invece, con un solo parametro, quello del pezzo, e restituisce una posizione nullabile.
Come classe per la posizione è possibile usare la classe Point presente negli ultimi articoli.
data class Point(var x: Int, var y: Int) enum class Color { White, Black } abstract class Piece(placing: Point, var board: Board, val color: Color) { init { board.place(this, placing) } abstract fun moves(): List<Point> val placing: Point? get() = board.place(this) } class Board { private val spots: Array<Array<Piece?>> private val reverseMap: MutableMap<Piece, Point> = HashMap() init { spots = Array(size = 8, init = { Array<Piece?>(size = 8, init = {null}) }) } fun place(piece: Piece, position: Point) { val row = position.y val col = position.x assert(row < 8 && col < 8 && row >= 0 && col >= 0) spots[row][col] = piece reverseMap.put(piece, Point(y = row, x = col)) } fun place(piece: Piece): Point? = reverseMap.get(piece) }
Spiegazioni eventuali verranno fatte nel prossimo articolo, comunque si è scelto di utilizzare un array di array (8×8) per definire i pezzi, e un dizionario per un mapping invertito, dove dato un pezzo viene data la sua posizione, per ottimizzarne la complessità da O(n^2) a O(1).
Ed ecco il nuovo esercizio:
Scrivere la classe Pawn che implementi la classe Piece, e contenga le regole del movimento dei pedoni a scacchi. Enumerare le caselle legali solamente in base alla posizione del singolo pedone sulla scacchiera; non in base a tutti gli altri pezzi presenti.
Testare il codice con un pedone bianco in E2, uno nero in E7, e un altro in una posizione qualsiasi tra la terza e la sesta riga.