Best Practices zwischen „extern“ und „öffentlich“.

Neben dem publicModifikator führt Ethereum den externaleinen ein. Beide können außerhalb des Vertrags und innerhalb (das spätere durch this.f()-Muster) aufgerufen werden. Darüber hinaus laut den Dokumenten :

Externe Funktionen sind manchmal effizienter, wenn sie große Datenarrays empfangen.

aber es gibt keine näheren infos was das eigentlich sometimesbedeutet und ob dieser effizienzgewinn auch für interne gespräche stattfindet.

Was sind die Best Practices für die Verwendung von externalvs- publicKeywords? Gibt es Muster oder Empfehlungen?

Antworten (8)

Ein einfaches Beispiel, das diesen Effekt demonstriert, sieht so aus:

pragma solidity^0.4.12;

contract Test {
    function test(uint[20] a) public returns (uint){
         return a[10]*2;
    }

    function test2(uint[20] a) external returns (uint){
         return a[10]*2;
    }
}

Wenn wir jede Funktion aufrufen, können wir sehen, dass die publicFunktion 496 Gas verbraucht, während die externalFunktion nur 261 verbraucht.

Der Unterschied besteht darin, dass Solidity in öffentlichen Funktionen Array-Argumente sofort in den Speicher kopiert, während externe Funktionen direkt aus Aufrufdaten lesen können. Die Speicherzuweisung ist teuer, während das Lesen von Anrufdaten billig ist.

Der Grund, warum publicFunktionen alle Argumente in den Speicher schreiben müssen, liegt darin, dass öffentliche Funktionen intern aufgerufen werden können, was eigentlich ein völlig anderer Prozess als externe Aufrufe ist. Interne Aufrufe werden über Sprünge im Code ausgeführt, und Array-Argumente werden intern durch Zeiger auf den Speicher übergeben. Wenn also der Compiler den Code für eine interne Funktion generiert, erwartet diese Funktion, dass sich ihre Argumente im Speicher befinden.

Für externe Funktionen muss der Compiler keine internen Aufrufe zulassen und erlaubt es daher, Argumente direkt aus calldata zu lesen, wodurch der Kopierschritt eingespart wird.

Als Best Practices sollten Sie verwenden, externalwenn Sie davon ausgehen, dass die Funktion immer nur extern aufgerufen wird, und verwenden, publicwenn Sie die Funktion intern aufrufen müssen. Es macht fast nie Sinn, das this.f()Muster zu verwenden, da dies die CALLAusführung eines Real erfordert, was teuer ist. Außerdem wäre die Übergabe von Arrays über diese Methode weitaus teurer als die interne Übergabe.

Sie werden im Wesentlichen immer dann Leistungsvorteile sehen, externalwenn Sie eine Funktion nur extern aufrufen und viele Aufrufdaten (z. B. große Arrays) übergeben.

Beispiele zur Unterscheidung:

öffentlich - alle können darauf zugreifen

extern - Kann nicht intern, sondern nur extern aufgerufen werden

intern - nur dieser Vertrag und davon abgeleitete Verträge können zugreifen

privat - kann nur von diesem Vertrag aus aufgerufen werden

