Salta el contingut
Logo esquerra


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:

  1. ✅ Definir i utilitzar structs per crear tipus de dades personalitzats
  2. ✅ Implementar mappings per emmagatzemar relacions clau-valor
  3. ✅ Diferenciar i gestionar arrays dinàmics i fixos
  4. ✅ Comprendre les ubicacions d'emmagatzematge: storage, memory, calldata
  5. ✅ Combinar estructures per crear lògica de negoci complexa
  6. ✅ 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ó → memory o calldata
  • Variables locals (tipus valor) → memory (implícit)
  • Variables locals (structs/arrays) → Explicitar storage o memory

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

  1. A Remix IDE, crea: 03_SistemaVotacio.sol
  2. 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:

  1. Desplegar (Account 1 = Propietari)
  2. Afegir candidats (Account 1):
    • afegirCandidat("Candidat A")
    • afegirCandidat("Candidat B")
    • afegirCandidat("Candidat C")
  3. Iniciar votació (Account 1):
    • iniciarVotacio()
  4. Votar (Account 2):
    • votar(1) → Candidat A
  5. Votar (Account 3):
    • votar(2) → Candidat B
  6. Votar (Account 4):
    • votar(1) → Candidat A
  7. Consultar resultats (Tothom):
    • obtenirTotsCandidats() → Veure vots per candidat
    • obtenirElMeuVot() (des de Account 2) → (1, true)
  8. Finalitzar (Account 1):
    • finalitzarVotacio()
  9. 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:

  1. Structs:

    • Espai: id, nom, capacitat, bool disponible, address propietari
    • Reserva: id, uint256 espaiId, address usuari, uint256 dataInici, uint256 dataFi, bool activa
  2. Mappings:

    • uint256 => Espai (espais per ID)
    • uint256 => Reserva (reserves per ID)
    • address => uint256[] (reserves per usuari)
  3. Arrays:

    • uint256[] (llista d'IDs d'espais)
  4. Funcions:

    • crearEspai(string memory nom, uint256 capacitat) → Només admin
    • reservarEspai(uint256 espaiId, uint256 dies) → Paga una tarifa (simulada)
    • cancel·larReserva(uint256 reservaId) → Només qui ha reservat
    • obtenirReservesUsuari() → Retorna les reserves de qui crida
    • obtenirEspaisDisponibles() → Retorna espais no reservats
  5. 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 Producte amb 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ó:

  1. Quina diferència hi ha entre memory i storage?
  2. Per què no es poden iterar els mappings directament?
  3. Què passa si accedeixes a una clau que no existeix en un mapping?
  4. Quina és la diferència entre array.pop() i delete array[index]?
  5. Quan hauries d'utilitzar calldata en lloc de memory?
  6. Com es declara un array de structs?
  7. Què és més costós en gas: escriure a storage o a memory?
  8. Com pots fer per iterar sobre les claus d'un mapping?
  9. Què passa amb les dades d'un struct quan es passa per memory a una funció?
  10. Per què és important inicialitzar arrays dinàmics abans d'usar-los?

Solució:

Fes clic per veure la solució proposada
  1. storage és permanent (blockchain), memory és temporal (execució)
  2. Perquè els mappings són taules hash disperses, no tenen índexs seqüencials
  3. Retorna el valor per defecte del tipus (0 per uint, false per bool, "" per string)
  4. pop() elimina l'últim i redueix length, delete buida l'element però manté length
  5. En funcions external per a dades que no es modificaran (més barat)
  6. MyStruct[] public arrayStructs;
  7. Escriure a storage és molt més costós
  8. Mantenint un array auxiliar amb les claus inserides
  9. Es fa una còpia, les modificacions no afecten l'original
  10. 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:

  1. 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
    

  2. Usa calldata per paràmetres:

    function externFunction(string calldata text) external {
        // Més barat que memory
    }
    

  3. Evita loops grans:

    • Cada iteració suma gas
    • Límit recomanat: < 100 iteracions
    • Millor: Paginació o events

Per debuggar estructures:

  • Usa funcions view per 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