Mejores prácticas `externas` vs `públicas`

Aparte del publicmodificador, Ethereum introduce el externaluno. Ambos pueden ser llamados fuera del contrato y dentro (el último por el patrón this.f()). Además, según los documentos :

Las funciones externas a veces son más eficientes cuando reciben grandes conjuntos de datos.

pero no hay más información sobre qué significa realmente sometimesy si esta ganancia de eficiencia se mantiene también para llamadas internas.

¿Cuáles son las mejores prácticas para usar la palabra clave externalvs public? ¿Hay algún patrón o recomendación?

Respuestas (8)

Un ejemplo simple que demuestra este efecto se ve así:

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

Al llamar a cada función, podemos ver que la publicfunción usa 496 gas, mientras que la externalfunción usa solo 261.

La diferencia se debe a que en las funciones públicas, Solidity copia inmediatamente los argumentos de la matriz en la memoria, mientras que las funciones externas pueden leer directamente desde los datos de llamada. La asignación de memoria es cara, mientras que la lectura de datos de llamadas es barata.

La razón por la que publiclas funciones necesitan escribir todos los argumentos en la memoria es que las funciones públicas pueden llamarse internamente, lo que en realidad es un proceso completamente diferente al de las llamadas externas. Las llamadas internas se ejecutan a través de saltos en el código y los argumentos de la matriz se pasan internamente mediante punteros a la memoria. Por lo tanto, cuando el compilador genera el código para una función interna, esa función espera que sus argumentos estén ubicados en la memoria.

Para funciones externas, el compilador no necesita permitir llamadas internas, por lo que permite que los argumentos se lean directamente desde calldata, ahorrando el paso de copia.

En cuanto a las mejores prácticas, debe usar externalsi espera que la función solo se llame externamente, y use publicsi necesita llamar a la función internamente. Casi nunca tiene sentido usar el this.f()patrón, ya que requiere un real CALLpara ejecutarse, lo cual es costoso. Además, pasar arreglos a través de este método sería mucho más costoso que pasarlos internamente.

Esencialmente, verá beneficios de rendimiento externalcada vez que solo llame a una función externamente y pase una gran cantidad de datos de llamada (por ejemplo, matrices grandes).

Ejemplos para diferenciar:

público - todos pueden acceder

externo: no se puede acceder internamente, solo externamente

interno: solo este contrato y los contratos derivados de él pueden acceder

privado: solo se puede acceder desde este contrato

Excelente y una respuesta muy útil. ¡Gracias Tjaden!
Si estamos diseñando una interfaz pública para contratos y otras personas confiarán en la interfaz (piense en EIP), ¿debemos usar siempre external?
^ Esto debe agregarse a los documentos de Solidity.
Algo anda mal con tu respuesta: joxi.ru/Drlz51Xc4MDGX2
Además, las funciones externas no se pueden anular en contratos derivados.
@Anron Pegov Estoy informando el costo del gas de ejecución, no el total. es decir, solo el costo del gas incurrido al ejecutar el contrato, no enviarle la transacción
¿Debería en este caso function name() public view returns (string)declararse ERC-20 Token Standard como externo para ahorrar algo de gasolina? Porque parece que la función name()no se llama internamente.
Esa función no tiene argumentos, por lo que no hay nada que copiar. Así que la ventaja de usar externalover publicse desvanece.
Solidity 0.6.9 ahora permite calldataser utilizado para cualquier variable o parámetro, incluso en internalfunciones.
Principiante aquí, gracias algunas preguntas emergentes: 1) ¿Sería mejor tener una función externaly internalen lugar de publictener que llamar a ambos entonces? 2) Como dijo el comentarista anterior, ¿es esto ahora irrelevante desde 0.6.9? 3) ¿Agregar viewmás mejora el rendimiento?
@TjadenHess ¿Podría verificar la última respuesta a continuación y ayudar a explicar si su explicación actual aún se mantiene? Solidity está cambiando rápidamente y algunos de los mecanismos podrían haber sido rediseñados.
¿Qué significa LLAMADA real?
Podría estar malinterpretando lo que significa "acceder internamente", pero ¿por qué entonces los constructores son públicos y no externos? (me parece que solo se llaman una vez, externamente)
@samlaf "Accedido internamente" significa llamado desde dentro del mismo contrato y no externamente desde otro contrato o transacción. Este último implica un código de operación especial como CALL, DELEGATECALL, etc. y es más caro. En cuanto a los constructores, no lo son, aplicar visibilidad a un constructor es un poco estirar el concepto y no encaja perfectamente , razón por la cual se eliminó del lenguaje en 0.7.0.
@Maxareo Sí, esta respuesta es correcta pero está desactualizada. La distinción es realmente sobre memoryvs calldata, que ya no está ligada a la visibilidad. En el último compilador externaly publicle dará el mismo código de bytes exacto si eso es lo único que difiere , como se muestra en mi respuesta en "¿Usar externo sobre público en una biblioteca reduce los costos de gas?" .

