CodeGym /Corsi /Kotlin SELF /Indici e sottostringhe in Kotlin: length, lastIndex, inde...

Indici e sottostringhe in Kotlin: length, lastIndex, indexOf, substring

Kotlin SELF
Livello 9, Lezione 3
Disponibile

1. Stringa come sequenza di caratteri

Quando inizi a “analizzare” una stringa, il cervello vuole automaticamente trattarla come testo di un libro. Ma al computer il libro non serve: gli serve una sequenza di caratteri, in cui ogni carattere sta al proprio posto e ha un numero. Questo numero si chiama indice, e in Kotlin (come quasi ovunque) l’indicizzazione parte da zero.

Indici da zero: perche il primo carattere e 0

L’idea “il primo elemento ha indice 0” all’inizio sembra una presa in giro, ma diventa presto comoda: indice = “quanti caratteri ci sono prima di quello corrente”. Prima del primo carattere ci sono zero caratteri — quindi indice 0. Prima del secondo ce n’e uno — quindi indice 1. Questo tornera utile quando taglieremo con substring().

Guardiamo la stringa "Kotlin":

Carattere
K
o
t
l
i
n
Indice
0
1
2
3
4
5

Il numero di caratteri e 6, mentre l’ultimo indice e 5.

length e lastIndex: non confondere “dimensione” e “ultimo indice”

La proprieta length dice quanti caratteri ci sono nella stringa. Invece lastIndex e l’indice dell’ultimo carattere.

E importante sentire la differenza: length e “quantita”, lastIndex e “ultima posizione”. Nelle collezioni e negli array si usa esattamente la stessa logica: l’ultimo indice e uguale a size - 1.

Mini esempio:

fun main() {
    val s = "Kotlin"

    println(s.length)      // 6
    println(s.lastIndex)   // 5
}

Se la stringa e vuota (""), allora length sara 0, e lastIndex diventera -1. Suona strano, ma e logico: “non esiste un ultimo carattere”, quindi “l’ultimo indice” finisce in negativo. Proprio per questo parliamo cosi tanto dei controlli dei limiti.

2. Accesso a un carattere per indice: s[i] e controllo accurato dei limiti

Ora che esistono gli indici, viene spontaneo fare cosi: “dammi il carattere numero 0”. E Kotlin permette davvero di accedere a un carattere per indice tramite parentesi quadre. Ma c’e un trabocchetto: se l’indice e sbagliato, il programma andra in errore con un’eccezione (e tu devi scrivere codice che non crolla per un solo spazio nel posto sbagliato).

s[i] restituisce Char: il testo non e sempre una stringa

L’operatore s[i] restituisce un singolo carattere di tipo Char. Un carattere non e una stringa. Una stringa e composta da molti caratteri.

fun main() {
    val s = "Kotlin"

    val first: Char = s[0]
    val last: Char = s[s.lastIndex]

    println(first) // K
    println(last)  // n
}

Se provi a fare s[6] per "Kotlin", il programma andra in errore: gli indici li sono solo 0..5.

Controllo dei limiti: “prima di prendere, assicurati che si possa”

Qui aiuta tenere a mente una regola breve: gli indici validi sono tutti i numeri che non sono minori di 0 e sono minori di length.

Mini helper che restituisce in modo sicuro un carattere o '?' se l’indice e errato:

fun charOrQuestion(text: String, index: Int): Char {
    if (index < 0 || index >= text.length) return '?'
    return text[index]
}

fun main() {
    println(charOrQuestion("Kotlin", 2))   // t
    println(charOrQuestion("Kotlin", 99))  // ?
}

Stringa vuota: il caso piu piccolo, ma molto insidioso

Una stringa vuota e una stringa di lunghezza 0. Non ha ne il primo ne l’ultimo carattere. Questo significa che anche s[0] e pericoloso.

