Un contrato inteligente puede almacenar cientos o miles de datos a lo largo de su vida útil. En estos casos debemos implementar estructuras de datos que nos permitan manipular gran cantidad de datos de forma fácil y organizada.
Qué son los array o vectores de datos
La primera estructura de datos, típica de cualquier lenguaje de programación, son los Arrays. Los arrays almacenan de forma secuencial datos de un mismo tipo y los mismos pueden ser accedidos a través de su índice o posición dentro del mismo. Pueden ser de una longitud determinada o de longitud dinámica dependiendo la necesidad.
// Array de números de hasta 3 posicionesuint[3] numbers =[1,2,3];// Array de números de longitud variableuint[] numbers;
En cualquier tipo de array, puedes agregar o quitar elementos utilizando los métodos push(x) y pop().
push(x) agrega un nuevo elemento pasado como argumento al final del array, mientras que pop() remueve el último elemento.
El acceso a estos datos debe realizarse a través del índice del elemento comenzando por cero. Si tenemos el array string words = ["Bienvenido", "a", "Platzi"], el elemento 2 corresponde al string "Platzi" y accedes a este con la sintaxis words[2].
Finalmente, puedes conocer la longitud de un array a través de su método myArr.length. El mismo devolverá un número entero que representa el tamaño del mismo.
Qué son los mappings o asignaciones de datos
Los arrays tiene sus limitaciones. Si tenemos un array con miles de datos, acceder a un valor en el medio de este puede ser costoso. Tener que recorrer todo el mismo para encontrar un valor requiere procesamiento y, por ende, consumo de gas. Los Mappings solucionan este problema permitiendo asignar valores a una clave única para acceder al dato.
Similar a un objeto donde acceder a sus propiedades a través de un nombre específico. Los mappings permiten utilizar cualquier tipo de clave para acceder a un valor y el mismo puede crecer y guardar tantos valores como necesitemos.
La declaración de un mapping requiere de especificar el tipo de dato de la clave, y el tipo de dato del valor que esta guardará.
mapping(string => uint) public myMapping;
Uno de los usos más habituales de los mappings es para guardar el balance económico de una cuenta en el contrato. Cada clave guarda una address y el valor es el valor en ETH que esta posee.
mapping(address=> uint)public balances;
El contenido del mapping puedes imaginarlo de la siguiente manera.
A través de direccion123, direccion456 y direccion789 puedes acceder rápidamente al balance de cada cuenta y su llamado se realiza de la forma balances["direccion123"].
Los mappings son completamente dinámicos, pudiendo almacenar un mapping dentro de otro.
mapping(address =>mapping(uint => bool)) public nested;
Aumentando así su complejidad y las posibilidades de guardar información.
Una desventaja de los mapping es que no permiten ser recorridos como un array u obtener su longitud. Para esto puedes hacer uso de extensiones y utilidades para darle más funcionalidad a esta estructura de datos si necesitas resolver casos de uso complejos.
Conclusión
Tanto los Arrays como los Mappings tienen sus ventajas y desventajas y pueden ser implementados para almacenar información. Comprender estas diferentes estructuras de datos que Solidity implementa nos permitirá tener más herramientas para el desarrollo contratos inteligentes, organizar la información y el acceso a estos.
Los métodos push y pop solo funcionan en los arrays dinámicos. Para agregar/cambiar el valor de un elemento en un array fijo usaremos los corchetes y la posición del elemento a cambiar. 😃
Excelente aporte, gracias Néstor !
Muchas gracias por el dato
Un reto por cada clase ha sido una buena técnica, ya que se va practicando lo aprendido, se revisa, se prueba y vamos adelantando el proyecto. Me ha gustado mucho.
Array
Contenedores que almacenan datos de un tipo especifico
Pueden ser de tamanios dinamicos o fijos
Fijos: uint[3] steps = [1,2,3];
Dinamicos: uint[] steps;
Se acceden a traves de su indice
Metodos push y pop solo funcionan en arrays dinamicos
Excelente Profe, claro, preciso y me gusta mucho la dinámica de poner retos y revisión entre clases.
Los Arrays son una colección ordenada de datos de un solo tipo, mientras los mappings son Hashmaps, es decir, estructuras de clave valor. El correcto uso de estas estructuras de datos mejora el rendimiento de nuestro contrato, disminuyendo la carga de computo. Por ejemplo, buscar un address en un array tiene una complejidad de O(N) mientras que en un mapping con el address como clave tiene una complejidad de O(1). Me corrigen si no estoy en lo correcto jajaja. Saludos y buen contenido
En general los mappings tienen una complejidad de O(log2(n)).
Este es mi avance con los arrays y las funciones nuevas del reto #7:
// SPDX-License-Identifier: GPL-3.0pragma solidity >=0.7.0<0.9.0;contract Proyecto_Platzi{enumState{Open,Closed} struct projectInfo{ string name; string description; string owner; address payable ownerWallet;State state; uint goal; uint currentFund;} projectInfo[] projects;mapping(string=> uint) projectsContributions; modifier onlyOwner(uint index){require( msg.sender== projects[index].ownerWallet,"Only owner can change the projects state");//la función es insertada en donde aparece este simbolo _;} modifier differentToTheOwner(uint index){require( msg.sender!= projects[index].ownerWallet,"The owner can't fund the projects");//la función es insertada en donde aparece este simbolo _;} event ChangeState(State previousState,State newState
); event FundValueGiven( uint fundGiven, uint valueToGoal, string greetingMessage
);functionviewProjects(uint index)public view returns(projectInfo memory){return projects[index];}functionviewProjectFund(string calldata projectName)public view returns(uint){return projectsContributions[projectName];}functioncreateProject(string calldata _name, string calldata _owner, string calldata _description, uint _goal,State _state)public{ projectInfo memory newProject =projectInfo(_name, _description, _owner,payable(msg.sender), _state, _goal,0); projects.push(newProject);}functionfundProject(uint ProjectIndex)public payable differentToTheOwner(ProjectIndex){require(projects[ProjectIndex].state!=State.Closed,"Sorry, this projects is closed and cannot receive funds");require(msg.value>0,"Sorry, fund value must be greater than 0, try again"); projects[ProjectIndex].ownerWallet.transfer(msg.value); projects[ProjectIndex].currentFund+= msg.value; projectsContributions[projects[ProjectIndex].name]= projects[ProjectIndex].currentFund; emit FundValueGiven(msg.value,(projects[ProjectIndex].goal- msg.value),"Thanks for your contribution");}functionchangeProjectState(State newState, uint ProjectIndex)publiconlyOwner(ProjectIndex){require(projects[ProjectIndex].state!= newState,"Sorry, to change the state you must put a different state from the current one"); emit ChangeState(projects[ProjectIndex].state, newState); projects[ProjectIndex].state= newState;}}
Tengo una pregunta que es transversal a todas las clases. ¿Es mejor usar la lista de modifiers que uno tiene en la línea en que nombra la función, o en el cuerpo de la función o mejor sin modifiers y poner el require en el cuerpo de la función? Creo que ese tema puede causar desorden. Yo opté por poner los modifiers siempre en la línea de nombre de la función y no en el cuerpo, esto porque una función debe hacer lo que debe hacer y no ponerse a hacer validaciones previas.
¿Qué opinan?
En mi opinión deberías definir un modifier si este se repite en varias funciones, de lo contrario si quieres hacer una validación muy especifica estaría mejor usar require, pero esta es mi opinión sería bueno que se cree una discusión sobre el tema
Un array es un array de acá a la China. No hay mucho que explicar.
Los Mapping utilizan el concepto de clave/valor, como si fuese un JSON o un Objeto al cual accedemos a sus datos a partir del nombre de las propiedades.
A diferencia de un objeto clásico de JavaScript cuya correspondencia sería un Struct, un mapping da una forma de objeto a un conjunto de datos regulares, que siempre tendrán el mismo tipo de Key para el mismo tipo de Value.
Cual es mejor utilizar un array dinamico o un array fijo en cuestion de gas. Por ejemplo si se que tengo un maximo de numero en el array ejemplo 100 pero puede tener 1, 2, 3 etc. es mejor el dinamico o el fijo?
Esto depende del problema, estrictamente los arrays fijos tienen un menor costo en gas, si tienes claro el tamaño de la variable es mejor que uses arrays fijos, de lo contrario si no tienes claro cuantos elementos puede llegar a tener el array es mejor que uses arrays dinámicos para evitar casos de barrera.
Porque en el constructor tambien no se cambio el tipo de la variable _fundraisingGoal de uint a FundraisingState ???
Hola Leonardo, el _fundraisingGoal va a almacenar la cantidad de ethers que queremos levantar en la ronda de fundraising y en el valor de fundraisingState nos va a decir cuál es el estado actual de la ronda abierto o cerrado.
Hola que tal estoy intentando de probar los arrays, y al compilarlo me da error, intente compilarlo en 0.8.7 y 0.8.11, con el compilador, el error es:
// SPDX-License-Identifier: GPL-3.0pragma solidity >=0.7.0<0.9.0;contract CrowdFunding{enumFundraisingState{Opened,Closed} struct Contribution{ address contributor; uint256 value;} struct Project{ string id; string name; string description; address payable author;FundraisingState state; uint256 funds; uint256 fundraisingGoal;}Project[]public projects;mapping(string=>Contribution[])public contributions; event ProjectCreated( string projectId, string name, string description, uint256 fundraisingGoal
); event ProjectFunded(string projectId, uint256 value); event ProjectStateChanged(string id,FundraisingState state); modifier isAuthor(uint256 projectIndex){require( projects[projectIndex].author== msg.sender,"You need to be the project author"); _;} modifier isNotAuthor(uint256 projectIndex){require( projects[projectIndex].author!= msg.sender,"As author you can not fund your own project"); _;}functioncreateProject(string calldata id, string calldata name, string calldata description, uint256 fundraisingGoal)public{require(fundraisingGoal >0,"fundraising goal must be greater than 0");Project memory project =Project( id, name, description,payable(msg.sender),FundraisingState.Opened,0, fundraisingGoal
); projects.push(project); emit ProjectCreated(id, name, description, fundraisingGoal);}functionfundProject(uint256 projectIndex)public payable
isNotAuthor(projectIndex){Project memory project = projects[projectIndex];require( project.state!=FundraisingState.Closed,"The project can not receive funds");require(msg.value>0,"Fund value must be greater than 0"); project.author.transfer(msg.value); project.funds+= msg.value; projects[projectIndex]= project; contributions[project.id].push(Contribution(msg.sender, msg.value)); emit ProjectFunded(project.id, msg.value);}functionchangeProjectState(FundraisingState newState, uint256 projectIndex)publicisAuthor(projectIndex){Project memory project = projects[projectIndex];require(project.state!= newState,"New state must be different"); project.state= newState; projects[projectIndex]= project; emit ProjectStateChanged(project.id, newState);}}
on.Este cambio a array si me quemó las pestañas jaja, pero ahí se los dejo por si a alguno le sirve la forma en la que lo abordé. Haciendo este ejercicio me di cuenta que al igual que en Java no se puede comparar los strings con el ==, que los emit sirven para debuggear y que para ver el contenido de un array hay que crear una función.
// SPDX-License-Identifier: MITpragma solidity ^0.8.0;contract Crowdfunding{//Array of projectsCrowdfundingProject[] projects;//Mapping of donationsmapping(string=> uint256) donations;//Crowdfunding Project structure struct CrowdfundingProject{ address payable projectAddress; string projectName; uint256 fundraisingGoal; uint256 moneyReceived;ProjectState projectState;}//Enum - Project States enumProjectState{Opened,Closed}//Modifier to validate the project with given name belong to sender address modifier onlyOwner(string calldata _projectName){ bool isProjectOwner =false;for(uint i =0; i < projects.length; i++){if(keccak256(bytes(projects[i].projectName))==keccak256(bytes(_projectName))&& projects[i].projectAddress== msg.sender){ isProjectOwner =true;}}require(isProjectOwner ==true,"Only owner can do this action"); _;}//Modifier to validate the project with given name does not belong to sender address modifier notOwnProject(string calldata _projectName){ bool isOwnProject =false;for(uint i =0; i < projects.length; i++){if(keccak256(bytes(projects[i].projectName))==keccak256(bytes(_projectName))&& projects[i].projectAddress== msg.sender){ isOwnProject =true;}}require(isOwnProject ==false,"Owner cannot fund his own project"); _;}//Event when a project is funded event FundedProject( address contributor, uint256 donation
);//Event when the project state changed event ProjectStateChanged( address author,ProjectState newState
);//Function to create a projectfunctioncreateProject(string calldata projectName, uint256 _fundraisingGoal)public{CrowdfundingProject memory crowdfundingProject =CrowdfundingProject(payable(msg.sender), projectName, _fundraisingGoal,0,ProjectState.Opened); projects.push(crowdfundingProject);}//Function to fund a projectfunctionfundProject(string calldata _projectName)publicnotOwnProject(_projectName) payable{require(bytes(_projectName).length>0,"The project name is mandatory");require(msg.value>0,"The donation should be greater than zero"); uint index; bool foundProject =false;CrowdfundingProject memory currentProject;for(uint i =0; i < projects.length; i++){if(keccak256(bytes(projects[i].projectName))==keccak256(bytes(_projectName))){ currentProject = projects[i]; foundProject =true; index = i;}} emit ProjectStateChanged(msg.sender, currentProject.projectState);require(foundProject ==true,"The project was not found");require(currentProject.projectState==ProjectState.Opened,"Crodwfunding is closed");require(currentProject.moneyReceived+ msg.value<= currentProject.fundraisingGoal,"Fundarising is almost full. Fund with less money."); currentProject.projectAddress.transfer(msg.value); currentProject.moneyReceived= currentProject.moneyReceived+ msg.value;//Update array and mapping projects[index]= currentProject; donations[_projectName]= currentProject.moneyReceived; emit FundedProject(msg.sender, msg.value);}//Function to change the project statefunctionchangeProjectState(string calldata _projectName)publiconlyOwner(_projectName){ uint index;CrowdfundingProject memory currentProject;for(uint i =0; i < projects.length; i++){if(keccak256(bytes(projects[i].projectName))==keccak256(bytes(_projectName))){ currentProject = projects[i]; index = i;}}if(currentProject.projectState==ProjectState.Opened){ currentProject.projectState=ProjectState.Closed; projects[index]= currentProject;}else{ currentProject.projectState=ProjectState.Opened; projects[index]= currentProject;} emit ProjectStateChanged(msg.sender, currentProject.projectState);}//Function to get all projectsfunctiongetAllProjects()public view returns(CrowdfundingProject[] memory){return projects;}//Function to get donations for a specific projectfunctiongetDonationsForProject(string calldata _projectName)public view returns(uint256){return donations[_projectName];}}
Este modifier puede servir para que no se ponga el mismo proyecto varias veces (con el project name como identificador)
modifier distinctProjects(string _name){ bool isDistinct =true;for(uint i =0; i < projects.length; i++){if(projects[i].name== _name){ isDistinct =false;}}require(isDistinct,"This project already exists"); _;}
¡Muy bueno! Hay que tener en cuenta el consumo del gas en este tipo de bucles. Tal vez cortar el for si encuentra que el proyecto existe o verificarlo por el nombre directamente.
Arrays
los arrays son como contenedores de tipos especifico