«Внешние» и «общедоступные» лучшие практики

Помимо publicмодификатора Ethereum вводит еще externalодин. Оба могут быть вызваны вне контракта и внутри (более поздний по шаблону this.f()). Более того, согласно документам :

Внешние функции иногда более эффективны, когда они получают большие массивы данных.

но нет дополнительной информации о том, что на самом деле sometimesозначает и сохраняется ли это повышение эффективности также для внутренних вызовов.

Каковы наилучшие методы использования ключевого слова externalvs public? Есть ли схемы или рекомендации?

Ответы (8)

Простой пример, демонстрирующий этот эффект, выглядит так:

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;
    }
}

Вызывая каждую функцию, мы видим, что publicфункция использует 496 газа, а externalфункция использует только 261.

Разница в том, что в публичных функциях Solidity сразу копирует аргументы массива в память, а внешние функции могут читать напрямую из calldata. Выделение памяти обходится дорого, тогда как чтение из calldata обходится дешево.

Причина, по которой publicфункции должны записывать все аргументы в память, заключается в том, что общедоступные функции могут вызываться внутри, что на самом деле является совершенно другим процессом, чем внешние вызовы. Внутренние вызовы выполняются через переходы в коде, а аргументы массива передаются внутри с помощью указателей на память. Таким образом, когда компилятор генерирует код для внутренней функции, эта функция ожидает, что ее аргументы будут расположены в памяти.

Для внешних функций компилятору не нужно разрешать внутренние вызовы, поэтому он позволяет считывать аргументы непосредственно из данных вызова, экономя шаг копирования.

Что касается лучших практик, вы должны использовать external, если вы ожидаете, что функция будет когда-либо вызываться только извне, и использовать, publicесли вам нужно вызвать функцию внутри. Практически никогда не имеет смысла использовать this.f()шаблон, так как для этого требуется выполнение реального CALL, что дорого. Кроме того, передача массивов с помощью этого метода будет намного дороже, чем передача их внутри.

По сути, вы увидите преимущества в производительности externalкаждый раз, когда вы вызываете функцию только извне и передаете много данных вызова (например, большие массивы).

Примеры для дифференциации:

общедоступный - все могут получить доступ

external - не может быть доступен внутри, только снаружи

внутренний - только этот контракт и контракты, вытекающие из него, могут получить доступ

частный - доступен только из этого контракта

Отличный и очень полезный ответ. Спасибо, Тьяден!
Если мы разрабатываем общедоступный интерфейс для контрактов, и другие люди будут полагаться на интерфейс (например, EIP), то должны ли мы всегда использовать external?
^ Это следует добавить в документы Solidity.
Что-то не так с вашим ответом: joxi.ru/Drlz51Xc4MDGX2
Кроме того, внешние функции нельзя переопределить в производных контрактах.
@Анрон Пегов Я сообщаю о стоимости газа для исполнения, а не об общей сумме. т.е. только стоимость газа, понесенная при фактическом выполнении контракта, а не при отправке ему транзакции
Должен ли в этом случае function name() public view returns (string)стандарт токенов ERC-20 быть объявлен внешним для экономии газа? Потому что кажется, что функция name()не вызывается внутри.
У этой функции нет аргументов, поэтому копировать нечего. Таким образом, преимущество использования externalнад publicисчезает.
Solidity 0.6.9 теперь позволяет calldataиспользовать любую переменную или параметр, даже в internalфункциях.
Новичок здесь, спасибо за несколько всплывающих вопросов: 1) Было бы лучше иметь функцию externalи internal, чем publicесли бы вам нужно было вызывать оба? 2) Как сказал комментатор выше, теперь это не имеет значения, так как 0.6.9? 3) Улучшает ли добавление viewпроизводительность?
@TjadenHess Не могли бы вы проверить последний ответ ниже и помочь объяснить, остается ли в силе ваше текущее объяснение. Solidity быстро меняется, и некоторые механизмы могли быть переработаны.
Что означает настоящий ЗВОНОК?
Возможно, я неправильно понимаю, что означает «внутренний доступ», но почему тогда конструкторы являются общедоступными, а не внешними? (мне кажется, что они называются только один раз, внешне)
@samlaf «Доступ изнутри» означает вызов из того же контракта, а не извне из другого контракта или транзакции. Последний включает в себя специальный код операции, такой как CALL, и т. д DELEGATECALL., и стоит дороже. Что касается конструкторов - их нет, применение видимости к конструкторам несколько расширяет концепцию и не совсем подходит , поэтому она была удалена из языка в 0.7.0.
@Maxareo Да, этот ответ правильный, но устаревший. Различие действительно касается memoryvs calldata, которое больше не связано с видимостью. В последнем компиляторе externalи publicдаст вам тот же точный байт-код, если это единственное, что отличается, как показано в моем ответе: «Снижает ли использование в библиотеке внешний над общедоступным какие-либо затраты на газ?» .

