Effizienter Ansatz zum Löschen von Elementen aus dem Array in Solidity

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 IDder 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 assetsRessourcen zuweisen (wäre sehr verschwenderisch, da Sie viele Ressourcen zur Verfügung haben) VS. assetsjedes 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 mappingfür jeden Benutzer eine speichern könnte, die angibt, welches Asset er hat, aber Sie können keine mappingvon einer Funktion zurückgeben, und ich kenne die Benchmarks von viewFunktionen und das "Brute-Forcing" aller Assets nicht für jeden Benutzer verfügbar ist, kann eine Menge Zeit in Anspruch nehmen, nehme ich an.

Hier ist eine Methode zum Löschen eines Array-Elements an einem bestimmten Index ethereum.stackexchange.com/questions/1527/…
Ich kenne diese Threads, ich habe auch ihre Methode vorgestellt. Obwohl meine Situation etwas anders ist und ich nach der Effizienz frage.
Der Titel sollte ungefähr so ​​lauten: Effizienter Ansatz zum Löschen von Elementen in Solidity-Arrays mit dynamischer Größe
@RomanFrolov Stimmt, bearbeitet.

Antworten (3)

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.

@random Hilft das?
Nein, es setzt nur die Gaseinstellung von Nicht-Null-Speicherwerten wieder auf Null zurück. Es ist ein wenig billiger, als explizit zu sagen, Array zu löschen [Index] Davon abgesehen gibt es in diesem Thread keine Antworten zur Effizienz, also muss ich wohl selbst recherchieren.
Dies ist eine unglaublich einfache Lösung für meinen Anwendungsfall. Danke.
Müssen Sie den Eintrag auch aus indexOfAsset entfernen?
Wie funktioniert es mit dem ersten Element?
Das Entfernen von indexOfAssethängt von Ihrem Anwendungsfall ab. Für meinen Anwendungsfall (NFTs, die niemals zerstört werden) removeAssetFromArrayfolgt auf a immer a, addAssetToArraysodass kein Entfernen aus erforderlich ist indexOfAsset.
Das Entfernen des ersten Elements ist dasselbe wie das Entfernen eines Assets: Das erste Element wird durch das letzte Element überschrieben und dann wird das letzte Element gelöscht.

Im Allgemeinen durch Verschieben des letzten Elements in der Liste in die zu löschende Zeile.

Arbeiten vom Benutzer structim 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.

So erstellen Sie ein Speicher-Array mit vielen Elementen:pragma solidity ^0.5.1; contract S {uint[] a;constructor() public {a.length = 2**20;}}
Looping ist fast immer falsch. Basierend auf Ihrer anderen Notiz haben Sie vielleicht einen anderen Anwendungsfall, vielleicht können Sie eine separate Frage öffnen.