Solidity tiene 2 palabras reservadas para el almacenamiento de datos: memory
y storage
. Como desarrollador es importante que conozcas las diferencias entre ellas y cuando debes usar una u otra.
De acuerdo con la documentación de Solidity, estas dos palabras clave se utilizan para referenciar tipos de datos donde…
“Los tipos complejos, es decir, los tipos que no siempre caben en 256 bits, deben manejarse con más cuidado que los tipos de valor que ya hemos visto. Dado que copiarlos
puede ser bastante costoso, debemos pensar si queremos que se almacenen en memory
-memoria- (que no es persistente) o en storage
-almacenamiento-
(donde las variables de estado son preservadas).
…
Cada tipo complejo, es decir, matrices y estructuras, tiene una anotación adicional, la “ubicación de datos”, sobre si se almacena en la memoria o en el almacenamiento.”
Ahora es importante ver dónde la Ethereum Virtual Machine (EVM) almacena datos:
“La máquina virtual Ethereum tiene tres áreas donde puede almacenar elementos.
El primero es “storage
” -almacenamiento-, donde residen todas las variables de estado del contrato. Cada contrato tiene su propio almacenamiento y es persistente entre llamadas de función y bastante costoso de usar.
El segundo es “memory
” -memoria-, se utiliza para almacenar valores temporales. Se borra entre llamadas de funciones (externas) y es más barato de usar.
El tercero es “stack”, que se utiliza para contener pequeñas variables locales. Es de uso casi gratuito, pero solo puede contener una cantidad limitada de valores.”
Más importante,
“Si, por ejemplo, se pasan tales variables en llamadas a funciones, sus datos no se copian si pueden permanecer en la memoria o en el almacenamiento.”
Aquí es donde se vuelve confuso, pero ten presente los siguientes puntos:
- Las palabras clave
storage
y memory
se utilizan para hacer referencia a datos en el almacenamiento y la memoria, respectivamente.
- El almacenamiento del contrato se asigna previamente durante la construcción del contrato y no se puede crear en la llamada de una función. Después de todo, tiene poco sentido crear una nueva variable en el almacenamiento de una función si se va a conservar.
- La memoria no se puede asignar durante la construcción del contrato, sino que se crea en la ejecución de una función. La variable de estado del contrato siempre se declara en el almacenamiento. Nuevamente, tiene poco sentido tener una variable de estado que no pueda persistir.
- Cuando asignamos una variable con referencia
memory
a partir de una variable de referencia de storage
, estamos copiando datos de la memoria al almacenamiento. No se crea ningún nuevo almacenamiento.
- Cuando asignamos una variable con referencia
storage
a partir de una variable con referencia a memory
, estamos copiando datos del almacenamiento a la memoria. Se asigna nueva memoria.
- Cuando una variable de
storage
se crea en función localmente mediante la búsqueda, simplemente hace referencia a los datos ya asignados en el almacenamiento. No se crea ningún nuevo almacenamiento.
Para recapitular, volvamos a la documentación:
"Ubicación de datos forzada:
Ubicación de datos predeterminada:
Podemos cambiar la ubicación de los datos solo para parámetros de funciones y variables locales en función. Cada vez que una referencia de storage
se envía a memory
, se realiza una copia y la modificación adicional en el objeto no se propaga de nuevo al estado del contrato. Datos con referencia memory
solo se puede “asignar” a storage
si los datos de la memoria se pueden copiar a una variable de estado preasignada.
Veamos algunos ejemplos para ilustrar mejor lo visto anteriormente.
Empecemos viendo el caso de algunos getters.
function getUsingStorage(uint _itemIdx)
public
// definir como non-view para calcular gas
// view
returns (uint)
{
Item storage item = items[_itemIdx];
return item.units;
}
function getUsingMemory(uint _itemIdx)
public
// definir como non-view para calcular gas
// view
returns (uint)
{
Item memory item = items[_itemIdx];
return item.units;
}
Ambas funciones devuelven el mismo resultado, excepto que en getUsingMemory
se crea una nueva variable y se usa más gas:
"gasUsed": 21849
"gasUsed": 22149
Por otro lado, para los setters:
function addItemUsingStorage(uint _itemIdx, uint _units)
public
// definir como non-view para calcular gas
// view
{
Item storage item = items[_itemIdx];
item.units += _units;
}
function addItemUsingMemory(uint _itemIdx, uint _units)
public
// definir como non-view para calcular gas
// view
{
Item memory item = items[_itemIdx];
item.units += _units;
}
Solo addItemUsingStorage
modifica la variable de estado (consumiendo más gas):
"gasUsed": 27053
"gasUsed": 22287
Para concluir, los puntos más importantes son:
memory
y storage
especifica a qué ubicación de datos se refiere la variable.
- No se puede crear una nueva variable con referencia a
storage
en una función. Cualquier variable con referencia a storage
en una función siempre hace referencia a un dato preasignado en el almacenamiento del contrato (variable de estado). Cualquier mutación persiste después de la llamada a la función.
- Nuevas variables con referencia a
memory
solo se pueden crear en una función. Pueden ser tipos complejos instanciados recientemente como arrays o structs (por ejemplo, a través de new int[…]) o copiados desde una variable de storage
.
- Como las referencias se pasan internamente a través de los parámetros de la función, recuerde que están predeterminadas a
memory
y, si la variable estuviera en storage
, se crearía una copia y cualquier modificación no persistiría.
💡 Para mayor referencia, visita el texto original “Ethereum Solidity: Memory vs Storage & When to Use Them”.
¿Quieres ver más aportes, preguntas y respuestas de la comunidad?