fun main() {
    val s = ""

    println(s.length)    // 0
    println(s.lastIndex) // -1
    println(s[0])        // tak delat' nel'zya — budet oshibka
}

Una buona abitudine che salva i nervi: prima di accedere a s[0] chiediti: “la stringa e sicuramente non vuota?”

3. Ricerca nella stringa: indexOf() e controlli contains(), startsWith(), endsWith()

Di solito non analizziamo le stringhe per noia, ma perche nella stringa c’e un separatore: spazio, @, =, due punti, punto e cosi via. Lo scenario e spesso questo: “trova dove si trova il separatore e dividi la stringa in due parti”.

indexOf(...): la convenzione di -1

Il metodo indexOf(...) cerca una sottostringa (o un carattere) e restituisce l’indice della prima occorrenza. Se non trova nulla, restituisce -1. Questa e una convenzione molto importante: -1 significa “non trovato”.

fun main() {
    val email = "user@example.com"

    val at = email.indexOf('@')
    println(at) // 4

    val slash = email.indexOf('/')
    println(slash) // -1
}

Se ti dimentichi di controllare -1 e inizi a tagliare la stringa a quell’indice, otterrai o un errore di limiti, o un risultato molto strano.

contains(), startsWith(), endsWith(): quando non servono gli indici

A volte non ti serve l’indice: ti serve solo un “si/no”. Allora e piu comodo usare i metodi di verifica.

fun main() {
    val filename = "report.txt"

    println(filename.contains("."))      // true
    println(filename.startsWith("rep"))  // true
    println(filename.endsWith(".txt"))   // true
}

Questi metodi rispondono a domande semplici:

  • contains() — la sottostringa passata e contenuta nella stringa?
  • startsWith() — la stringa inizia con la sottostringa passata?
  • endsWith() — la stringa finisce con la sottostringa passata?

Questi controlli spesso rendono il codice piu sicuro: metti meno le mani sugli indici, e hai meno probabilita di sbagliare “di 1”.

4. substring(): tagliamo la stringa in parti senza inciampare

Se gli indici sono le “coordinate” nella stringa, allora substring() sono le “forbici”. Quasi ogni analisi di stringhe si riduce a uno schema: trovare l’indice del separatore e ricavare la parte sinistra/destra.

Regola principale di substring(start, end): end non e incluso

Il metodo substring(start, end) prende i caratteri dalla posizione start inclusa, mentre end non viene incluso. E lo stile classico dell’“intervallo semiaperto”: e incredibilmente comodo, ma all’inizio confonde.

Schema:

"Kotlin"
 012345

substring(0, 3) -> prendiamo gli indici 0,1,2 -> "Kot"

Esempio:

fun main() {
    val s = "Kotlin"

    println(s.substring(0, 3)) // Kot
    println(s.substring(3, 6)) // lin
}

Nota: end puo essere uguale a length, ed e normale. “Fino alla fine della stringa” e proprio end = length.

substring(start) — dalla posizione fino alla fine

Esiste una forma comoda substring(start): prende da start fino alla fine della stringa.

fun main() {
    val s = "Kotlin"

    println(s.substring(2)) // tlin
}

Questo e molto comodo per la parte destra dopo il separatore: “dopo @ fino alla fine”, “dopo = fino alla fine”.

Limiti di substring(): dove il programma puo andare in errore

substring() richiede limiti corretti. In modo semplificato: start e end devono stare nell’intervallo 0..length, e start <= end. Se lo violi, ci sara un errore.

Per questo un programmatore esperto quasi mai scrive substring(...) “di getto”: prima controlla che gli indici siano validi.

5. Pattern “trovato → verificato → estratto”

Ora mettiamo insieme tutto in un pattern solido e lo aggiungiamo alla nostra app didattica da console. Immaginiamo di fare un programmino “Profilo utente”: l’utente inserisce nome completo in una sola riga e email, e noi stampiamo una scheda ordinata. Oggi ci concentreremo proprio su come ottenere in modo sicuro parti di queste stringhe tramite gli indici.