Super und eine sehr hilfreiche Antwort. Thx Tjaden!
Wenn wir eine öffentliche Schnittstelle für Verträge entwerfen und andere Personen sich auf die Schnittstelle verlassen (denken Sie an EIPs), sollten wir dann immer external?
^ Dies sollte den Solidity-Dokumenten hinzugefügt werden.
Etwas stimmt mit Ihrer Antwort nicht: joxi.ru/Drlz51Xc4MDGX2
Außerdem können externe Funktionen in abgeleiteten Verträgen nicht überschrieben werden.
@Anron Pegov Ich berichte die Gaskosten für die Hinrichtung, nicht die Gesamtkosten. dh nur die Gaskosten, die durch die tatsächliche Ausführung des Vertrags entstehen, nicht das Senden der Transaktion an ihn
Sollte in diesem Fall function name() public view returns (string)der ERC-20 Token Standard als extern deklariert werden, um etwas Gas zu sparen? Weil es so aussieht, als würde die Funktion name()nicht intern aufgerufen.
Diese Funktion hat keine Argumente, also gibt es nichts zu kopieren. Der Vorteil der Verwendung von externalover publicverblasst also.
Solidity 0.6.9 erlaubt jetzt die calldataVerwendung für jede Variable oder jeden Parameter, sogar in internalFunktionen.
Anfänger hier, danke ein paar Fragen Popup: 1) Wäre es besser, eine externalund - internalFunktion zu haben, als publicwenn Sie dann beide aufrufen müssten? 2) Wie der Kommentator oben sagte, ist dies jetzt irrelevant, da 0.6.9? 3) Verbessert das Hinzufügen viewweiterer Leistung?
@TjadenHess Würden Sie bitte die neueste Antwort unten überprüfen und erklären, ob Ihre aktuelle Erklärung noch gilt. Die Solidität ändert sich schnell und einige der Mechanismen wurden möglicherweise neu gestaltet.
Was bedeutet echter CALL?
Ich verstehe vielleicht falsch, was "interner Zugriff" bedeutet, aber warum sind Konstruktoren dann öffentlich und nicht extern? (scheint mir, als würden sie nur einmal extern aufgerufen)
@samlaf "interner Zugriff" bedeutet Aufruf innerhalb desselben Vertrags und nicht extern von einem anderen Vertrag oder einer Transaktion. Letzteres beinhaltet einen speziellen Opcode wie CALL, DELEGATECALL, etc. und ist teurer. Was Konstruktoren angeht - sie sind beides nicht, das Anwenden von Sichtbarkeit auf Konstruktoren dehnt das Konzept irgendwie aus und passt nicht perfekt , weshalb es in 0.7.0 aus der Sprache entfernt wurde.
@Maxareo Ja, diese Antwort ist richtig, aber veraltet. Bei der Unterscheidung geht es wirklich um memoryvs calldata, die nicht mehr an die Sichtbarkeit gebunden ist. Auf dem neuesten Compiler externalund publicgeben Ihnen genau den gleichen Bytecode, wenn dies das einzige ist, was sich unterscheidet, wie in meiner Antwort in "Reduziert die Verwendung von extern über öffentlich in einer Bibliothek die Gaskosten?" .

Update für Solidity ^0.8

Tjadens Antwort ist großartig, aber ich denke, dass sie ein Update für die neuesten Versionen von Solidity verdient. Sein Code-Snippet lässt sich nicht mehr kompilieren. Sie erhalten jetzt diesen Fehler:

Datenposition muss memoryoder calldatafür Parameter in Funktion sein, aber es wurde keine angegeben.

Das liegt an der neuen Anforderung, bei der Verwendung von Referenztypen wie Arrays explizit zu sein . Auch memoryund calldatasind jetzt unabhängig von ihrer Sichtbarkeit in allen Funktionen erlaubt.

Eine Umschreibung würde in etwa so aussehen:

pragma solidity >=0.8.13;

contract ExternalPublicTest {
    function foo(uint[20] memory a) public pure returns (uint){
         return a[10]*2;
    }

    function bar(uint[20] calldata a) external pure returns (uint){
         return a[10]*2;
    }    
}

Als Randnotiz können Sie calldataVariablen in dekodieren, memoryaber nicht umgekehrt.

Umstrukturierung der obigen Antwort zur Verdeutlichung:

pragma solidity^0.4.12;

contract Test {

    /*
    Cost: 496 Gas 
    This can be called internally or externally
    Since internal calls expects function arguements to be allocated to memory, solidity immediately
    copies array arguments to memory (This is what cost the additional gas.) 
    */
    function test(uint[20] a) public returns (uint) {
        return a[10] * 2;
    }

    /*
    Cost: Gas 261
    Doesnt allow internal calls, read directly from CALLDATA saving on the copying step(memory allocation).
    */
    function test(uint[20] a) external returns (uint) {
        return a[10] * 2;
    }


    /*
     Executed via JUMPs in code, array arguments are passed internally by pointers to memmory
      Function expects argument to be located in memory. 
     */
    function test(uint[20] a) internal returns (uint) {
        return a[10] * 2;
    }
}
  • Interne Aufrufe sind am billigsten, da sie über Code JUMP ausgeführt werden und Zeiger an den Speicher übergeben.
  • Interne Aufrufe öffentlicher Funktionen sind teuer, da interne Funktionsaufrufe erwarten, dass die Argumente dem Speicher zugewiesen werden, da die öffentliche Funktion nicht weiß, ob der Aufruf extern oder intern ist, kopiert sie die Argumente in den Speicher und ist daher teurer
  • Wenn Sie wissen, dass die Funktion nur extern aufgerufen wird, verwenden Sie sieexternal

  • Es macht fast nie Sinn, das this.f()-Muster zu verwenden, da dies die Ausführung eines echten CALL erfordert, der teuer ist.