Actualización para Solidez ^0.8

La respuesta de Tjaden es excelente, pero creo que merece una actualización para las últimas versiones de Solidity. Su fragmento de código ya no se compila. Recibe este error ahora:

La ubicación de los datos debe ser memoryo calldatapara el parámetro en función, pero no se proporcionó ninguno.

Esto se debe al nuevo requisito de ser explícito al usar tipos de referencia , como matrices. También memoryy calldataahora están permitidos en todas las funciones independientemente de su visibilidad.

Una reescritura sería algo como esto:

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

Como nota al margen, puede decodificar calldatavariables memorypero no al revés.

Reestructurando la respuesta anterior para mayor claridad:

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;
    }
}
  • Las llamadas internas son las más económicas, ya que se ejecutan mediante código JUMP, pasando punteros a memoria.
  • Las llamadas internas para funciones públicas son costosas porque las llamadas a funciones internas esperan que los argumentos se asignen a la memoria, ya que la función pública no sabe si la invocación es externa o interna, copia los argumentos en la memoria y, por lo tanto, es más costosa.
  • Si sabe que la función solo se llamará externamente, useexternal

  • Casi nunca tiene sentido usar el patrón this.f(), ya que requiere que se ejecute una LLAMADA real, lo cual es costoso.

Acabo de comprobar el resultado con el último compilador. Parece que la reducción del costo del gas se debe a la memoria frente a los datos de llamada, es decir, no importa si una función es externa o pública. Lo que importa es si la matriz de entrada es memoria o datos de llamada.

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

respuesta sencilla

El publices equivalente a externalmás internal.

En otras palabras, ambos publicy externalpueden llamarse desde fuera de su contrato (como MetaMask), pero estos dos solo publicpueden llamarse desde otras funciones dentro de su contrato.

Debido a que publicotorga más acceso que external, la mejor práctica general es preferir external. Entonces puede considerar cambiar a publicsi comprende completamente las implicaciones de seguridad y diseño.

También tenga en cuenta que externalsignifica externo al contrato, no a la red. Tanto externallas publicfunciones como pueden llamarse desde otro contrato dentro de la misma transacción. Del documento:

Las funciones externas son parte de la interfaz del contrato, lo que significa que se pueden llamar desde otros contratos y mediante transacciones.

Por lo tanto external, no evita las llamadas reentrantes a la función. Para eso, uno puede usar ReentrancyGuard de OpenZeppelin , pero eso cuesta gasolina.

He probado con 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;
   }
}

como puede ver, el especificador de visibilidad de función utilizado es externo

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

en este caso, el especificador de visibilidad de función utilizado es público

para ambos casos el gas consumido es 22116 (en remix) por lo que no hay diferencia entre externo y publico en cuanto al consumo de gas

Otra prueba:

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

En este caso he probado una transacción que cambia el estado del contrato del contrato y el costo de ejecución es 43300

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

ejecutando la misma función pero cambiando de externo a público obtuve el mismo resultado: el costo de ejecución es igual a 43300

Entonces, en términos de consumo de gas, no vi ninguna diferencia.

Entonces, para los compiladores más recientes, una función externa es una función pública que obliga a que sus argumentos residan en calldata, mientras que una función pública es una función que es visible desde el exterior y permite que sus argumentos residan tanto en la memoria como en calldata.

Para llamadas externas, los datos siempre residen en calldata incluso cuando la función es pública. En ese caso, habrá un paso de conversión de calldata a memoria.
Tampoco tiene sentido especificar calldata en una función que realiza llamadas adicionales a funciones internas, a menos que desee forzar el linting de solo lectura, ya que invariablemente se copiará en la memoria antes de pasar a la función interna.