La realta e dura: le persone inseriscono il nome in modo casuale. Si, mettono spazi strani. Si, a volte dimenticano @. Non sono “utenti cattivi”, e “mondo reale”.

Analizziamo il nome completo "Imya Familiya" con indexOf(' ')

La versione piu semplice del nome completo sono due parole separate da uno spazio. Per ora non usiamo apposta split() (ci sara nella prossima lezione), quindi faremo un “taglio manuale”: troviamo l’indice dello spazio e ricaviamo la parte sinistra e destra.

fun main() {
    val fullName = "Ivan Petrov"
    val space = fullName.indexOf(' ')

    val firstName = if (space == -1) fullName else fullName.substring(0, space)
    val lastName = if (space == -1) "" else fullName.substring(space + 1)

    println(firstName) // Ivan
    println(lastName)  // Petrov
}

Nota space + 1: non vogliamo che lo spazio finisca nel cognome. Se lo spazio e l’ultimo carattere della stringa, substring(space + 1) diventera substring(length), e questa e semplicemente una stringa vuota (ed e normale).

Estraiamo il dominio dall’email: indexOf('@') e controlli

L’email e un esempio classico di stringa con separatore. Vogliamo prendere il dominio: tutto cio che sta dopo @. Ma dobbiamo proteggerci dai casi "@example.com" e "user@".

fun extractDomain(email: String): String {
    val at = email.indexOf('@')
    if (at == -1) return "INVALID"
    if (at == 0) return "INVALID"
    if (at == email.lastIndex) return "INVALID"

    return email.substring(at + 1)
}

fun main() {
    println(extractDomain("user@example.com")) // example.com
    println(extractDomain("user@"))            // INVALID
}

Qui usiamo email.lastIndex per controllare: “@ e l’ultimo carattere o no”. Ed e proprio un caso in cui lastIndex si legge meglio di length - 1.

Analizziamo una stringa nel formato "key=value"

Un formato di input molto diffuso e chiave=valore. Oggi faremo una versione semplice: troviamo = e tagliamo la stringa.

fun main() {
    val line = "city=Tokio"
    val eq = line.indexOf('=')

    val key = if (eq == -1) "" else line.substring(0, eq)
    val value = if (eq == -1) "" else line.substring(eq + 1)

    println("key='$key', value='$value'") // key='city', value='Tokio'
}

Il pattern e lo stesso: trovato l’indice → verificato -1 → estratte la parte sinistra e destra.

Costruiamo la “scheda profilo” senza confondere parsing e output

Ora assembliamo un piccolo frammento della nostra app: leggiamo fullName e email, calcoliamo firstName, lastName, domain e stampiamo la scheda.

fun extractDomain(email: String): String {
    val at = email.indexOf('@')
    if (at <= 0 || at == email.lastIndex) return "INVALID"
    return email.substring(at + 1)
}

fun main() {
    print("Vvedite imya i familiyu: ")
    val fullName = readln()

    print("Vvedite email: ")
    val email = readln()

    val space = fullName.indexOf(' ')
    val firstName = if (space == -1) fullName else fullName.substring(0, space)
    val lastName = if (space == -1) "" else fullName.substring(space + 1)

    val domain = extractDomain(email)

    val card = """
        |Profil'
        |-------
        |Imya: $firstName
        |Familiya: $lastName
        |Domen email: $domain
    """.trimMargin()

    println(card)
}

Qui la logica di parsing (indici, indexOf(), substring()) sta vicina, ma non e mescolata in un’unica riga enorme. Questo stile e piu facile da fare debug: se la scheda e strana, puoi stampare i valori intermedi space, firstName, lastName.

Promemoria su indici e punti critici

Quando lavori con le stringhe, e utile avere in mente una piccola tabella-promemoria: cosa viene restituito e cosa va controllato obbligatoriamente.