Обновление для Solidity ^0.8

Ответ Тьядена великолепен, но я думаю, что он заслуживает обновления для последних версий Solidity. Его фрагмент кода больше не компилируется. Вы получаете эту ошибку сейчас:

Местоположение данных должно быть memoryили calldataдля параметра в функции, но ничего не было указано.

Это связано с новым требованием быть явным при использовании ссылочных типов , таких как массивы. Также memoryи calldataтеперь разрешены во всех функциях независимо от их видимости.

Переписывание будет выглядеть примерно так:

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;
    }    
}

В качестве примечания, вы можете декодировать calldataпеременные в , memoryно не наоборот.

Реструктуризация ответа выше для ясности:

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;
    }
}
  • Внутренние вызовы самые дешевые, так как выполняются через код JUMP, передавая указатели на память.
  • Внутренние вызовы общедоступных функций являются дорогостоящими, поскольку внутренние вызовы функций предполагают, что аргументы будут выделены в память, поскольку общедоступная функция не знает, является ли вызов внешним или внутренним, она копирует аргументы в память и, следовательно, является более дорогостоящей.
  • Если вы знаете, что функция будет вызываться только извне, используйтеexternal

  • Почти никогда не имеет смысла использовать шаблон this.f(), так как для этого требуется выполнение реального CALL, что дорого.

Только что проверил результат с последним компилятором. Кажется, что снижение стоимости газа вызвано памятью против calldata, т.е. является ли функция внешней или общедоступной, не имеет значения. Важно то, является ли входной массив памятью или данными вызова.

// 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;
    }    
}

Простой ответ

Эквивалентно плюсу . public_externalinternal

Другими словами, оба publicи externalмогут быть вызваны вне вашего контракта (например, MetaMask), но только эти два publicмогут быть вызваны из других функций внутри вашего контракта.

Поскольку publicпредоставляет больше доступа, чем external, рекомендуется предпочесть external. Затем вы можете подумать о переходе на publicнего, если полностью понимаете последствия для безопасности и дизайна.

Также обратите внимание, что это externalозначает внешний по отношению к контракту, а не к сети. Обе функции externalи publicмогут вызываться из другого контракта в рамках одной и той же транзакции. Из документа:

Внешние функции являются частью интерфейса контракта, а значит, их можно вызывать из других контрактов и через транзакции.

Так externalчто не предотвращает повторные вызовы функции. Для этого можно использовать ReentrancyGuard от OpenZeppelin , но это стоит газа.

Я пробовал с Solidity 0.8.14.

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

как вы можете видеть, используемый спецификатор видимости функции является внешним

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

в этом случае используемый спецификатор видимости функции является общедоступным

в обоих случаях потребляемый газ равен 22116 (в ремиксе), поэтому нет разницы между внешним и общественным потреблением газа.

Другой тест:

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

В этом случае я протестировал транзакцию, которая меняет состояние контракта, а стоимость выполнения составляет 43300.

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

выполнив ту же функцию, но изменив ее с внешней на публичную, я получил тот же результат: стоимость выполнения равна 43300

Так что по расходу газа я разницы не увидел

Таким образом, для последних компиляторов внешняя функция — это общедоступная функция, которая заставляет свои аргументы находиться в данных вызова, в то время как общедоступная функция — это функция, видимая извне и позволяющая своим аргументам находиться как в памяти, так и в данных вызова.

Для внешних вызовов данные всегда находятся в calldata, даже если функция общедоступна. В этом случае будет шаг преобразования из calldata в память.
Также нет смысла указывать данные вызова в функции, которая выполняет дополнительные вызовы внутренних функций, если только вы не хотите принудительно применять линтинг только для чтения, поскольку они будут неизменно копироваться в память перед передачей внутренней функции.