Ich möchte für jeden Benutzer ein Array von gehaltenen Assets aufbewahren (jedes Asset hat eine ID).
Meine bisherige Lösung ist:
struct User {
uint userId;
uint[] assets;
}
Für jeden asset
, den der Benutzer hält, drücke ich auf das Array des Benutzers die ID
der asset
.
Ich möchte dem Benutzer die Möglichkeit geben, eine asset
.
Was wäre der effizienteste Ansatz dafür?
Jedem Benutzer alle verfügbaren assets
Ressourcen zuweisen (wäre sehr verschwenderisch, da Sie viele Ressourcen zur Verfügung haben) VS. assets
jedes Mal, wenn er eine löschen möchte, über alle seine iterieren asset
, sie im Array finden, sie dann daraus löschen und das gesamte Array entsprechend verschieben - außerdem ist der Code dafür irgendwie abscheulich:
function deleteAsset(uint assetId) external returns(uint) {
bool assetFound = false;
// Check if the asset found is the last asset (or we go out of boundaries)
if (allUsers[msg.sender].assets[allUsers[msg.sender].assets.length - 1] == assetId){
assetFound = true;
}
else{
// Iterate over all user assets and find its index
for (uint i = 0; i < allUsers[msg.sender].assets.length - 1; i++) {
if (!assetFound && allUsers[msg.sender].assets[i] == assetId)
assetFound = true;
if(assetFound)
allUsers[msg.sender].assets[i] = allUsers[msg.sender].assets[i + 1];
}
}
if (assetFound){
delete allUsers[msg.sender].assets[allUsers[msg.sender].assets.length - 1];
allUsers[msg.sender].assets.length--;
}
}
Es wäre viel einfacher, wenn ich mapping
für jeden Benutzer eine speichern könnte, die angibt, welches Asset er hat, aber Sie können keine mapping
von einer Funktion zurückgeben, und ich kenne die Benchmarks von view
Funktionen und das "Brute-Forcing" aller Assets nicht für jeden Benutzer verfügbar ist, kann eine Menge Zeit in Anspruch nehmen, nehme ich an.
Quelle: https://github.com/su-squares/ethereum-contract/blob/master/contracts/SuNFT.sol
Bitte schön:
Algorithmus:
uint[] assets;
mapping(uint=>uint) indexOfAsset;
function removeAssetFromArray(uint _assetToDelete) {
uint index = indexOfAsset[_assetToDelete];
if (!index) return;
if (assets.length > 1) {
assets[index] = assets[assets.length-1];
}
assets.length--; // Implicitly recovers gas from last element storage
}
Vorbehalt: Dieser Ansatz geht von einer geordneten Menge aus, nicht von einem Array. Mit anderen Worten, es wird davon ausgegangen, dass das Array keine doppelten Elemente enthält.
indexOfAsset
hängt von Ihrem Anwendungsfall ab. Für meinen Anwendungsfall (NFTs, die niemals zerstört werden) removeAssetFromArray
folgt auf a immer a, addAssetToArray
sodass kein Entfernen aus erforderlich ist indexOfAsset
.Im Allgemeinen durch Verschieben des letzten Elements in der Liste in die zu löschende Zeile.
Arbeiten vom Benutzer struct
im OP-Code.
pragma solidity 0.5.1;
contract DeleteUser {
struct UserStruct {
uint userId;
uint[] assets;
}
mapping(address => UserStruct) public userStructs;
function deleteUserAsset(address user, uint assetIndex, uint asset) public {
UserStruct storage u = userStructs[user];
require(u.assets.length > assetIndex);
require(u.assets[assetIndex] == asset); // this is a sanity check in case the list was re-ordered
u.assets[assetIndex] = u.assets[u.assets.length-1];
u.assets.length--;
}
}
Die Plausibilitätsprüfung ist nicht unbedingt erforderlich, da die Frage formuliert ist, aber die Funktion verlässt sich darauf, dass der Aufrufer weiß, welche Zeile gelöscht werden soll. Dies könnte ein echtes Problem sein, wenn die Liste durch den Aufruf der deleteUserAsset()
Funktion neu organisiert wird. Eine einfache Lösung besteht darin, diese Überprüfung hinzuzufügen, um auf der sicheren Seite zu sein.
Noch besser ist es meiner Meinung nach, sich nicht darauf zu verlassen, dass der Anrufer dies weiß. Dazu bräuchten wir eine Suche in einem Schritt, um die Zeile zu finden, in der ein bestimmter Schlüssel lebt. Es wird ein weiteres 32-Byte-Wort benötigen, um das zu speichern.
pragma solidity 0.5.1;
contract DeleteUser {
struct UserStruct {
bytes32[] assets;
mapping(bytes32 => uint) assetPointers;
}
mapping(address => UserStruct) userStructs;
function isUserAsset(address user, bytes32 assetId) public view returns(bool isIndeed) {
if(userStructs[user].assets.length == 0) return false;
return userStructs[user].assets[userStructs[user].assetPointers[assetId]] == assetId;
}
function deleteUserAsset(address user, bytes32 assetId) public {
UserStruct storage u = userStructs[user];
require(isUserAsset(user, assetId));
uint rowToDelete = u.assetPointers[assetId];
u.assets[rowToDelete] = u.assets[u.assets.length-1];
u.assets.length--;
}
}
Ich hoffe es hilft.
Für den Anfang konnte ich kein Speicherarray mit mehr als 100 Elementen in einem Konstruktor erstellen. Ebenso gehe ich davon aus, dass das Überschleifen eines auch fehlschlagen wird, wenn es zu groß wird.
Die Ergebnisse sind klar, das Verschieben von Indizes nach dem Löschen eines Elements aus einem dynamischen Array ist eine schreckliche Idee, und stattdessen sollten 0-Werte auf dem Client gefiltert werden.
Durchlaufen eines Speicherarrays mit 10 Einträgen und Löschen des LETZTEN Elements
transaction cost 20514 gas
Durchlaufen eines Speicherarrays mit 100 Einträgen und Löschen des LETZTEN Elements
transaction cost 43349 gas
Schleife durch ein Speicherarray mit 10 Einträgen und Löschen des MIDDLE-Elements
transaction cost 44762 gas
Durchlaufen eines Speicherarrays mit 100 Einträgen und Löschen des MIDDLE-Elements
transaction cost 340387 gas
Diese Gaskostenerhöhung ist einfach nicht zu rechtfertigen.
pragma solidity ^0.5.1; contract S {uint[] a;constructor() public {a.length = 2**20;}}
Karan Kurbur
Ismael
Willkürlich
Roman Frolov
Willkürlich