singe/docs/it/lessons/08-when-it-goes-wrong.adoc

512 lines
21 KiB
Text

[#_lesson_8_when_it_goes_wrong]
== Lezione 8: Quando qualcosa va storto
image::learn/08-when-it-goes-wrong.png[La lezione finita, 480]
Hai scritto un gioco. Da qui in poi passerai più tempo ad aggiustare programmi
che a scriverli, e non è segno che ci sia qualcosa che non va in te. È il
mestiere. La differenza tra chi si diverte a programmare e chi ci rinuncia non è
quasi mai il talento; è se ha imparato a leggere quello che il computer gli sta
dicendo.
Quindi in questa lezione non c'è niente di nuovo da costruire. Ci sono i cinque
messaggi che vedrai più spesso per il resto della tua vita, che cosa vuol dire
davvero ciascuno, e quattro modi per stanare l'errore che c'è dietro. Tieni
aperto `dodge.singe` della settima lezione, perché lo romperai apposta parecchie
volte.
[#_two_kinds_of_stop]
=== Due tipi di arresto
Singe si ferma per due motivi diversi, e lo dice in due modi diversi.
Quando lo script non parte nemmeno, ottieni questa forma:
----
Error running script: dodge/dodge.singe:92: 'then' expected near 'spawnBlock'
----
Lua legge tutto il tuo file prima di eseguirne una sola parola, e questo vuol
dire che non è riuscito a dare un senso a ciò che ha letto. Non è stato eseguito
niente.
Quando lo script è partito bene ma qualcosa è andato storto dopo, ottieni invece
questa forma:
----
Error executing function 'onOverlayUpdate': dodge/dodge.singe:98: attempt to perform arithmetic on a nil value (global 'blockSped')
stack traceback:
dodge/dodge.singe:98: in function 'updatePlaying'
dodge/dodge.singe:136: in function 'onOverlayUpdate'
----
Il gioco stava girando. Singe ha chiamato la tua `onOverlayUpdate`, come fa
sessanta volte al secondo, e questa volta la chiamata non è tornata.
Entrambe le forme contengono le stesse tre cose, e sono proprio le tre cose che
vuoi: un file, una riga e una lamentela. Leggile in quest'ordine, ogni volta.
I numeri di riga di questa lezione vengono dal listato finito della settima
lezione. Se i tuoi sono spostati di una riga o due, lo è anche il tuo file, e
niente nel leggere un errore dipende dal fatto che il numero corrisponda.
[#_the_ones_that_stop_it_before_it_starts]
=== Quelli che lo fermano prima che parta
Questi sono quelli facili, e sono anche quelli che fanno sentire peggio, perché
lo schermo resta nero e non c'è niente da guardare.
Vai in `updatePlaying` e cancella la parola `then` dalla riga
`if spawnTimer <= 0 then`. Salva:
----
Error running script: dodge/dodge.singe:92: 'then' expected near 'spawnBlock'
----
Lua è arrivato alla fine della riga con `if`, voleva la parola `then` e invece
ha trovato l'inizio della riga successiva -- che è `spawnBlock()`, ed è quella
la parte `near 'spawnBlock'`. «Near» vuol dire «è qui che me ne sono accorto», e
di solito è la prima cosa *dopo* quello che hai dimenticato.
Ora una più cattiva. Rimetti `then`, e cancella invece la parola `end` sull'ultima riga
di `spawnBlock`. Salva:
----
Error running script: dodge/dodge.singe:148: 'end' expected (to close 'function' at line 58) near <eof>
----
La riga 148 è la fine del file, appena dopo la sua ultima riga. Non c'è niente
di sbagliato nella riga 148. Lua ha letto fino in fondo -- `<eof>` è il modo in cui chiama la fine di un
file -- sempre aspettando un `end` che non è mai arrivato, e a quel punto aveva
inghiottito ogni funzione dopo `spawnBlock` come se ne facesse parte.
La metà utile di quel messaggio è `(to close 'function' at line 58)`, che è dove
comincia `spawnBlock`. Quando un messaggio punta alla fine del file, credi a
quello che dice tra parentesi, non al numero di riga. È anche per questo che il
rientro conta così tanto: un `end` mancante è quasi invisibile in un elenco di
righe, ed evidente nel momento in cui tutto ciò che sta sotto è rientrato di un
passo di troppo.
Altri due che vale la pena saper riconoscere, perché li incontrerai entrambi
questa settimana:
----
Error running script: dodge/dodge.singe:41: unfinished string near '"GAME OVER)'
----
Una virgoletta che hai aperto e mai chiuso, che hai incontrato nella prima
lezione.
----
Error running script: dodge/dodge.singe:135: 'then' expected near '='
----
Questo è `if state = "playing" then`. Un `=` imposta una variabile, due `==`
fanno una domanda, e Lua non ti lascia impostare una variabile dentro un `if`.
Sembra una lamentela strana, e vuol dire sempre la stessa cosa.
[#_attempt_to_index_a_nil_value]
=== Attempt to index a nil value
Questo è quello che vedrai più spesso, e di gran lunga.
Vai in `spawnBlock` e scrivi male `block` nella prima riga che lo usa, così che
diventi `blocl.x = math.random(0, screenWidth - BLOCK_SIZE)`. Salva, premi la
barra spaziatrice:
----
Error executing function 'onOverlayUpdate': dodge/dodge.singe:60: attempt to index a nil value (global 'blocl')
stack traceback:
dodge/dodge.singe:60: in function 'spawnBlock'
dodge/dodge.singe:92: in function 'updatePlaying'
dodge/dodge.singe:136: in function 'onOverlayUpdate'
----
*Indicizzare* è ciò che fa il punto. `blocl.x` vuol dire «entra nella cosa
chiamata `blocl` e trovaci la `x`». Puoi farlo solo con una tabella.
*Nil* è il nome che Lua dà a un nome senza niente dietro. Non l'hai visto molto,
perché sei stato attento a dare un valore a tutto, ma ogni nome che non hai mai
usato è già `nil`, e scrivere male un nome ne inventa uno nuovo di zecca.
Quindi il messaggio intero dice: *hai usato un punto su qualcosa che non
esiste*.
La parte tra parentesi è il regalo. `global 'blocl'` è il nome esatto della cosa
che era vuota. Cercalo nel tuo file. Se compare una volta sola, è un errore di
battitura, e l'hai trovato in dieci secondi.
La parola prima del nome ti dice dove l'ha cercato Lua:
* `global` -- un nome senza un `local` davanti, da nessuna parte.
* `local` -- un nome che hai dichiarato con `local` nella funzione in cui ti
trovi.
* `field` -- un nome dopo un punto, come la `y` in `block.y`.
* `upvalue` -- una variabile `local` di un livello più esterno che questa
funzione riesce a vedere, ed è ciò che sono tutte le variabili di
`dodge.singe`, visto che sono dichiarate in cima al file e usate dentro le
funzioni.
`field` è l'altra causa comune, e non è affatto un errore di battitura. Se hai
scritto `block.hit.when` e non hai mai impostato `block.hit`, ottieni
`attempt to index a nil value (field 'hit')`. Il punto che fallisce è il
secondo. Lua ti dice quale, nominandolo.
[#_attempt_to_call_a_nil_value]
=== Attempt to call a nil value
Rimetti a posto `blocl`. Ora scrivi male una funzione del motore: in `drawGame`,
cambia `overlayPrint` in `overlayPirnt`. Salva:
----
Error executing function 'onOverlayUpdate': dodge/dodge.singe:36: attempt to call a nil value (global 'overlayPirnt')
stack traceback:
dodge/dodge.singe:36: in function 'drawGame'
dodge/dodge.singe:140: in function 'onOverlayUpdate'
----
Stesso `nil`, crimine diverso. *Chiamare* è ciò che fanno le parentesi, e puoi
farlo solo con una funzione. Non esiste nessuna funzione chiamata
`overlayPirnt`, quindi `overlayPirnt` è `nil`, quindi le parentesi non hanno
niente da chiamare.
Succede per quattro motivi, in ordine di frequenza:
. Hai scritto male una delle tue funzioni.
. Hai scritto male una di quelle di Singe. Niente nel motore controlla come
scrivi quando il file viene caricato, quindi un nome del motore scritto male è
solo un altro nome che si trova a essere vuoto, e non ti costa niente finché la
riga non viene eseguita.
. La funzione non esiste proprio in Singe -- era un'ipotesi ragionevole, e le
ipotesi ragionevoli sono sbagliate più o meno una volta su due. L'elenco delle
funzioni del Riferimento è l'unico modo per esserne sicuro.
. L'hai resa `local` e l'hai chiamata da un punto che sta sopra quello in cui è
dichiarata. È esattamente per questo che le funzioni di `dodge.singe` sono
globali.
C'è una versione molto peggiore di questo errore, e l'hai incontrata nella prima
lezione senza saperlo: scrivi male il nome di una *callback* e non ottieni
nessun messaggio. `onOverlayUpdated` non è un errore. È una funzione
perfettamente valida di cui Singe non ha mai sentito parlare e che non chiamerà
mai, quindi lo schermo resta nero e niente si lamenta. Quando un'intera
funzionalità non fa assolutamente niente, sospetta del nome.
[#_attempt_to_perform_arithmetic_on_a_nil_value]
=== Attempt to perform arithmetic on a nil value
Rimetti a posto `overlayPrint`. In `updatePlaying`, scrivi male `blockSpeed`
nella riga della caduta, così che diventi `block.y = block.y + blockSped`:
----
Error executing function 'onOverlayUpdate': dodge/dodge.singe:98: attempt to perform arithmetic on a nil value (global 'blockSped')
stack traceback:
dodge/dodge.singe:98: in function 'updatePlaying'
dodge/dodge.singe:136: in function 'onOverlayUpdate'
----
L'aritmetica è `+`, `-`, `*` e `/`. Non puoi sommare il niente a un numero. Di
nuovo le parentesi nominano la cosa vuota, e di nuovo la causa più comune è un
errore di battitura -- ma questo ha una seconda causa che vale la pena
conoscere, perché è più difficile da vedere.
Una variabile a cui non hai mai dato un numero è `nil`, non zero. Se aggiungi al
gioco un punteggio record e scrivi `local hiScore` in cima senza `= 0`, contiene
`nil` finché qualcosa non la imposta, e il primo `hiScore + score` ferma il
gioco. Lua non fa partire i numeri da zero per te. Sei tu a farli partire da
zero.
Due parenti stretti, con la stessa causa e la stessa cura:
----
dodge/dodge.singe:37: attempt to concatenate a nil value (upvalue 'hiScore')
----
`..` che attacca una stringa a qualcosa che non c'è. Questo lo vedrai ogni volta
che stampi un punteggio che hai dimenticato di impostare -- qui, `hiScore`
aggiunto alla riga del punteggio in `drawGame`, che ora è la riga 37 visto che
sopra c'è `local hiScore`.
----
dodge/dodge.singe:105: attempt to compare nil with number
----
È `if score > hiScore then`, messo sotto `state = "over"` per tenere il
punteggio migliore quando una partita finisce, senza niente in `hiScore`. Nota
che questo messaggio non
nomina niente tra parentesi. Quando Lua non sa dirti quale lato era vuoto, la
sezione dopo la prossima ti spiega come scoprirlo.
[#_an_argument_of_the_wrong_kind]
=== Un argomento del tipo sbagliato
Finora tutti i messaggi venivano da Lua. Questo viene da Singe, e ha un aspetto
completamente diverso, quindi vale la pena incontrarlo apposta.
Supponi di voler vedere che cosa c'è in `blocks`, e di provare a stamparlo.
Aggiungi questa riga a `drawGame`, sotto il punteggio:
[source,lua]
----
overlayPrint(1, 3, blocks)
----
----
37:overlayPrint: Argument 3 must be a string.
----
Niente nome del file, niente traceback, nessun `attempt to` qualcosa. Una
lamentela del motore comincia con il numero di riga, poi il nome della funzione
che si è rifiutata, poi che cosa voleva. Qui a `overlayPrint` è stata passata
una tabella dove serve del testo, e si è fermata invece di disegnare qualcosa
senza senso.
Il messaggio fratello conta invece di controllare:
----
34:overlayBox: Expected 4 argument(s), got 3.
----
È un `overlayBox` a cui manca un angolo. Lo strano `argument(s)` è del motore,
non un tuo errore di battitura.
Quando ottieni uno di questi, la risposta è la voce di quella funzione nel
Riferimento, e ti ci vorrà un minuto. Ogni voce elenca gli argomenti in ordine,
con che cosa deve essere ciascuno. Singe li controlla perché l'alternativa è un
gioco che gira per dieci minuti e poi disegna spazzatura.
[#_finding_out_what_is_really_in_there]
=== Scoprire che cosa c'è davvero lì dentro
Metà del debug non è affatto leggere errori. È il caso in cui niente va in crash
e niente funziona: blocchi che non compaiono mai, un giocatore che non si muove,
un punteggio fermo a zero. Per questo devi vedere dentro il gioco mentre gira, e
ci sono due modi.
Il primo è `debugPrint`, che scrive una riga nel terminale da cui hai avviato
Singe. Prendi la riga che non riesci a capire e mettine una sopra:
[source,lua]
----
debugPrint("blocks " .. #blocks .. ", timer " .. spawnTimer)
----
Ora stai guardando quello che c'è davvero invece di quello che sei sicuro che ci
sia, ed è in quella distanza che vivono i bug. Stampa `playerX` allo stesso modo e
vedrai la verità anche su di lui: viene stampato come `160.0`, non `160`, perché in Lua dividere con
`/` dà sempre un numero con una parte decimale, anche quando la parte decimale è
zero.
Due cose ti morderanno. La prima è che `debugPrint` vuole per forza del testo o
un numero; dagli qualsiasi altra cosa e ferma il gioco come ha fatto
`overlayPrint`. `debugPrint(blocks)` non ti mostrerà un elenco, e
`debugPrint("left " .. goingLeft)` in cima a `updatePlaying` non ti mostrerà
nemmeno un `true` o un `false`:
----
dodge/dodge.singe:78: attempt to concatenate a boolean value (upvalue 'goingLeft')
----
La soluzione è `tostring`. Prende qualsiasi cosa e te ne dà una versione in
testo, quindi `tostring(goingLeft)` è `"true"` o `"false"` e la riga funziona.
La seconda è che `onOverlayUpdate` gira sessanta volte al secondo, quindi un
`debugPrint` lì dentro stampa sessanta righe al secondo e non riesci a leggerne
nessuna. Lo dice anche il Riferimento, nella sua voce: la chiamata costa poco, ma
niente ne limita la frequenza.
Prima di tutto, metti un interruttore davanti a ogni riga di debug che aggiungi,
così puoi spegnere il rumore senza doverle andare a cercare di nuovo:
[source,lua]
----
local DEBUG = true
function debugLog(text)
if DEBUG then
debugPrint(text)
end
end
----
Ogni `debugLog` del gioco obbedisce a quell'unico `true`. Cambialo in `false` e
il gioco tace; rimettilo com'era e torna tutto.
Poi stampa quando qualcosa *succede*, invece che a ogni fotogramma:
[source,lua]
----
debugLog("hit at y " .. math.floor(block.y) .. ", lives now " .. lives)
----
Oppure stampa a tempo, con un conto alla rovescia fatto esattamente come quello
che fa comparire i blocchi:
[source,lua]
----
reportTimer = reportTimer - 1
if reportTimer <= 0 then
debugReport()
reportTimer = REPORT_FRAMES
end
----
La cartella `learn` ha tutto quanto in `08-when-it-goes-wrong.singe`: è il
gioco della settima lezione con un resoconto circa ogni due secondi, una riga
quando vieni colpito e una riga quando la partita finisce.
Il secondo modo di vedere dentro è mettere le cose sullo schermo, che è meglio
per tutto ciò che cambia a ogni fotogramma, perché sessanta righe al secondo in
un terminale non servono a niente, ma un numero che scatta nell'angolo è facile
da tenere d'occhio:
[source,lua]
----
if DEBUG then
overlayPrint(1, 3, "BLOCKS " .. #blocks .. " PLAYER " .. playerX)
end
----
[#_cutting_the_problem_in_half]
=== Tagliare il problema a metà
Quando non riesci a vedere quale di venti righe è sbagliata, smetti di guardare
e comincia a togliere.
Metti due trattini all'inizio di una riga e Lua ignora tutto il resto. Quello è
un *commento*, e di solito serve a lasciare appunti per te stesso, ma l'uso
migliore è questo:
[source,lua]
----
-- overlayPrint(1, 1, "SCORE " .. score .. " LIVES " .. lives)
----
Commenta metà di `drawGame` ed eseguilo. Se il problema c'è ancora, sta nella
metà che hai tenuto. Se è sparito, sta nella metà che hai tolto. Rimetti quella
metà e taglia a metà *lei*. Venti righe richiedono cinque giri, e ogni giro è un
salvataggio e un'occhiata.
Funziona anche su cose più grandi delle righe. Commenta il corpo di
`updatePlaying` e i blocchi smettono di muoversi, il che ti dice se la cosa che
stai inseguendo sta nel movimento o nel disegno. Commenta il ramo di
`collideRects` e guarda se il gioco smette di andare in crash. Mentre fai questo
non stai cercando di aggiustare niente. Stai cercando di scoprire dove non è.
[#_going_round_faster]
=== Fare il giro più in fretta
Tutto questo dipende dal poter provare qualcosa in un paio di secondi, ed è a
questo che serve `-R`. Lo usi dalla prima lezione; ecco che cosa fa davvero, e
vale la pena saperlo ora che stai per appoggiarti a lui.
Singe tiene d'occhio ogni file di script che il tuo gioco ha caricato. Quando ne
salvi uno, il gioco riparte dall'inizio: tutto ciò che lo script aveva creato
viene buttato via e il tuo file viene eseguito da capo, nel tempo che serve a
leggere il file invece che in quello che serve ad avviare il motore. `F5` fa la
stessa cosa a comando, senza salvare niente.
Un tasto che tieni premuto durante un ricaricamento viene ignorato finché non lo
lasci, così il giocatore non scappa via da solo dopo un ricaricamento.
I due tipi di arresto qui si comportano in modo diverso, ed è utile sapere quale
stai guardando senza leggere il messaggio. Uno script che non si compila *viene
stampato, e Singe sopravvive*: la finestra resta aperta e vuota, il file resta
sotto osservazione, e salvare la correzione riporta indietro il gioco. Un crash
dentro una callback -- tutti i messaggi `attempt to` e le lamentele del motore
sugli argomenti -- butta giù Singe, e lo riavvii a mano.
[#_reading_a_traceback]
=== Leggere un traceback
Le righe sotto `stack traceback:` sono l'elenco delle funzioni che erano in corso
quando tutto si è fermato, a partire dalla più interna. Ecco di nuovo quello di
`blocl`:
----
dodge/dodge.singe:60: in function 'spawnBlock'
dodge/dodge.singe:92: in function 'updatePlaying'
dodge/dodge.singe:136: in function 'onOverlayUpdate'
----
Leggilo dal basso verso l'alto ed è una frase. Singe ha chiamato
`onOverlayUpdate`. Alla riga 136, `onOverlayUpdate` ha chiamato `updatePlaying`.
Alla riga 92, `updatePlaying` ha chiamato `spawnBlock`. Alla riga 60,
`spawnBlock` si è arresa.
La riga in cima è dove si è rotto. Le righe sotto sono come ci è arrivato, e
sono ciò che ti serve quando la riga in cima sembra innocente. Una funzione che
somma un numero a `block.y` non è sbagliata; una funzione che ha messo un blocco
nell'elenco senza una `y` lo è. Solo la pista collega le due cose, e la pista ti
viene stampata ogni singola volta.
[#_the_habit]
=== L'abitudine
Tutto ciò che c'è qui sopra è tecnica. Questa è l'abitudine, e conta di più:
*Cambia una cosa. Eseguilo. Guarda che cosa è successo.*
Quando qualcosa non funziona, la tentazione è cambiare quattro cose insieme,
perché una di loro sarà per forza quella giusta. Fallo e smette di funzionare in
un modo nuovo, e ora non sai quale delle quattro è stata, o se due di loro si
stanno annullando a vicenda. Hai reso il problema più grande e l'hai nascosto
meglio.
Una modifica. Esegui. Guarda. Sembra più lento. È la cosa più veloce che ci sia,
e ogni programmatore esperto che incontrerai lo fa, esattamente per il motivo
che stai per scoprire.
[#_try_it_8]
=== Prova tu
. *Conoscili tutti.* Fai apposta in `dodge.singe` ciascuno dei cinque errori di
questa lezione, uno alla volta, e leggi il messaggio prima di correggerlo. Non
ne avrai mai più paura.
. *Osserva chi crea i blocchi.* Metti dentro `spawnBlock` un `debugPrint` che
stampi la `x` del nuovo blocco. Gioca per trenta secondi, poi guarda i numeri.
Sono sparsi su tutto lo schermo, o preferiscono un lato?
. *Spegnilo.* Cambia `DEBUG` in `false` nello script della lezione, e assicurati
che non venga stampato niente e che il gioco non cambi.
. *Un bug senza errore.* In `updatePlaying`, cambia `if spawnTimer <= 0 then`
in `if spawnTimer == 0 then`, che sembra voler dire la stessa cosa. Eseguilo e
comincia una partita. Non c'è nessun errore, niente va in crash, e non cade
mai nemmeno un blocco, quindi non puoi neanche perdere. Metti un `debugLog`
dentro `spawnBlock`, aggiungi `spawnTimer` alla prima riga che stampa
`debugReport` e gioca di nuovo. Scopri da quello che dicono, e da quello che
uno dei due non dice mai, perché `==` era la domanda sbagliata da fare.
. *Rompi il nome di una callback.* Rinomina `onInputPressed` in `onInputPress`
ed eseguilo. Non viene stampato nessun errore, niente va in crash, il giocatore non si
muove e la barra spaziatrice non fa partire una partita. Poi spiega a te
stesso perché questo è l'errore più pericoloso di questa lezione.
[#_what_you_learned_8]
=== Cosa hai imparato
* Gli errori hanno un file, una riga e una lamentela. Leggili in quest'ordine.
* `Error running script` vuol dire che non è stato eseguito niente; la forma con
un traceback vuol dire che stava girando e si è fermato.
* `nil` è un nome senza niente dietro, e sta dietro alla maggior parte dei
messaggi di errore che vedrai.
* La parola tra parentesi nomina la cosa vuota, e dice se era una globale, una
locale, un campo o un upvalue.
* Un `end` mancante viene segnalato alla fine del file, e le parentesi ti dicono
dov'è davvero lo sbaglio.
* Le lamentele del motore hanno un aspetto diverso: un numero di riga, il nome di
una funzione e che cosa voleva invece.
* `debugPrint` ti mostra che cosa contiene davvero una variabile. `tostring`
rende stampabile qualsiasi cosa, e un interruttore come `DEBUG` spegne il
rumore.
* Commenta una metà con `--` per scoprire in quale metà sta il problema.
* Una callback scritta male non produce nessun errore, il che la rende peggiore
di una che lo produce.
* Cambia una cosa, eseguilo, guarda.
[#_next_time_8]
=== La prossima volta
Questa è la fine della prima parte. Sai scrivere un gioco, e ora sai anche
aggiustarne uno, il che vuol dire che tutto ciò che viene da qui in poi è
aggiunta, non fondamenta. La seconda parte comincia sostituendo quei riquadri
colorati con delle vere immagini, e da lì il gioco che stai costruendo comincia
ad assomigliare a un gioco a cui qualcun altro vorrebbe giocare.