https://www.trytoprogram.com/c-programming/c-programming-pointers/
Seção #1
Vamos aprender sobre “Ponteiros/Referências de Armazenamento” agora.
O que são eles?
Simplificando, são “ponteiros” que apontam para o próprio armazenamento de um contrato.
Bem, “apontar para o próprio armazenamento de um contrato” merece uma explicação adicional.
function setStudentData(StudentData storage studentData) public {
// código
}
Os ponteiros de armazenamento são “criados” usando a palavra-chave storage
ao lado do nome de uma variável na assinatura da função, como mostrado em studentData
acima.
Tal função só pode ser usada em uma biblioteca Solidity e não em um contrato.
Considere o código abaixo:
//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
}
}
Isso gera um erro de compilação:
É porque, por seu design, o Solidity não permite que uma função de contrato aceite quaisquer ponteiros/referências de armazenamento como seu parâmetro.
Por outro lado, o Solidity permite, por seu design, que suas bibliotecas aceitem ponteiros de armazenamento como parâmetros em suas funções.
Portanto, o código abaixo 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
}
}
Uma discussão perspicaz sobre este aspecto da linguagem está aqui, caso você queira explorar mais sobre isso.
Lembre-se, o armazenamento/variáveis em um contrato são criados apenas na implantação do contrato, na maneira como são declarados dentro do código.
Não faz sentido criar novas variáveis de armazenamento durante uma chamada de função de contrato quando tais variáveis deveriam persistir.
Existem outros aspectos também relacionados a essa característica específica do Solidity que ainda estou explorando.
Se você encontrar quaisquer fontes credíveis/relevantes onde eu possa ler mais sobre esse assunto complicado, por favor, deixe um comentário. Obrigado.
Seção #2
Considere o código de exemplo abaixo:
//SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
library StoragePointers {
struct StudentData {
uint256 id;
string name;
}
// para ler -> memória
function getStudentData(StudentData memory studentData) public pure returns (uint256){
return studentData.id;
}
// para escrever -> armazenamento
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);
// }
}
A função setStudentData()
da biblioteca StoragePointers
só aceita variável do tipo ponteiro/referência de armazenamento como seu argumento, com outro uint256 _id
.
Portanto, a função setDataOfStudents()
do contrato StorageP
gera um erro de compilação quando invocada usando o tipo struct _studentData
, porque o tipo struct _studentData
é declarado na memória.
Agora, o código revisado ao descomentar a parte comentada no contrato compila. Veja abaixo:
//SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
library StoragePointers {
struct StudentData {
uint256 id;
string name;
}
// para ler -> memória
function getStudentData(StudentData memory studentData) public pure returns (uint256){
return studentData.id;
}
// para escrever -> armazenamento
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);
}
}
Aqui, como _studentData
é uma variável de armazenamento do contrato, ela pode invocar setStudentData(_id)
.
Aprendizado:
Uma função (de uma biblioteca) só aceitará uma referência de armazenamento como parâmetro se for declarada dessa forma e essa declaração só pode acontecer em uma biblioteca, pelo design do Solidity, e não em um contrato.
Essa biblioteca pode definitivamente ser importada/reutilizada em um contrato para alcançar essa funcionalidade dentro de um contrato.
Seção #3
Você já leu sobre Passagem por Valor vs. Passagem por Referência?
Usarei essa analogia aqui.
Simplificando, Passagem por Valor = você cria outra variável atribuindo o valor da 1ª variável. Assim, há 2 cópias (variáveis) do mesmo valor.
Mudanças em qualquer um dos valores não afetarão a outra cópia.
Passagem por Referência, por outro lado = quando atribuímos a referência (a um valor em si) a outra variável, a nova variável ainda se refere/aponta apenas para o valor original.
Isso significa que ambas as variáveis de referência apontam para o mesmo valor.
E, portanto, mudanças feitas no valor acessando qualquer uma das referências refletirão claramente na outra também.
Vou abordar os 2 cenários acima usando um código de exemplo abaixo.
Cenário #1
Atribuindo uma variável de armazenamento a uma variável local/memória em uma função que acaba criando 2 cópias diferentes de um valor.
Cenário #2
Atribuindo uma variável de armazenamento a uma variável do tipo referência de armazenamento em uma função que acaba criando outra referência ao armazenamento, o que significa duas referências apontando para o mesmo valor (não duas cópias separadas).
//SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
library StoragePointers {
struct StudentData {
uint256 id;
string name;
}
// para ler -> memória
function getStudentData(StudentData memory studentData) public pure returns (uint256){
return studentData.id;
}
// para escrever -> armazenamento
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);
}
// ======================
// Analogia: Passagem 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" no `return` NÃO afeta o valor original de "id" da variável de armazenamento "_studentData"
}
function returnStorage() public view returns (uint256) {
return _studentDataSto.id;
}
// ======================
// Analogia: Passagem por REFERÊNCIA
function setDataOfStudentsUsingStorageRef(uint256 _id, string memory _name) public {
// ambos o mapeamento com índice (id) e o newStudentData apontam para o mesmo local de armazenamento
StoragePointers.StudentData storage newStudentData = studData[_id];
newStudentData.id = _id;
newStudentData.name = _name;
}
}
Cenário #1:
Após implantar o contrato, execute setDataOfStudentsStorage(uint256 _id)
fornecendo _id = 5
como argumento de entrada.
Agora, execute ambas as funções: returnMemory()
e returnStorage()
uma após a outra.
Você obterá a seguinte saída:
Consulte os comentários escritos ao lado do código acima para melhor entendimento.
A linha de código abaixo criou 2 cópias diferentes do mesmo valor de armazenamento.
_studentDataMem.id = _studentDataSto.id;
É por isso que return (_studentDataMem.id + 1);
retornou 6
enquanto return _studentDataSto.id;
retornou 5
apenas.
Cenário #2:
Execute a função setDataOfStudentsUsingStorageRef()
e insira _id = 1
e _name = Manu
.
Em seguida, verifique os valores usando o mapeamento studData[id = 1] => "Manu"
:
A linha de código abaixo cria uma nova referência de armazenamento (newStudentData
) para o mesmo valor de armazenamento que já está sendo referenciado pelo mapeamento studData[_id];
em algum local de armazenamento no armazenamento do contrato.
StoragePointers.StudentData storage newStudentData = studData[_id];
É por isso que o mapeamento fornece a saída de id = 1
e name = Manu
, pois a transação de setDataOfStudentsUsingStorageRef()
fez mudanças diretamente no armazenamento do contrato.
Ufa... Eu sei, realmente, foi muita coisa para absorver.
Eu recomendo fortemente que você execute os mesmos trechos de código que criei para sua compreensão, no Remix IDE, inserindo diferentes valores para ver as saídas.
Isso conclui a parte 2.
E, como sempre, serei grato se você puder deixar um feedback honesto/reação e/ou crítica construtiva sobre este artigo para que eu possa melhorá-lo.
Na próxima e última parte, explicarei alguns conceitos avançados de armazenamento usando o Yul e transformarei você em um profissional! :)
Até a próxima vez, boa programação (em Solidity)! :)
Bibliografia:
Artigo original publicado por Manu Kapoor. Traduzido por Paulinho Giovannini.
Latest comments (0)