Lua/Tabelle
Le tabelle in Lua sono più flessibili delle strutture dati in molti altri linguaggi. Sono anche chiamate dizionari (perché creano valori corrispondenti agli indici, come le definizioni in un dizionario corrispondono ai termini), array associativi (perché associano valori agli indici, rendendoli così array di valori associati agli indici), tabelle hash, tabelle dei simboli, mappe hash e mappe. Vengono create utilizzando i costruttori di tabelle, definiti da due parentesi graffe che possono contenere valori separati da virgole o punti e virgola. L'esempio seguente mostra un array (un elenco ordinato di valori) e come si può ottenere la lunghezza di un array.
local array = {5, "testo", {"altro testo", 463, "ancora altro testo"}, "altro testo"}
print(#array) --> 4 ; l'operatore # per le stringhe può essere utilizzato anche per gli array, nel qual caso restituirà il numero di valori nell'array
-- Questo esempio dimostra come le tabelle possono essere annidate in altre tabelle. Poiché le tabelle stesse sono valori, possono includere altre tabelle. Gli array di tabelle sono chiamati array multidimensionali.
Le tabelle possono contenere valori di qualsiasi tipo, eccetto nil
. Il motivo è logico: nil
rappresenta l'assenza di un valore, e inserire "l'assenza di un valore" in una tabella non avrebbe senso. I valori nelle tabelle possono essere separati da virgole o punti e virgola, e possono continuare su più righe. È comune (ma non necessario) usare le virgole per i costruttori di tabelle a riga singola, e i punti e virgola per le tabelle con più righe.
local array = {
"testo";
{
"altro testo, in una tabella nidificata";
1432
};
true -- i valori booleani possono essere anche nelle tabelle
}
Le tabelle sono composte da campi, che sono coppie di valori, uno dei quali è un indice (chiamato anche chiave), l'altro è il valore che corrisponde a quell'indice. Negli array, gli indici sono sempre valori numerici. Nei dizionari, gli indici possono avere qualsiasi valore.
local dictionary = {
"testo";
testo = "altro testo";
654;
[8] = {}; -- una tabella vuota
func = function() print("le tabelle possono contenere anche funzioni!") end;
["1213"] = 1213
}
Come illustra l'esempio sopra, i valori possono essere aggiunti a un dizionario come se fossero in array con solo il valore (nel qual caso l'indice sarà un numero), con un identificatore e un segno di uguale anteposto al valore (nel qual caso l'indice sarà la stringa che corrisponde all'identificatore) o con un valore racchiuso tra parentesi e un segno di uguale anteposto al valore (nel qual caso l'indice è il valore tra parentesi). Quest'ultimo è il modo più flessibile per far corrispondere un indice a un valore perché può essere utilizzato con qualsiasi valore o espressione.
È possibile accedere a un valore in una tabella inserendo l'indice a cui corrisponde il valore tra parentesi, dopo un'espressione che restituisce la tabella:
local dictionary = {number = 6}
print(dictionary["number"]) --> 6
Se l'indice è una stringa che segue i criteri per gli identificatori Lua (non contiene spazi, non inizia con un numero e non contiene altro che numeri, lettere e caratteri di underline), è possibile accedervi senza parentesi, anteponendo un punto alla stringa:
local dictionary = {["number"] = 6}
print(dictionary.number) --> 6
I due esempi precedenti creano una tabella identica e stampano lo stesso valore, ma definiscono e accedono agli indici utilizzando notazioni diverse. È anche possibile, nel caso di tabelle che contengono altre tabelle, accedere a un valore che si trova all'interno della tabella nidificata indicizzando la prima tabella per ottenere la tabella nidificata, e poi indicizzando il valore all'interno della tabella nidificata:
local nested_table = {
[6] = {
func = function() print("6") end
}
}
nested_table[6].func() -- Questo accederà alla tabella nidificata, accederà alla funzione che si trova al suo interno e quindi chiamerà quella funzione, che stamperà il numero 6.
Ciclo foreach
modificaIl capitolo sulle istruzioni descrive due tipi di cicli: cicli controllati da condizioni e cicli controllati da un contatore. In Lua, esiste un terzo tipo di ciclo, il ciclo foreach
, che è anche chiamato ciclo for
generico (generic for loop). Un ciclo foreach
è un ciclo che consente al programmatore di eseguire codice per ogni campo in una tabella. L'esempio seguente mostra un ciclo foreach
che percorre gli elementi in un array di numeri e stampa tutti gli indici e la somma dei valori corrispondenti con il numero uno:
local array = {5, 2, 6, 3, 6}
for index, value in next, array do
print(index .. ": " .. value + 1)
end
-- Output:
-- 1: 6
-- 2: 3
-- 3: 7
-- 4: 4
-- 5: 7
I due cicli nell'esempio seguente faranno la stessa cosa del ciclo nell'esempio precedente.
local array = {5, 2, 6, 3, 6}
for index, value in pairs(array) do
print(index .. ": " .. value + 1)
end
for index, value in ipairs(array) do
print(index .. ": " .. value + 1)
end
Il metodo mostrato nel primo esempio fa la stessa cosa del primo ciclo nell'esempio precedente. Tuttavia, l'ultimo (quello che usa la funzione ipairs
) itererà solo fino alla prima chiave intera assente nella tabella, il che significa che percorrerà solo i valori numerici che sono in ordine, come in un array. C'erano due funzioni chiamate table.foreach
e table.foreachi
, ma sono state deprecate in Lua 5.1 e rimosse da Lua 5.2. La deprecazione è uno stato applicato a una funzionalità o a una pratica per indicare che è stata rimossa o sostituita e che dovrebbe essere evitata. Utilizzare il ciclo foreach
per percorrere le tabelle è quindi preferibile all'utilizzo di queste due funzioni.
Scompattare le tabelle
modificaIn Lua si distingue tra una tupla, una semplice lista di valori, e una tabella, una struttura dati che associa indici a valori. Una chiamata di funzione a una funzione che restituisce molti valori viene valutata come una tupla. Anche un elenco di valori in un'istruzione di assegnazione in cui vengono assegnate molte variabili contemporaneamente è una tupla. L'espressione vararg, utilizzata nelle funzioni variadiche, è ancora una tupla. Poiché una tupla è un elenco di valori e quindi non un singolo valore, non può essere memorizzata in una variabile, sebbene possa essere memorizzata in molte variabili. È possibile convertire una tupla in un array inserendo l'espressione che viene valutata come la tupla in un costruttore di tabelle.
function return_tuple()
return 5, 6 -- Poiché questa funzione restituisce due valori, una chiamata di funzione ad essa viene valutata come una tupla.
end
local array = {return_tuple()} -- uguale a array locale = {5, 6}
local a, b = return_tuple() -- uguale ad a, b locali = 5, 6
print(return_tuple()) -- uguale a print(5, 6)
È possibile decomprimere un array (trasformarlo da tabella a tupla) utilizzando la funzione unpack
della libreria delle tabelle con l'array come argomento:
local array = {7, 4, 2}
local a, b, c = table.unpack(array)
print(a, b, c) --> 7, 4, 2
Nelle versioni di Lua precedenti alla 5.2, la funzione unpack
faceva parte della libreria di base. Da allora è stata spostata nella libreria table.
Metodi
modificaPoiché le tabelle possono contenere funzioni e possono associare un nome a queste funzioni, spesso vengono utilizzate per creare librerie. Lua ha anche una forma compatta che può essere utilizzata per creare metodi, funzioni utilizzate per manipolare un oggetto, che generalmente sarà rappresentato dalla tabella. Questo potrebbe essere un po' difficile da capire per chi non conosce la programmazione orientata agli oggetti. I due esempi qui sotto fanno esattamente la stessa cosa:
local object = {}
function object.func(self, arg1, arg2)
print(arg1, arg2)
end
object.func(object, 1, 2) --> 1, 2
local object = {}
function object:func(arg1, arg2)
print(arg1, arg2)
end
object:func(1, 2) --> 1, 2
Quando si chiama una funzione che si trova in una tabella e che corrisponde a un indice che è una stringa, usare i due punti invece di un punto aggiungerà un argomento nascosto che è la tabella stessa. Allo stesso modo, definire una funzione in una tabella usando i due punti invece di un punto aggiungerà un parametro nascosto self nell'elenco dei parametri. L'uso dei due punti per definire la funzione non significa che i due punti debbano essere usati anche per chiamare la funzione e viceversa, perché sono completamente intercambiabili.
Ordinamento
modificaL'ordinamento delle tabelle in Lua può essere relativamente banale. Nella maggior parte dei casi, la funzione sort
della libreria table può essere utilizzata per ordinarle, il che rende tutto relativamente semplice. La funzione sort
ordina gli elementi di un array in un ordine dato, senza creare un nuovo array. Se una funzione viene fornita come secondo argomento, dovrebbe ricevere due elementi dell'array e restituire true
quando il primo elemento dovrebbe precedere il secondo nell'ordine finale. Se non viene fornito alcun secondo argomento, Lua ordinerà gli elementi nell'array in base all'operatore <
, che restituisce true
quando utilizzato con due numeri e quando il primo numero è minore del secondo, ma che funziona anche con le stringhe, nel qual caso restituisce true
quando la prima stringa è lessicograficamente minore della seconda.
Metatabelle
modificaLe metatabelle (metatables) sono tabelle che possono essere utilizzate per controllare il comportamento di altre tabelle. Ciò avviene tramite metametodi (metamethods), campi in quella tabella che indicano alla macchina virtuale Lua cosa deve essere fatto quando il codice tenta di manipolare la tabella in modi specifici. I metametodi sono definiti dal loro nome. Il metametodo __index
, ad esempio, dice a Lua cosa fare quando il codice tenta di indicizzare in una tabella un campo che non esiste ancora. La metatabella di una tabella può essere impostata utilizzando la funzione setmetatable
, che accetta due tabelle come argomenti: la prima tabella è la tabella di cui la metatabella deve essere impostata e la seconda è la metatabella su cui deve essere impostata la metatabella della tabella. Esiste anche una funzione getmetatable
, che restituisce la metatabella di una tabella. Il seguente codice mostra come una metatabella potrebbe essere utilizzata per far apparire tutti i campi inesistenti in una tabella come se contenessero un numero; questi numeri vengono generati casualmente, utilizzando la funzione math.random
:
local associative_array = {
defined_field = "questo campo è definito"
}
setmetatable(associative_array, {
__index = function(self, index)
return math.random(10)
end
})
Ci sono molte cose che si possono notare nell'esempio sopra. Anzitutto il nome del campo contenente il metametodo __index
è preceduto da due caratteri di sottolineatura. È sempre così: quando Lua cerca metametodi nella metatabella di una tabella, cerca indici che corrispondono al nome di un metametodo e che iniziano con due caratteri di underscore. Un'altra cosa che si può notare è che il metametodo __index
è in effetti una funzione (la maggior parte dei metametodi, ma non tutti, sono funzioni), che accetta due argomenti. Il primo argomento, self, è la tabella di cui il metametodo __index
è stato invocato. Nel nostro caso, potremmo semplicemente fare riferimento direttamente alla variabile associative_array, ma questo è utile quando una singola metatabella viene utilizzata per molte tabelle. Il secondo argomento è l'indice che si è tentato di indicizzare. Infine, si può notare che la funzione dice alla macchina virtuale Lua cosa dovrebbe essere dato al codice che ha indicizzato la tabella restituendo un valore. La maggior parte dei metametodi può essere solo una funzione, ma il metametodo __index
può anche essere una tabella. Se è una tabella, Lua, quando un programma tenta di indicizzare un campo della tabella che non esiste, cercherà in quella tabella lo stesso indice. Se lo trova, restituirà il valore corrispondente a quell'indice nella tabella specificata nel metametodo __index
.
Metametodo | Descrizione |
---|---|
__newindex(self, index, value)
|
Il metametodo __newindex può essere utilizzato per dire alla macchina virtuale Lua cosa fare quando un programma tenta di aggiungere un nuovo campo in una tabella. Può essere solo una funzione e verrà chiamata solo se non esiste già un campo con l'indice su cui il programma tenta di scrivere. Ha tre argomenti: i primi due sono gli stessi argomenti del metametodo __index e il terzo è il valore a cui il programma ha tentato di impostare il valore del campo.
|
__concat(self, value)
|
Il metametodo __concat può essere utilizzato per indicare alla macchina virtuale Lua cosa fare quando un programma tenta di concatenare un valore alla tabella, utilizzando l'operatore di concatenazione (.. ).
|
__call(self, ...)
|
Il metametodo __call può essere utilizzato per specificare cosa dovrebbe accadere quando un programma tenta di chiamare la tabella. Ciò rende possibile far sì che una tabella si comporti come se fosse una funzione. Gli argomenti passati quando la tabella è stata chiamata vengono forniti dopo l'argomento self, che è la tabella stessa, come sempre. Questo metametodo può essere utilizzato per restituire valori, nel qual caso la chiamata alla tabella restituirà tali valori.
|
__unm(self)
|
Questo metametodo può essere utilizzato per specificare l'effetto dell'utilizzo dell'operatore unario - sulla tabella.
|
__tostring(self)
|
Questo metametodo può essere una funzione che restituisce il valore che la funzione tostring dovrebbe restituire quando viene chiamata con la tabella come argomento.
|
__add(self, value)
|
Questi metametodi possono essere usati per dire alla macchina virtuale cosa fare rispettivamente quando un valore viene aggiunto, sottratto, moltiplicato o diviso per la tabella. Gli ultimi due sono simili, ma sono rispettivamente per l'operatore modulo (% ) e l'operatore esponenziale (^ ).
|
__sub(self, value)
| |
__mul(self, value)
| |
__div(self, value)
| |
__mod(self, value)
| |
__pow(self, value)
| |
__eq(self, value)
|
Questo metametodo è utilizzato dall'operatore di uguaglianza (== ). Sarà utilizzato solo se i due valori confrontati hanno la stessa metatabella. L'operatore di non uguaglianza (~= ) utilizza l'opposto del risultato di questa funzione.
|
__lt(self, value)
|
Questi metametodi sono utilizzati dagli operatori "minore di" e "minore o uguale a". Gli operatori "maggiore di" e "maggiore o uguale a" restituiranno l'opposto di ciò che restituiscono questi metametodi. Saranno utilizzati solo se i valori hanno la stessa metatabella. |
__le(self, value)
| |
__gc(self)
|
Questo metametodo viene chiamato da Lua prima che il garbage collector raccolga un valore a cui è associata una metatabella con questo metametodo. Funziona solo con valori userdata e non può essere utilizzato con le tabelle. |
__len(self)
|
Questo metametodo viene chiamato da Lua quando l'operatore di lunghezza (# ) è utilizzato su un valore userdata. Non può essere utilizzato con le tabelle.
|
__metatable
|
Se questo metametodo è presente (può essere qualsiasi cosa), usare getmetatable sulla tabella restituirà il valore di questo metametodo anziché quello della metatabella, e non sarà consentito modificare la metatabella della tabella con la funzione setmetatable .
|
__mode
|
Questo metametodo dovrebbe essere una stringa che può contenere la lettera k o v (o entrambe). Se è presente la lettera k, le chiavi della tabella saranno deboli. Se è presente la lettera v, i valori della tabella saranno deboli. Cosa significa questo verrà spiegato più avanti, insieme alla garbage collection. |
Le metatabelle, sebbene i normali programmi Lua possano usarle solo con le tabelle, sono in effetti il meccanismo al centro del modo in cui Lua gestisce operatori e operazioni, e possono in effetti essere usate teoricamente con qualsiasi valore. Tuttavia, Lua consente di usarle solo con tabelle e valori userdata creati con la funzione non documentata newproxy
. Utilizzando l'API C di Lua o la libreria di debug, è possibile impostare la metatabella di valori di altri tipi, come numeri e stringhe.
Talvolta è preferibile eseguire un'operazione su una tabella senza invocare metametodi. Ciò è possibile per l'indicizzazione, l'aggiunta di campi in una tabella, il controllo dell'uguaglianza e l'ottenimento della lunghezza di una tabella utilizzando rispettivamente le funzioni rawget
, rawset
, rawequal
e rawlen
. La prima restituisce il valore corrispondente all'indice passato come secondo argomento nella tabella fornita come primo argomento. La seconda assegna al valore fornito come terzo argomento il valore corrispondente all'indice dato come secondo argomento nella tabella fornita come primo argomento. La terza restituisce se i due valori passati sono uguali. Infine, la quarta restituisce la lunghezza (un numero intero) dell'oggetto che le viene passato, il quale deve essere una tabella o una stringa.
Iteratori
modificaUn iteratore è una funzione utilizzata congiuntamente a un ciclo foreach
. Nella maggior parte dei casi, un iteratore viene utilizzato per eseguire un ciclo o percorrere una struttura dati. Esempi di ciò sono gli iteratori restituiti dalle funzioni pairs
e ipairs
che vengono utilizzati, rispettivamente, per percorrere gli elementi di una tabella o di un array. La funzione pairs
, ad esempio, restituisce la funzione next
, insieme alla tabella che viene fornita come argomento, il che spiega perché in pairs(dictionary)
equivale a in next, dictionary
, poiché la prima in realtà valuta la seconda.
Non c'è alcun requisito per cui gli iteratori debbano sempre lavorare con strutture dati, poiché gli iteratori possono essere progettati per qualsiasi caso in cui sia richiesto il looping. Ad esempio, la funzione file:lines
restituisce un iteratore che restituisce una riga da un file a ogni iterazione. Analogamente, la funzione string.gmatch
restituisce un iteratore che restituisce una corrispondenza di un pattern in una stringa a ogni iterazione. Questo codice, ad esempio, stamperebbe tutte le righe in un file chiamato file.txt.
for line in io.open("file.txt"):lines() do
print(line)
end
Creare gli iteratori
modificaUn iteratore è costituito da tre cose:
- una funzione di trasformazione,
- un valore di stato,
- una o più variabili di ciclo.
La funzione di trasformazione viene utilizzata per modificare i valori delle variabili del ciclo (le variabili che compaiono tra for
e in
) per ogni iterazione del ciclo. Questa funzione viene richiamata prima di ogni iterazione e accetta, come argomenti, i valori a cui sono state impostate le variabili del ciclo durante l'ultima iterazione. Si prevede che la funzione restituisca una tupla (uno o più valori) contenente i nuovi valori per queste variabili. Le variabili del ciclo saranno impostate sui componenti della tupla restituita e il ciclo passerà attraverso un'iterazione. Una volta completata tale iterazione (purché non sia stata interrotta da un'istruzione break
o return
), la funzione di trasformazione verrà richiamata di nuovo e restituirà un altro set di valori, a cui saranno impostate le variabili del ciclo per l'iterazione successiva e così via. Questo ciclo di chiamata della funzione di trasformazione e iterazione sulle istruzioni del ciclo continuerà finché la funzione di trasformazione non restituirà nil
.
Insieme alle variabili del ciclo, la funzione di trasformazione viene anche passata in un valore di stato che rimarrà costante per tutto il ciclo. Il valore di stato può essere utilizzato, ad esempio, per mantenere un riferimento alla struttura dati, al file handle o alla risorsa su cui la funzione di trasformazione sta iterando.
Di seguito è riportato un esempio di una funzione di trasformazione che produrrà una sequenza di numeri fino a 10. Questa funzione di trasformazione richiede solo una variabile di ciclo il cui valore verrà memorizzato in value.
function seq(state, value)
if (value >= 10) then
return nil -- Se restituisce nil terminerà il ciclo
else
local new_value = value + 1 -- Il valore successivo da utilizzare nel ciclo è il valore corrente di `value` più 1.
-- Questo valore verrà utilizzato come valore di `value` la prossima volta che questa funzione verrà chiamata.
return new_value
end
end
Il ciclo for
generico si aspetta una tupla contenente la funzione di trasformazione, il valore di stato e i valori iniziali delle variabili del ciclo. Questa tupla può essere inclusa direttamente dopo la parola chiave in
.
-- Questo visualizzerà i numeri da 1 a 10.
for value in seq, nil, 0 do
print(value)
end
Nella maggior parte dei casi, tuttavia, questa tupla verrà restituita da una funzione. Ciò consente l'uso di iterator factory che, quando chiamate, restituiscono un nuovo iteratore che può essere utilizzato con un ciclo for
generico:
function count_to_ten()
local function seq(state, value)
if (value >= 10) then
return nil
else
local new_value = value + 1
return new_value
end
end
return seq, nil, 0
end
for value in count_to_ten() do
print(value)
end
Poiché Lua supporta chiusure e funzioni come oggetti di prima classe, la iterator factory' può anche accettare argomenti che possono essere utilizzati all'interno della funzione di trasformazione.
function count_to(limit)
local function seq(state, value)
if (value >= limit) then
return nil
else
local new_value = value + 1
return new_value
end
end
return seq, nil, 0
end
for value in count_to(10) do -- Stampa i numeri da 1 a 10
print(value)
end
for value in count_to(20) do -- Stampa i numeri da 1 a 20
print(value)
end