Ich habe gerade das Ergebnis mit dem neuesten Compiler überprüft. Es scheint, dass die Reduzierung der Gaskosten durch Speicher vs. Anrufdaten verursacht wird, dh ob eine Funktion extern oder öffentlich ist, spielt keine Rolle. Entscheidend ist, ob das Eingabearray Speicher oder Aufrufdaten ist.

// SPDX-License-Identifier: MIT

pragma solidity 0.8.4;

contract ExternalPublicTest {
    function test(uint[20] memory a) public pure returns (uint){
         return a[10]*2;
    }

    function test2(uint[20] calldata a) public pure returns (uint){
         return a[10]*2;
    }    
}

Einfache Antwort

Das publicist gleichbedeutend mit externalplus internal.

Mit anderen Worten, sowohl publicals auch externalkönnen von außerhalb Ihres Vertrags aufgerufen werden (wie MetaMask), aber von diesen beiden publickönnen nur von anderen Funktionen innerhalb Ihres Vertrags aufgerufen werden.

Da publicmehr Zugriff als gewährt externalwird, ist die allgemein bewährte Methode, zu bevorzugen external. Dann können Sie einen Wechsel zu erwägen, publicwenn Sie die Auswirkungen auf Sicherheit und Design vollständig verstehen.

Beachten Sie auch, dass externaldies vertragsextern bedeutet, nicht das Netzwerk. Sowohl externalals publicauch Funktionen können von einem anderen Vertrag innerhalb derselben Transaktion aufgerufen werden. Aus dem Dokument:

Externe Funktionen sind Teil der Vertragsschnittstelle, können also aus anderen Verträgen und über Transaktionen aufgerufen werden.

Verhindert also externalnicht ablaufinvariante Aufrufe der Funktion. Dafür kann man den ReentrancyGuard von OpenZeppelin verwenden , aber das kostet Benzin.

Ich habe es mit solidity 0.8.14 versucht

pragma solidity 0.8.14;
contract Test {
   function test(uint[2] calldata a) external pure returns (uint){
     return a[1]*2;
   }
}

Wie Sie sehen können, ist der verwendete Sichtbarkeitsbezeichner der Funktion extern

pragma solidity 0.8.14;
contract Test {
   function test(uint[2] calldata a) public pure returns (uint){
     return a[1]*2;
   }
}

in diesem Fall ist der verwendete Funktionssichtbarkeitsbezeichner öffentlich

In beiden Fällen beträgt das verbrauchte Gas 22116 (im Remix), sodass beim Gasverbrauch kein Unterschied zwischen extern und öffentlich besteht

Ein weiterer Test:

pragma solidity 0.8.14;
contract Test1 {
  uint a;
  function test() external{
       a = 1;
  }
}

In diesem Fall habe ich eine Transaktion getestet, die den Vertragsstatus des Vertrags ändert, und die Ausführungskosten betragen 43300

pragma solidity 0.8.14;
contract Test2 {
  uint a;    
  function test() public{
       a = 1;
  }
}

Wenn ich dieselbe Funktion ausführe, aber von extern zu öffentlich wechsele, habe ich dasselbe Ergebnis erhalten: Die Ausführungskosten betragen 43300

Also beim Spritverbrauch sehe ich keinen Unterschied

Für die neuesten Compiler ist eine externe Funktion also eine öffentliche Funktion, die ihre Argumente dazu zwingt, sich in Calldata zu befinden, während eine öffentliche Funktion eine Funktion ist, die von außen sichtbar ist und es ihren Argumenten ermöglicht, sich sowohl im Speicher als auch in Calldata zu befinden.

Bei externen Anrufen stehen die Daten immer in den Anrufdaten, auch wenn die Funktion öffentlich ist. In diesem Fall findet ein Konvertierungsschritt von den Anrufdaten zum Speicher statt.
Es macht auch keinen Sinn, calldata in einer Funktion anzugeben, die zusätzliche interne Funktionen aufruft – es sei denn, Sie möchten schreibgeschütztes Linting erzwingen –, da sie ausnahmslos in den Speicher kopiert werden, bevor sie an die interne Funktion übergeben werden.