¿Cuál es el patrón adecuado para usar una variable temporal y garantizar que se almacene?

Un contrato tiene una variable de estado, que es compleja. Para el ejemplo, digamos que es un mapeo de estructuras. Dentro de un método, quiero acceder a eso y almacenar los cambios. Al igual que:

contract FooManager {
  struct Foo {
    uint expiresAt;
    uint balance;
  }
  mapping(address => Foo) public fooIndex;

  public function claimFrom(address minter) public payable {
    require(fooIndex[minter].balance < amount);

    fooIndex[minter].balance += msg.value;
    fooIndex[minter].expiresAt += 1000;
  }
}

En este ejemplo simple, la repetición de fooIndex[minter].methodsigue siendo manejable y legible. Pero con, por ejemplo, asignaciones de niños, digamos la Fooestructura, rápidamente se vuelve enredado y feo. fooIndex[minter].claimants[msg.sender].balance = msg.valueno hay excepciones allí.

¿ Cuál es el patrón común a DRYesto? Puedo crear una variable de memoria, como a continuación, pero eso no se almacena. El gas adicional para asignar memoria puede convertirse en un problema si se repite este patrón, pero probablemente sea insignificante en la mayoría de los casos.

  public function claimFrom(address minter) public payable {
    Foo memory thisFoo;
    thisFoo = fooIndex[minter];
    require(thisFoo.balance < amount);

    thisFoo.balance += msg.value;
    thisFoo.expiresAt += 1000;
  }
}

He visto algunos ejemplos en los que, al final, la variable de memoria se vuelve a asignar a la variable de almacenamiento. Al igual que:

  public function claimFrom(address minter) public payable {
    Foo memory thisFoo;
    thisFoo = fooIndex[minter];
    require(thisFoo.balance < amount);

    thisFoo.balance += msg.value;
    thisFoo.expiresAt += 1000;

    fooIndex[minter] = thisFoo;
  }

Funciona, pero me parece torpe: asignar algo incorrecto u olvidar reasignar es un error obvio. También me parece un desperdicio, especialmente en el caso de estructuras grandes, en las que solo se deben anular los atributos sucios (cambiados), y no toda la estructura; de nuevo, esto es más obvio en situaciones en las que una estructura contiene asignaciones de niños con otras estructuras, etc.: un árbol. Sin embargo, no estoy seguro de si el compilador puede y optimizará esto.

¿Me perdí una construcción de lenguaje obvia? ¿Entendí mal el storagey memorypor completo?

Respuestas (2)

El punto es que es más barato usar variables de memoria para elaboración que variables de almacenamiento.

El enfoque para usar una variable de memoria temporal es eficiente y limpio, solo haga dos cálculos para comprender si es conveniente en su caso particular.

Quiero decir: si solo agrega un valor a la variable de almacenamiento y eso es todo, se puede evaluar, puede ser que no sea el caso; por el contrario, en el caso de que acceda diez veces a la variable temporal y luego almacene el valor en la variable de almacenamiento, no hay dudas.

Evalúa tu necesidad: cuanto más limpio es más conveniente.

(Puede encontrar el costo de la gasolina para acceder y crear variables temporales en el papel amarillo o en otro lugar muy fácilmente)

Al crear una variable en la memoria y asignarle su variable de almacenamiento, está copiando toda la estructura en la memoria. Si su estructura tiene muchos campos y solo usa algunos de ellos, está desperdiciando combustible.

function claimFrom(address minter) public payable {
    Foo memory thisFoo;
    thisFoo = fooIndex[minter];
    require(thisFoo.balance < amount);

    thisFoo.balance += msg.value;
    thisFoo.expiresAt += 1000;
  }
}

Además, esta es una copia , si la modifica, esos cambios se descartarán a menos que la almacene nuevamente.

Si solo usa algunos campos y necesita actualizar esos campos, probablemente sea mejor si los asigna a una referencia de almacenamiento.

function claimFrom(address minter) public payable {
    // This will create a variable reference to storage
    Foo storage thisFoo = fooIndex[minter];
    require(thisFoo.balance < amount);

    thisFoo.balance += msg.value;
    thisFoo.expiresAt += 1000;
  }
}

Dado que la variable es una referencia al almacenamiento, los cambios en ella se reflejan inmediatamente en el almacenamiento.

Un caso de uso en el que la asignación a la memoria tiene sentido es si su función es compleja y el uso de más variables provoca el mensaje de error "Stack too deep" y no desea actualizar el almacenamiento.

function calcBalanceFrom(address minter, uint value) public payable {
    Foo memory thisFoo = fooIndex[minter];

    thisFoo.balance += (msg.value + thisFoo.value) / value / 2; // complex formula
    thisFoo.expiresAt += 1000 + (thisFoo.balance / 10**12);

    emit NewBalance(thisFoo.balance, thisFoo.expiresAt);
  }
}

Todo esto solo se aplica cuando las optimizaciones del compilador están deshabilitadas. Cuando las optimizaciones están habilitadas, el compilador puede simplificar algunas de estas operaciones (o no). Con las optimizaciones habilitadas, es mejor probar varios enfoques.