https://www.trytoprogram.com/c-programming/c-programming-pointers/
Sección #1
Vamos a aprender sobre los 'Punteros/Referencias de Almacenamiento' ahora.
¿Qué son?
Simplificando, son 'punteros' que señalan al propio almacenamiento de un contrato.
Bien, 'apuntar al propio almacenamiento de un contrato' merece una explicación adicional.
function setStudentData(StudentData storage studentData) public {
// código
}
Los punteros de almacenamiento se 'crean' utilizando la palabra clave '
storage
junto al nombre de una variable en la firma de la función, como se muestra en studentData
arriba.
Esa función solo puede ser utilizada en una biblioteca de Solidity y no en un contrato.
Considera el código a continuación:
//SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
contract StoragePointers {
struct StudentData {
uint256 id;
string name;
}
function setStudentData(StudentData storage studentData) public {
// código
}
}
Esto genera un error de compilación:
Es porque, por su diseño, Solidity no permite que una función de contrato acepte ningún puntero/referencia de almacenamiento como su parámetro.
En cambio, Solidity permite, por su diseño, que sus bibliotecas acepten punteros de almacenamiento como parámetros en sus funciones.
Por lo tanto, el código a continuación compila.
//SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
library StoragePointers {
struct StudentData {
uint256 id;
string name;
}
function setStudentData(StudentData storage studentData) public {
// código
}
}
Una discusión perspicaz sobre este aspecto del lenguaje se encuentra aquí, en caso de que desee explorar más sobre esto.
Recuerda que el almacenamiento/variables en un contrato se crean únicamente en la implementación del contrato, de la manera en que se declaran en el código.
No tiene sentido crear nuevas variables de almacenamiento durante una llamada de función de contrato cuando esas variables deberían persistir.
Existen otros aspectos relacionados con esta característica específica de Solidity que aún estoy explorando.
Si encuentras fuentes creíbles y relevantes donde pueda obtener más información sobre este tema complejo, por favor, deja un comentario. Gracias.
Sección #2
Considera el código de ejemplo a continuación:
//SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
library StoragePointers {
struct StudentData {
uint256 id;
string name;
}
// para leer -> memória
function getStudentData(StudentData memory studentData) public pure returns (uint256){
return studentData.id;
}
// para escribir -> almacenamiento
function setStudentData(StudentData storage studentData, uint256 _id) public {
studentData.id = _id;
}
}
contract StorageP {
using StoragePointers for *;
// StoragePointers.StudentData _studentData;
function setDataOfStudents(uint256 _id) public pure {
StoragePointers.StudentData memory _studentData;
_studentData.setStudentData(_id);
}
// function setDataOfStudents(uint256 _id) public {
// _studentData.setStudentData(_id);
// }
}
La función setStudentData()
de la biblioteca StoragePointers
solo acepta una variable de tipo puntero/referencia de almacenamiento como su argumento, junto con otro uint256 _id
.
Por lo tanto, la función setDataOfStudents()
del contrato StorageP
generará un error de compilación cuando se invoque utilizando el tipo struct _studentData
, ya que el tipo struct _studentData
se declara en la memoria.
Ahora, el código revisado al descomentar la parte comentada en el contrato de hecho compila. Velo a continuación:
//SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
library StoragePointers {
struct StudentData {
uint256 id;
string name;
}
// para leer -> memória
function getStudentData(StudentData memory studentData) public pure returns (uint256){
return studentData.id;
}
// para escribir -> almacenamiento
function setStudentData(StudentData storage studentData, uint256 _id) public {
studentData.id = _id;
}
}
contract StorageP {
using StoragePointers for *;
StoragePointers.StudentData _studentData;
// function setDataOfStudents(uint256 _id) public pure {
// StoragePointers.StudentData memory _studentData;
// _studentData.setStudentData(_id);
// }
function setDataOfStudents(uint256 _id) public {
_studentData.setStudentData(_id);
}
}
Aquí, dado que _studentData
es una variable de almacenamiento del contrato, puede invocar setStudentData(_id)
.
Aprendizaje:
Una función (de una biblioteca) sólo aceptará una referencia de almacenamiento como parámetro si se declara de esa manera, y esta declaración solo puede ocurrir en una biblioteca, según el diseño de Solidity, y no en un contrato.
Esta biblioteca puede ser importada y reutilizada en un contrato para lograr esta funcionalidad dentro del contrato.
Sección #3
¿Has leído sobre Pasar por Valor vs. Pasaje por referencia?
Usaré esta analogía aquí.
En términos simples, Paso por Valor significa que creas otra variable asignando el valor de la primera variable. De esta manera, tienes 2 copias (variables) del mismo valor.
Los cambios en cualquiera de los valores no afectarán a la otra copia.
Paso por Referencia, en su contraparte, implica que cuando asignamos la referencia (al valor en sí) a otra variable, la nueva variable aún se refiere/apunta sólo al valor original.
Esto significa que ambas variables de referencia apuntan al mismo valor.
Y, por lo tanto, los cambios realizados en el valor al acceder a cualquiera de las referencias se reflejarán claramente en la otra también.
Abordaré los 2 escenarios anteriores utilizando un código de ejemplo a continuación.
Escenario #1
Al asignar una variable de almacenamiento a una variable local/en memoria en una función, esto hace que se creen dos copias diferentes de un valor.
Escenario #2
Al asignar una variable de almacenamiento a una variable de tipo referencia de almacenamiento en una función, esto hace que se cree otra referencia al almacenamiento, lo que significa que do
//SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
library StoragePointers {
struct StudentData {
uint256 id;
string name;
}
// para leer -> memória
function getStudentData(StudentData memory studentData) public pure returns (uint256){
return studentData.id;
}
// para escribir -> almacenamiento
function setStudentData(StudentData storage studentData, uint256 _id) public {
studentData.id = _id;
}
}
contract StorageP {
using StoragePointers for *;
StoragePointers.StudentData _studentDataSto;
// variável de armazenamento: mapeamento
// uint256: 'id' aponta para um estudante específico
mapping(uint256 => StoragePointers.StudentData) public studData;
function setDataOfStudentsStorage(uint256 _id) public {
_studentDataSto.setStudentData(_id);
}
// ======================
// Analogía: Passa por VALOR
function returnMemory() public view returns (uint256) {
StoragePointers.StudentData memory _studentDataMem;
_studentDataMem.id = _studentDataSto.id; // 2 cópias criadas
return (_studentDataMem.id + 1);
// "+1" en `return` NO afecta el valor original del "id" de la variable de almacenamiento "_studentData"
}
function returnStorage() public view returns (uint256) {
return _studentDataSto.id;
}
// ======================
// Analogía: Pasa por REFERENCIA
function setDataOfStudentsUsingStorageRef(uint256 _id, string memory _name) public {
// ambos el mapeo con el índice (id) y el newStudentData apuntan al mismo lugar de almacenamiento
StoragePointers.StudentData storage newStudentData = studData[_id];
newStudentData.id = _id;
newStudentData.name = _name;
}
}
Escenario #1
Después de desplegar el contrato, ejecuta setDataOfStudentsStorage(uint256 _id)
proporcionando _id = 5
como argumento de entrada.
Ahora, ejecuta ambas las funciones: returnMemory()
y returnStorage()
una tras otra.
Obtendrás la siguiente salida:
Consulta los comentarios escritos junto al código anterior para una mejor comprensión.
La línea de código a continuación creó 2 copias diferentes del mismo valor de almacenamiento.
_studentDataMem.id = _studentDataSto.id
Es por eso que return (_studentDataMem.id + 1);
devolvió 6, mientras que return _studentDataSto.id;
solo devolvió 5.
Escenario #2
Ejecuta la función setDataOfStudentsUsingStorageRef()
e ingrese _id = 1
y _name = Manu
.
Luego, verifica los valores utilizando el mapeo studData[id = 1] => "Manu"
:
La línea de código a continuación crea una nueva referencia de almacenamiento (newStudentData
) para el mismo valor de almacenamiento que ya está siendo referenciado por el mapeo studData[_id]
en algún lugar de almacenamiento en el contrato.
StoragePointers.StudentData storage newStudentData = studData[_id];
Exacto, es por eso que el mapeo proporciona la salida de id = 1
y name = Manu
, ya que la transacción de setDataOfStudentsUsingStorageRef()
realizó cambios directamente en el almacenamiento del contrato.
Uf... lo sé, realmente fue mucho para asimilar.
Recomiendo encarecidamente que ejecutes los mismos fragmentos de código que he creado para tu comprensión en el Remix IDE, ingresando diferentes valores para ver las salidas.
Esto concluye la parte 2.
Y, como siempre, estaré agradecida si puedes proporcionar una retroalimentación honesta, reacción y/o crítica constructiva sobre este artículo para que pueda mejorarlo.
En la próxima y última parte, explicaré algunos conceptos avanzados de almacenamiento utilizando Yul y te convertiré en un profesional.
Hasta la próxima, ¡feliz programación (en Solidity)! :)
Bibliografía:
https://docs.soliditylang.org/en/v0.8.15/internals/layout_in_storage.html?source=post_page-----84415fb11e60--------------------------------
https://betterprogramming.pub/all-about-solidity-data-locations-part-i-storage-e50604bfc1ad?source=post_page-----84415fb11e60--------------------------------
https://coinsbench.com/storage-pointers-in-solidity-4a2d7c55a054#:~:text=Photo%20by%20Jefferson%20Santos%20on,a%20specific%20value%20or%20variable
https://forum.soliditylang.org/t/storage-across-contracts/352?source=post_page-----84415fb11e60--------------------------------
Artículo original publicado por Manu Kapoor. Traducido por Juliana Cabeza.
Discussion (0)