Retour au cours

games / lua

Tables : tableaux, dictionnaires et itération

Leçon 31 exercice

Explication

La table : la seule structure de données de Lua, et c'est voulu

Lua fait un choix radical : il n'a qu'UN SEUL type de structure de données composite, la table. Pas de tableau séparé, pas de dictionnaire séparé, pas de liste séparée — tout est une table, qui peut se comporter comme un tableau indexé par des nombres, comme un dictionnaire indexé par des chaînes, ou comme les deux à la fois. Cette simplicité radicale est à la fois la grande force et la grande subtilité du langage.

Ce que vous allez apprendre

  • Utiliser une table comme tableau indexé (à partir de 1) ou comme dictionnaire clé/valeur
  • Ajouter, insérer et retirer des éléments avec table.insert/table.remove
  • Comprendre pourquoi #t n'est fiable que sur une séquence sans trou
  • Parcourir une table imbriquée (tables de tables) pour représenter des structures complexes
  • Écrire une fonction de copie explicite pour éviter le piège de la copie par référence

Dans quel contexte ?

Un script d'inventaire de jeu doit stocker à la fois une liste ordonnée d'objets équipés (l'arme en main, en position 1) et un dictionnaire de statistiques du joueur (santé, mana, niveau). En Lua, ces deux besoins, qui semblent différents, se résolvent avec la même structure : la table. Savoir laquelle des deux "formes" utiliser, et surtout comment éviter que deux inventaires ne partagent accidentellement la même mémoire, est une compétence essentielle avant d'écrire le moindre système de jeu un peu sérieux.

Usage de la tableSyntaxe typiqueParcours recommandé
Tableau (array){"a", "b", "c"}ipairs
Dictionnaire{cle = valeur}pairs
Les deux mélangés{1, 2, cle = valeur}ipairs puis pairs séparément

Piège fréquent

local copie = original ne copie jamais le contenu d'une table : les deux variables pointent vers la même donnée en mémoire. Modifier copie modifie aussi original. Il faut une fonction explicite (shallowCopy ou deepCopy, montrées plus bas) pour obtenir une vraie copie indépendante.

Le décalage d'index qui déroute tout le monde au début

Contrairement à la quasi-totalité des langages modernes (Python, JavaScript, C), les tables Lua utilisées comme tableau commencent leur indexation à 1, pas à 0. Ce choix, hérité des langages plus anciens comme Fortran, est une source d'erreurs classique chez les développeurs venant d'un autre langage — le réflexe à acquérir dès maintenant.

Pourquoi #t n'est pas toujours fiable

L'opérateur # donne la "longueur" d'une table, mais son comportement n'est bien défini QUE si la table ne contient pas de "trou" (un index manquant au milieu de la séquence). Dès qu'un trou apparaît, le résultat de #t devient imprévisible selon l'implémentation. C'est une limite à connaître pour éviter des bugs subtils dans du code qui semble pourtant correct au premier regard.

Le piège de la copie par référence

Comme dans beaucoup de langages, assigner une table à une nouvelle variable ne copie PAS son contenu : les deux variables pointent vers la même table en mémoire, et modifier l'une modifie l'autre. Il faut écrire soi-même une fonction de copie explicite (superficielle ou profonde selon le besoin) si l'on veut réellement dupliquer les données — un oubli fréquent qui cause des bugs difficiles à comprendre.

Commandes & code

Tables : tableaux, dictionnaires et itération

La table est LA structure de données unique de Lua : elle sert à la fois de tableau, de dictionnaire, d'objet, et même de module.

lua
-- Table utilisée comme tableau (array), indices entiers commençant à 1 (PAS 0)
local inventory = {"potion", "epee", "bouclier"}
print(inventory[1])       -- "potion" (premier élément à l'index 1, pas 0)
print(#inventory)          -- 3, l'opérateur "#" donne la longueur (pour un tableau sans trou)

inventory[4] = "arc"        -- ajout en fin
table.insert(inventory, "casque")           -- ajoute en fin également
table.insert(inventory, 1, "torche")         -- insère en position 1, décale le reste
table.remove(inventory, 1)                    -- retire l'élément en position 1
lua
-- Table utilisée comme dictionnaire (map/hash), clés arbitraires
local player = {
  name = "Alice",
  level = 12,
  ["class"] = "Mage",         -- syntaxe équivalente à "class = "Mage""
  [1] = "special_first_slot",  -- une table peut mélanger clés entières et clés string
}
print(player.name, player["level"], player.class)
lua
-- Tables imbriquées, très courantes pour représenter des structures complexes
local gameState = {
  players = {
    { name = "Alice", hp = 100, position = { x = 10, y = 20 } },
    { name = "Bob", hp = 80, position = { x = 5, y = 15 } },
  },
  settings = { difficulty = "hard", maxPlayers = 4 },
}

print(gameState.players[1].name)                 -- "Alice"
print(gameState.players[2].position.x)             -- 5
gameState.players[1].hp = gameState.players[1].hp - 20
lua
-- Piège classique : "#" (longueur) n'est fiable QUE sur une séquence sans trou (nil au milieu)
local broken = {1, 2, nil, 4}
print(#broken)   -- résultat NON SPÉCIFIÉ par le standard (peut être 2 ou 4 selon l'implémentation)

-- Solution : maintenir une longueur explicite si des trous sont possibles, ou utiliser table.pack
local safe = table.pack(1, 2, nil, 4)
print(safe.n)     -- 4, table.pack stocke explicitement le nombre d'arguments dans le champ "n"
lua
-- Copie de table : l'affectation directe copie la RÉFÉRENCE, pas le contenu (comme les objets en JS/Python)
local original = {hp = 100, mana = 50}
local reference = original           -- reference pointe vers la MÊME table
reference.hp = 0
print(original.hp)                    -- 0 aussi ! ce n'était pas une copie

-- Copie superficielle (shallow copy) explicite
local function shallowCopy(t)
  local copy = {}
  for k, v in pairs(t) do
    copy[k] = v
  end
  return copy
end

-- Copie profonde (deep copy) récursive, nécessaire pour des tables imbriquées
local function deepCopy(t)
  if type(t) ~= "table" then return t end
  local copy = {}
  for k, v in pairs(t) do
    copy[k] = deepCopy(v)
  end
  return copy
end
lua
-- Fonctions utilitaires courantes de la bibliothèque "table"
local scores = {42, 17, 99, 3, 56}
table.sort(scores)                                    -- tri en place, croissant par défaut
table.sort(scores, function(a, b) return a > b end)     -- tri décroissant avec fonction de comparaison

print(table.concat({"a", "b", "c"}, ", "))              -- "a, b, c"
print(table.concat(scores, "-"))                          -- "99-56-42-17-3"

Résumé

  • Les indices de tableau Lua commencent à 1, pas 0 ; #t n'est fiable que sans trou dans la séquence.
  • Une table est copiée PAR RÉFÉRENCE : il faut une fonction explicite pour une copie superficielle ou profonde.
  • table.insert, table.remove, table.sort, table.concat couvrent la majorité des manipulations courantes.
  • ipairs parcourt la partie séquentielle dans l'ordre, pairs parcourt toutes les clés sans ordre garanti.

Exercices pratiques

1 disponible
1

Mission : sauver un inventaire de crafting corrompu

Objectif : Diagnostiquer un bug de partage de mémoire entre deux inventaires et une longueur imprévisible, puis corriger le script.

Contexte

Un script de crafting FiveM prépare un inventaire de secours à partir de l'inventaire principal du joueur :

lua
local mainInventory = { "potion", "epee", "bouclier" }
local backupInventory = mainInventory

table.remove(backupInventory, 2)   -- le joueur "vend" une copie de l'épée depuis le backup

print(#mainInventory)   -- affiche 2, alors que le joueur n'a RIEN vendu de son inventaire principal !

mainInventory[5] = "torche"
mainInventory[4] = nil
print(#mainInventory)   -- résultat imprévisible selon le testeur

Le créateur ne comprend pas pourquoi vendre depuis backupInventory fait disparaître l'épée de mainInventory, ni pourquoi #mainInventory donne des résultats différents entre deux tests.

Résoudre l’exercice →