Operazione Cosa restituisce Cosa controllare per non andare in errore
s.length
numero di caratteri se e 0, la stringa e vuota
s.lastIndex
indice dell’ultimo carattere sara -1 se la stringa e vuota
s[i]
Char
per indice
i
deve soddisfare
0 <= i < length
s.indexOf(x)
indice oppure -1 -1 va gestito obbligatoriamente
s.substring(a, b)
pezzo di stringa
a
e
b
in
0..length
, e
a <= b
s.substring(a)
da
a
fino alla fine
a
deve essere in
0..length

E un altro “ponte” importante per capire: gli indici che partono da zero sono un principio generale di Kotlin per le strutture indicizzabili. Si manifesta allo stesso modo in array, liste e cicli su indices.

6. Errori tipici quando si lavora con indici e sottostringhe

Errore n. 1: confondere length e “ultimo indice”.
Un errore molto comune e pensare che, se la lunghezza della stringa e 6, allora l’ultimo indice e 6. In realta, con indicizzazione da zero, l’ultimo indice e sempre di 1 in meno. Da qui nascono gli errori “di 1”: o prendi il carattere sbagliato, o vai fuori dai limiti.

Errore n. 2: usare s[0] senza controllare se la stringa e vuota.
Una stringa vuota capita piu spesso di quanto sembri: l’utente preme Enter, arriva input vuoto, da qualche parte si concatena in "". Se poi fai text[0], il programma va in errore. L’abitudine giusta e controllare prima text.isEmpty() o almeno text.length == 0, e solo dopo entrare negli indici.

Errore n. 3: ignorare indexOf(...) == -1.
indexOf non “si lamenta”: restituisce onestamente -1. Ma se poi usi subito quel -1 come limite in substring(), inizia il caos: nel caso migliore avrai un’eccezione, nel caso peggiore taglierai la parte sbagliata e otterrai un risultato “silenziosamente errato”. Per questo la combinazione “trovato → verificato → estratto” deve diventare un riflesso.

Errore n. 4: dimenticare che substring(start, end) non include end.
E una fonte di errori particolarmente fastidiosi: il programma sembra funzionare, ma produce una risposta “quasi corretta” — di un carattere piu corta o piu lunga. La cura migliore e ripetersi sempre: “start incluso, end escluso”, e fare un piccolo schema su carta quando hai dubbi.

Errore n. 5: estrarre la parte destra senza rimuovere il separatore (substring(at) invece di substring(at + 1)).
Se dividi "user@example.com" sul @, vuoi la parte destra senza @. Quindi substring(at + 1). Se scrivi substring(at), il dominio iniziera con @, e poi la logica di “validita” e i confronti di stringhe potrebbero comportarsi in modo strano. E particolarmente sgradevole nei programmi in cui poi il dominio viene confrontato con "gmail.com".

Errore n. 6: mescolare il parsing della stringa e la formattazione del risultato in un’unica stringa enorme.
Tecnicamente puoi scrivere un unico gigantesco println("Imya: ${...} ..."), ma quasi sicuramente porterà a errori e a scarsa leggibilita. E piu affidabile ricavare prima val firstName, val lastName, val domain, e solo dopo assemblare l’output finale. Questo codice e piu facile da leggere, piu facile da testare e piu facile da sistemare quando l’utente inserira di nuovo qualcosa di “creativo”.

1
Compito
Kotlin SELF, livello 9, lezione 3
Bloccato
Passaporto della stringa
Passaporto della stringa
1
Compito
Kotlin SELF, livello 9, lezione 3
Bloccato
Carattere all'indice
Carattere all'indice
1
Compito
Kotlin SELF, livello 9, lezione 3
Bloccato
Ricerca del separatore
Ricerca del separatore
1
Compito
Kotlin SELF, livello 9, lezione 3
Bloccato
Nome e cognome
Nome e cognome
Commenti
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION