SESSIÓ 3 - UD2.3: ESTRUCTURES I MAPPINGS - Solidity¶
Setmana 3 (4-10 maig) - 2 hores
FITXA TÈCNICA¶
| Dada | Valor |
|---|---|
| Unitat | UD2 - Smart Contracts |
| Tema | Estructures de dades: Structs, Mappings i Arrays |
| Durada | 2 hores |
| Nivell | Intermedi |
| Eines | Remix IDE |
| Requisits | Sessions 1 i 2 completades |
OBJECTIUS D'APRENENTATGE¶
Al finalitzar aquesta sessió, l'alumnat serà capaç de:
- ✅ Definir i utilitzar
structsper crear tipus de dades personalitzats - ✅ Implementar
mappingsper emmagatzemar relacions clau-valor - ✅ Diferenciar i gestionar arrays dinàmics i fixos
- ✅ Comprendre les ubicacions d'emmagatzematge:
storage,memory,calldata - ✅ Combinar estructures per crear lògica de negoci complexa
- ✅ Optimitzar el gas evitant còpies innecessàries de dades
TEMPORITZACIÓ DE LA SESSIÓ¶
| Temps | Activitat | Metodologia |
|---|---|---|
| 0-10 min | Revisió sessió anterior | Q&A + dubtes modifiers |
| 10-30 min | Teoria: Estructures i Storage | Exposició + diagrames |
| 30-100 min | Pràctica guiada: Sistema de Votació | Codificació conjunta |
| 100-120 min | Exercici: Gestió de reserves | Pràctica individual |
MATERIAL TEÒRIC¶
1. Ubicacions d'Emmagatzematge (Storage Locations)¶
Entendre on es guarden les dades és crucial per a la seguretat i eficiència del gas.
| Ubicació | Descripció | Durada | Gas | Ús |
|---|---|---|---|---|
storage |
Emmagatzematge permanent de la blockchain | Permanent | Molt car | Variables d'estat |
memory |
Memòria temporal durant l'execució | Funció actual | Barato | Paràmetres, variables locals |
calldata |
Memòria només lectura per paràmetres externs | Funció actual | Més barat | Paràmetres external |
Exemple Pràctic:
contract ExempleStorage {
uint256 public valorStorage; // Per defecte: storage
// Funció que rep dades
function exemple(
uint256 _valorMemory, // Per defecte: memory
string calldata _text // calldata per eficiència
) public {
uint256 local = _valorMemory; // memory
valorStorage = local; // Copia de memory a storage (car)
}
// Important amb structs i arrays
struct Dada {
uint256 valor;
}
Dada public dadaStorage; // storage
function modificarStorage() public {
Dada storage ref = dadaStorage; // Referència (modifica l'original)
ref.valor = 100;
}
function modificarMemory() public {
Dada memory copia = dadaStorage; // Còpia (no modifica l'original)
copia.valor = 200;
// dadaStorage.valor segueix sent 100
}
}
Regla d'Or:
- Variables d'estat →
storage - Paràmetres de funció →
memoryocalldata - Variables locals (tipus valor) →
memory(implícit) - Variables locals (structs/arrays) → Explicitar
storageomemory
2. Structs (Estructures)¶
Permeten agrupar variables sota un mateix nom.
contract ExempleStructs {
// Definició del struct
struct Persona {
string nom;
uint256 edat;
address wallet;
bool verificat;
}
// Variable d'estat tipus struct
Persona public propietari;
constructor() {
propietari = Persona({
nom: "Admin",
edat: 30,
wallet: msg.sender,
verificat: true
});
}
// Funció per crear nou usuari
function crearUsuari(
string memory _nom,
uint256 _edat
) public returns (Persona memory) {
Persona memory nouUsuari = Persona({
nom: _nom,
edat: _edat,
wallet: msg.sender,
verificat: false
});
return nouUsuari;
}
// Actualitzar directament
function actualitzarEdat(uint256 novaEdat) public {
propietari.edat = novaEdat; // Accés directe a propietats
}
}
3. Mappings (Taules Hash)¶
Els mappings són com diccionaris o taules hash. No es poden iterar directament.
contract ExempleMappings {
// mapping(clau => valor)
mapping(address => uint256) public saldos;
mapping(string => uint256) public idsPerNom;
mapping(uint256 => address) public propietariPerId;
// Mappings anidats
mapping(address => mapping(address => uint256)) public permisos;
function depositar() public payable {
saldos[msg.sender] += msg.value;
}
function consultarSaldo(address usuari) public view returns (uint256) {
return saldos[usuari];
// Si no existeix, retorna 0 (valor per defecte)
}
function donarPermis(address usuari, address delegat, uint256 nivell) public {
permisos[usuari][delegat] = nivell;
}
// ⚠️ IMPORTANT: No es pot iterar un mapping
// function obtenirTotsUsuaris() public view returns (address[]) {
//
// ❌ Això no es pot fer directament
// }
}
Solució per iterar: Combinar amb un array.
contract MappingAmbArray {
mapping(address => uint256) public saldos;
address[] public llistaUsuaris;
mapping(address => bool) public jaRegistrat;
function registrar() public {
if (!jaRegistrat[msg.sender]) {
llistaUsuaris.push(msg.sender);
jaRegistrat[msg.sender] = true;
}
saldos[msg.sender] += 100;
}
function obtenirTotalUsuaris() public view returns (uint256) {
return llistaUsuaris.length;
}
}
4. Arrays (Matrius)¶
Poden ser fixos o dinàmics.
contract ExempleArrays {
// Array fix (5 elements)
uint256[5] public arrayFix;
// Array dinàmic
uint256[] public arrayDinamic;
string[] public noms;
// Afegir elements
function afegir(uint256 valor) public {
arrayDinamic.push(valor);
}
// Afegir amb push retornant índex
function afegirNom(string memory nom) public returns (uint256) {
noms.push(nom);
return noms.length - 1; // Retornar índex
}
// Accedir
function obtenirNom(uint256 index) public view returns (string memory) {
require(index < noms.length, "Index fora de rang");
return noms[index];
}
// Eliminar (últim element)
function eliminarUltim() public {
require(noms.length > 0, "Array buit");
noms.pop();
}
// Eliminar element específic (costós en gas)
function eliminarPerIndex(uint256 index) public {
require(index < noms.length, "Index invalid");
// Moure l'últim element a la posició a eliminar
noms[index] = noms[noms.length - 1];
noms.pop();
}
// Longitud
function longitud() public view returns (uint256) {
return noms.length;
}
}
PRÀCTICA GUIADA PAS A PAS¶
EXERCICI 1: Sistema de Votació Descentralitzat¶
Objectiu: Crear un contracte de votació utilitzant structs, mappings i arrays.
Pas 1: Crear el Fitxer¶
- A Remix IDE, crea:
03_SistemaVotacio.sol - Estructura bàsica:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
contract SistemaVotacio {
// DEFINIREM LES ESTRUCTURES AQUÍ
}
Pas 2: Definir Structs i Variables¶
contract SistemaVotacio {
// Struct per al candidat
struct Candidat {
uint256 id;
string nom;
uint256 vots;
bool actiu;
}
// Variables d'estat
address public propietari;
uint256 public totalCandidats;
uint256 public totalVots;
bool public votacioIniciada;
bool public votacioFinalitzada;
// Mappings
mapping(uint256 => Candidat) public candidats; // ID -> Candidat
mapping(address => bool) public haVotat; // Address -> Bool
mapping(address => uint256) public votRealitzat; // Address -> CandidatID
// Array per iterar candidats
uint256[] public llistaCandidatsIds;
// Events
event CandidatRegistrat(uint256 indexed id, string nom);
event VotEmes(address indexed votant, uint256 indexed candidatId);
event VotacioFinalitzada(uint256 guanyadorId);
constructor() {
propietari = msg.sender;
}
// Modificadors
modifier nomesPropietari() {
require(msg.sender == propietari, "No es propietari");
_;
}
modifier votacioActiva() {
require(votacioIniciada, "Votacio no iniciada");
require(!votacioFinalitzada, "Votacio finalitzada");
_;
}
// ... continuarem amb les funcions
}
Pas 3: Funcions de Gestió de Candidats¶
contract SistemaVotacio {
// ... (codi anterior)
// Afegir candidat (només propietari)
function afegirCandidat(string memory nom)
public
nomesPropietari
{
require(bytes(nom).length > 0, "Nom buit");
require(!votacioIniciada, "No es poden afegir candidats durant la votacio");
totalCandidats++;
uint256 nouId = totalCandidats;
candidats[nouId] = Candidat({
id: nouId,
nom: nom,
vots: 0,
actiu: true
});
llistaCandidatsIds.push(nouId);
emit CandidatRegistrat(nouId, nom);
}
// Obtenir tots els candidats (per al frontend)
function obtenirTotsCandidats()
public
view
returns (
uint256[] memory ids,
string[] memory noms,
uint256[] memory vots,
bool[] memory actius
)
{
uint256 longitud = llistaCandidatsIds.length;
ids = new uint256[](longitud);
noms = new string[](longitud);
vots = new uint256[](longitud);
actius = new bool[](longitud);
for (uint256 i = 0; i < longitud; i++) {
uint256 id = llistaCandidatsIds[i];
Candidat memory c = candidats[id];
ids[i] = c.id;
noms[i] = c.nom;
vots[i] = c.vots;
actius[i] = c.actiu;
}
return (ids, noms, vots, actius);
}
}
Pas 4: Funcions de Votació¶
contract SistemaVotacio {
// ... (codi anterior)
// Iniciar votació
function iniciarVotacio() public nomesPropietari {
require(!votacioIniciada, "Ja iniciada");
require(totalCandidats >= 2, "Minim 2 candidats");
votacioIniciada = true;
}
// Votar
function votar(uint256 candidatId) public votacioActiva {
require(!haVotat[msg.sender], "Ja has votat");
require(candidatId > 0 && candidatId <= totalCandidats, "Candidat invalid");
require(candidats[candidatId].actiu, "Candidat no actiu");
// Registrar vot
haVotat[msg.sender] = true;
votRealitzat[msg.sender] = candidatId;
candidats[candidatId].vots++;
totalVots++;
emit VotEmes(msg.sender, candidatId);
}
// Finalitzar votació
function finalitzarVotacio() public nomesPropietari {
require(votacioIniciada, "No iniciada");
require(!votacioFinalitzada, "Ja finalitzada");
votacioFinalitzada = true;
// Trobar guanyador
uint256 guanyadorId = 0;
uint256 maxVots = 0;
for (uint256 i = 0; i < llistaCandidatsIds.length; i++) {
uint256 id = llistaCandidatsIds[i];
if (candidats[id].vots > maxVots) {
maxVots = candidats[id].vots;
guanyadorId = id;
}
}
emit VotacioFinalitzada(guanyadorId);
}
// Obtenir guanyador
function obtenirGuanyador()
public
view
returns (uint256 id, string memory nom, uint256 vots)
{
require(votacioFinalitzada, "Votacio no finalitzada");
uint256 guanyadorId = 0;
uint256 maxVots = 0;
for (uint256 i = 0; i < llistaCandidatsIds.length; i++) {
uint256 id = llistaCandidatsIds[i];
if (candidats[id].vots > maxVots) {
maxVots = candidats[id].vots;
guanyadorId = id;
}
}
return (guanyadorId, candidats[guanyadorId].nom, candidats[guanyadorId].vots);
}
// Consultar el meu vot
function obtenirElMeuVot()
public
view
returns (uint256 candidatId, bool votat)
{
return (votRealitzat[msg.sender], haVotat[msg.sender]);
}
}
Pas 5: Provar el Contracte¶
Seqüència de proves:
- Desplegar (Account 1 = Propietari)
- Afegir candidats (Account 1):
afegirCandidat("Candidat A")afegirCandidat("Candidat B")afegirCandidat("Candidat C")
- Iniciar votació (Account 1):
iniciarVotacio()
- Votar (Account 2):
votar(1)→ Candidat A
- Votar (Account 3):
votar(2)→ Candidat B
- Votar (Account 4):
votar(1)→ Candidat A
- Consultar resultats (Tothom):
obtenirTotsCandidats()→ Veure vots per candidatobtenirElMeuVot()(des de Account 2) → (1, true)
- Finalitzar (Account 1):
finalitzarVotacio()
- Obtenir guanyador:
obtenirGuanyador()→ (1, "Candidat A", 2)
EXERCICI PROPOSAT: SISTEMA DE RESERVES D'ESPAIS¶
Enunciat:¶
Crea un contracte SistemaReserves per gestionar reserves d'espais (sales, cotxes, equipament).
Requisits obligatoris:
-
Structs:
Espai: id, nom, capacitat, bool disponible, address propietariReserva: id, uint256 espaiId, address usuari, uint256 dataInici, uint256 dataFi, bool activa
-
Mappings:
uint256 => Espai(espais per ID)uint256 => Reserva(reserves per ID)address => uint256[](reserves per usuari)
-
Arrays:
uint256[](llista d'IDs d'espais)
-
Funcions:
crearEspai(string memory nom, uint256 capacitat)→ Només adminreservarEspai(uint256 espaiId, uint256 dies)→ Paga una tarifa (simulada)cancel·larReserva(uint256 reservaId)→ Només qui ha reservatobtenirReservesUsuari()→ Retorna les reserves de qui cridaobtenirEspaisDisponibles()→ Retorna espais no reservats
-
Validacions:
- No reservar espai no disponible
- No superar capacitat (simulat)
- Data fi > data inici
Template per començar:¶
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
contract SistemaReserves {
address public admin;
uint256 public totalEspais;
uint256 public totalReserves;
uint256 public tarifaDiaria = 0.01 ether;
struct Espai {
uint256 id;
string nom;
uint256 capacitat;
bool disponible;
address propietari;
}
struct Reserva {
uint256 id;
uint256 espaiId;
address usuari;
uint256 dataInici;
uint256 dataFi;
bool activa;
}
mapping(uint256 => Espai) public espais;
mapping(uint256 => Reserva) public reserves;
mapping(address => uint256[]) public reservesPerUsuari;
uint256[] public llistaEspaisIds;
event EspaiCreat(uint256 indexed espaiId, string nom);
event ReservaFeta(uint256 indexed reservaId, uint256 espaiId, address usuari);
event ReservaCancel·lada(uint256 indexed reservaId);
constructor() {
admin = msg.sender;
}
modifier nomesAdmin() {
require(msg.sender == admin, "No es admin");
_;
}
// COMPLETA LES FUNCIONS
}
Solució:¶
Fes clic per veure la solució proposada
// ... (structs i variables anteriors)
function crearEspai(string memory nom, uint256 capacitat) public nomesAdmin {
totalEspais++;
espais[totalEspais] = Espai(totalEspais, nom, capacitat, true, msg.sender);
llistaEspaisIds.push(totalEspais);
emit EspaiCreat(totalEspais, nom);
}
function reservarEspai(uint256 espaiId, uint256 dies) public payable {
require(espais[espaiId].disponible, "Espai no disponible");
require(msg.value >= tarifaDiaria * dies, "Pagament insuficient");
totalReserves++;
uint256 dataInici = block.timestamp;
uint256 dataFi = dataInici + (dies * 1 days);
reserves[totalReserves] = Reserva({
id: totalReserves,
espaiId: espaiId,
usuari: msg.sender,
dataInici: dataInici,
dataFi: dataFi,
activa: true
});
reservesPerUsuari[msg.sender].push(totalReserves);
espais[espaiId].disponible = false;
emit ReservaFeta(totalReserves, espaiId, msg.sender);
}
function cancel·larReserva(uint256 reservaId) public {
Reserva storage reserva = reserves[reservaId];
require(reserva.usuari == msg.sender, "No es la teva reserva");
require(reserva.activa, "Reserva no activa");
reserva.activa = false;
espais[reserva.espaiId].disponible = true;
// Reemborsament (simplificat)
payable(msg.sender).transfer(tarifaDiaria);
emit ReservaCancel·lada(reservaId);
}
function obtenirReservesUsuari() public view returns (uint256[] memory) {
return reservesPerUsuari[msg.sender];
}
EXERCICI EXTRA (Opcional)¶
Gestió d'Inventari amb Històric:
- Crea un struct
Producteamb historial de moviments - Cada moviment (entrada/sortida) guarda timestamp i quantitat
- Utilitza un array dins del struct per l'historial
- Implementa funció per obtenir l'historial complet d'un producte
MATERIALS DE SUPORT
Cheat Sheet d'Estructures:
// STRUCT
struct Persona { string nom; uint256 edat; }
Persona memory p = Persona("Anna", 30);
Persona storage pRef = persones[id]; // Referència
// MAPPING
mapping(address => uint256) saldos;
saldos[msg.sender] = 100;
uint256 s = saldos[adreça]; // 0 si no existeix
// ARRAY
uint256[] public numeros;
numeros.push(10);
uint256 ult = numeros[numeros.length - 1];
numeros.pop(); // Elimina últim
delete numeros[0]; // Elimina element (deixa forat)
// STORAGE LOCATIONS
function func(
uint256 _a, // memory (implícit)
string calldata _b, // calldata (més barat)
bytes memory _c // memory
) public {
uint256 local = _a; // memory
}
Errors Comuns i Solucions:¶
| Error | Causa | Solució |
|---|---|---|
TypeError: Invalid location for parameter |
Location incorrecta (ex: storage en paràmetre) | Usa memory o calldata per paràmetres |
UnimplementedFeatureError: Copying of type struct |
Intentar copiar struct complex a memory | Usa referències storage o simplifica |
Index out of bounds |
Accedir a array fora de rang | Verifica index < array.length |
Mapping iteration |
Intentar fer loop en mapping | Usa array auxiliar per guardar claus |
Bones Pràctiques:¶
✅ Fes:
- Usa calldata per a strings/arrays en funcions external
- Usa storage per referenciar variables d'estat dins funcions
- Combina mapping + array per poder iterar
- Inicialitza arrays dinàmics amb new type[](size) si saps la mida
❌ No facis:
- No copiis structs grans a memory innecessàriament
- No intentis iterar mappings directament
- No oblidis verificar límits d'arrays
- No usis delete en arrays si vols mantenir l'ordre (usa swap+pop)
Enllaços Útils:¶
- Solidity Types: https://docs.soliditylang.org/en/latest/types.html
- Structs: https://solidity-by-example.org/structs/
- Mappings: https://solidity-by-example.org/mapping/
- Arrays: https://solidity-by-example.org/array/
QÜESTIONARI DE REPÀS¶
Respon abans de la propera sessió:
- Quina diferència hi ha entre
memoryistorage? - Per què no es poden iterar els mappings directament?
- Què passa si accedeixes a una clau que no existeix en un mapping?
- Quina és la diferència entre
array.pop()idelete array[index]? - Quan hauries d'utilitzar
calldataen lloc dememory? - Com es declara un array de structs?
- Què és més costós en gas: escriure a
storageo amemory? - Com pots fer per iterar sobre les claus d'un mapping?
- Què passa amb les dades d'un struct quan es passa per
memorya una funció? - Per què és important inicialitzar arrays dinàmics abans d'usar-los?
Solució:¶
Fes clic per veure la solució proposada
storageés permanent (blockchain),memoryés temporal (execució)- Perquè els mappings són taules hash disperses, no tenen índexs seqüencials
- Retorna el valor per defecte del tipus (0 per uint, false per bool, "" per string)
pop()elimina l'últim i redueix length,deletebuida l'element però manté length- En funcions
externalper a dades que no es modificaran (més barat) MyStruct[] public arrayStructs;- Escriure a
storageés molt més costós - Mantenint un array auxiliar amb les claus inserides
- Es fa una còpia, les modificacions no afecten l'original
- Perquè comencen buits i cal assignar-los mida o fer push
PREPARACIÓ PER LA SESSIÓ 4¶
Abans de la propera classe:
✅ Completa l'exercici del sistema de reserves
✅ Prova iteracions amb arrays i mappings
✅ Respon el qüestionari de repàs
✅ Porta dubtes sobre storage locations
Material a revisar:
- Arrays dinàmics vs fixos
- Mappings i iteració
- Gas optimization bàsic
Pròxima sessió: Desplegament a Testnet Sepolia i testing real
CONSELLS PER A L'ALUMNAT¶
Per optimitzar gas:¶
-
Packaging de variables:
// ❌ Car (3 slots de storage) uint256 public a; uint256 public b; bool public c; // ✅ Eficient (1 slot de storage) uint256 public a; uint256 public b; bool public c; // Solidity 0.8+ fa packing automàtic en structs -
Usa
calldataper paràmetres:function externFunction(string calldata text) external { // Més barat que memory } -
Evita loops grans:
- Cada iteració suma gas
- Límit recomanat: < 100 iteracions
- Millor: Paginació o events
Per debuggar estructures:¶
- Usa funcions
viewper retornar arrays complets - Exemple:
function getArray() public view returns (uint256[] memory) - Usa events per registrar canvis en structs
✅ CHECKLIST FINAL DE LA SESSIÓ 3:
- Entenc la diferència entre storage, memory i calldata
- Puc definir i usar structs
- Sé com funcionen els mappings i les seves limitacions
- Puc iterar arrays correctament
- He completat el sistema de votació
- He intentat l'exercici de reserves
- He respost el qüestionari de repàs
