Introduzione a Kotlin: Parte 4 – Flusso di controllo

I

Benvenuti nella quarta parte Flusso di controllo in Kotlin della serie Introduzione a Kotlin. Potete trovare qui l’articolo precedentequi 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 .() 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, trueo 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ù brevechiara 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,

  1. Nei cicli, un break o un continue classico, affligge solo l’ultimo loop in cui ci si trova
  2. 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.

A proposito di me

Ruggiero Altini

Laureato in Informatica, appassionato di algoritmi, ha esperienza in Computer Vision, Machine Learning e nello sviluppo di app per iOS e Android. Ha usato e usa estensivamente Java, Python, C++, Swift e Kotlin. Si interessa attualmente ai rami dell'Intelligenza Artificiale.

I nostri Partner

Gli articoli più letti

Articoli recenti

Commenti recenti