Pull to refresh

Основные модификаторы и ключевые слова в языке Solidity

В этой статье будут описаны некоторые непонятные новичкам слова, встречающиеся в языке разработки смарт контрактов Solidity. Материал будет полезен начинающим разработчикам Solidity или тем, кто хочет просто поближе познакомится с языком.

Модификаторы видимости

Модификаторы видимости обязательны, всего их бывает 4. Указывает на то, откуда функция может быть вызвана:

  • external - внешние функции, могут быть вызваны только из другого контракта или через транзакцию, вызов func() не сработает внутри контракта, сработает this.func(), но практика с использованием this нежелательна, поскольку такая операция дороже. Если у вас появилась потребность вызвать external функцию внутри контракта, то лучше переименуйте её в public(следующий пункт)

  • public - функции с этим модификатором могут быть вызваны откуда угодно

  • internal - внутренние функции, нельзя вызвать через транзакцию, можно вызвать внутри контракта и контрактов, наследующих его. В этом случае this не работает

  • private - функции с этим модификатором можно вызвать только внутри текущего контракта

contract A {
    uint256 public someNumber;

    // может быть вызвана откуда угодно
    function publicFunc(uint256 _variable) public {
      someNumber = _variable;
    } 

    // может быть вызвана с помощью транзакции или с другого контракта со своим адресом(не путать с дочерним контрактом)
    function externalFunc(uint256 _variable) external {
      someNumber = _variable;
    } 

    // может быть вызвана внутри текущего контракта и в наследующих контрактах(contract A, B)
    function internalFunc(uint256 _variable) internal {
      someNumber = _variable;
    } 

    // может быть вызвана только внутри текущего контракта(contract A)
    function privateFunc(uint256 _variable) private {  
      someNumber = _variable;
    } 
}

contract B is A {

    // эта функция сработает, потому что publicFunc можно вызвать откуда угодно
    function callPublic(uint256 _variable) public {
      publicFunc(_variable);
    } 

    // эта функция не сработает, так как внешние функции могут быть вызваны либо с помощью транзакции, либо с другого контракта
    function callExternal(uint256 _variable) public {
      externalFunc(_variable);
    } 

    // эта функция сработает, потому что внутренние функции могут быть вызваны либо внутри контракта либо в контрактах, которые наследуют текущий
    function callInternal(uint256 _variable) public {
      internalFunc(_variable);
    } 

    // эта функция не сработает, потому что приватные функции могут быть вызваны только внутри контракта, где они были объявлены
    function callPrivate(uint256 _variable) public {
      privateFunc(_variable);
    } 
}


// представим что контракт C это отдельный контракт который существует обособлено от контрактов выше
// по сценарию контракт C обращается к отдельно задеплоенному контракту A, 
// адрес контракта A передается в контракт C во время деплоя

contract C {
    A externalContract;
  
    constructor(address _externalContract) {
      // во время деплоя передаем адрес контракта A, чтобы потом к нему обращаться
      externalContract = A(_externalContract);
    }

    // эта функция сработает, потому что publicFunc можно вызвать откуда угодно
    function callPublic(uint256 _variable) public {
      externalContract.publicFunc(_variable);
    } 

    // эта функция сработает, так как внешние функции могут быть вызваны либо с помощью транзакции,
    // либо с другого контракта(контракт C таким является)
    function callExternal(uint256 _variable) public {
      externalContract.externalFunc(_variable);
    } 

    // эта функция не сработает, потому что внутренние функции могут быть вызваны либо внутри контракта 
    // либо в контрактах, которые наследуют текущий
    function callInternal(uint256 _variable) public {
      externalContract.internalFunc(_variable);
    } 

    // эта функция не сработает, потому что приватные функции 
    // могут быть вызваны только внутри контракта, где они были объявлены
    function callPrivate(uint256 _variable) public {
      externalContract.privateFunc(_variable);
    }  
}

Модификаторы view и pure

В Solidity все функции можно условно разделить на функции чтения(Read Contract) и функции записи(Write Contract). Функции чтения не вносят изменений в переменные контракта, поэтому они бесплатные и вызвать их может кто угодно. Функции записи, напротив, могут изменять состояние какой-либо переменной, а потому они платные.

Пояснение выше было сделано постольку, поскольку модификаторы view и pure применимы только к функциям чтения(Read Contract) - это важный момент, про который всегда надо помнить.

  • модификатором view(смотреть) помечаются функции, которые обращаются к переменным контрактам и возвращают либо эту переменную либо другую информацию, которая как-то связана с этой переменной

  • модификатором pure(чистый) помечаются функции, которые работают только с аргументами, которая эта функция принимает, эти функции можно назвать изолированными от переменных контракта, потому что к ним функция никогда не обращается - собственно, поэтому эти функции чистые. Чистые функции, например, полезны для вычисления каких-то громоздких арифметических операций, которые удобно вынести в отдельный блок кода

contract D {
    // private говорит о том, что к этой переменной 
    // можно обратиться только внутри контракта
    uint private x = 1;

    // эта функция помечена как view, потому что она не изменяет содержимое имеющихся переменных контракта,
    // но при этом возвращает сумму этой переменной и передаваемого аргумента
    function addToX(uint256 _y) public view returns (uint256) {
      return x + _y;
    } 

    // эта функция помечена как view, потому что она обращается к переменной контракта, но не вносит никаких изменений
    function returnX() public view {
      return x;
    }

    // эта функция помечена как pure, потому что она работает только с теми значениями, которые принимает
    function sum(uint256 _a, uint256 _b) public pure returns (uint256) {
      return _a + _b;
    }
}

Ключевые слова constant и immutable

  • constant - этим словом можно помечать переменные, которые никогда не будут изменены после компиляции контракта

  • immutable - этим словом помечаются переменные значение которых назначается в момент деплоя контракта в конструкторе

// представим что у нас есть контракт, имеющий одну view функцию, 
// которая принимает число, а возвращает умноженное на 10 это число

contract E {
    // переменная multiplier это умножитель, 
    // число 10 присваевается в момент компиляции контракта
    // и больше никогда не изменяется 
    uint256 public constant multiplier = 10; 

    // в переменную timeDeploy зашивается количество секунд unix времени
    // таким образом мы можем посмотреть в какой момент контракт был опубликован в сети
    uint256 public immutable timeDeploy;

    // функция конструктор вызывается в момент деплоя контракта
    constructor() { 
      // записываем момент времени, когда контракт был задеплоен
      timeDeploy = block.timestamp; 
    }

    // функция которая принимает число, а возвращает это число умноженное на 10
    function multiplyByTen(uint256 _var) public view {
      return _var * multiplier;
    }

    // попробуем изменить значения переменных multiplier, timeDeploy

    // с этой функцией контракт не скомпилируется
    // TypeError: Cannot assign to a constant variable.
    function setMultiplier(uint256 _var) public {
        multiplier = _var;
    }

    // с этой функцией контракт не скомпилируется
    // TypeError: Cannot write to immutable here: Immutable variables can only be initialized inline or assigned directly in the constructor.
    function setTimeDeploy(uint256 _var) public {
        timeDeploy = _var;
    }
}

Слово payable

Этим словом декларируются функции, которые принимают нативную валюту сети(ETH), а также адреса на которые нативная валюта отправляется.

Рассмотрим следующий пример, где показано использование payable в разных ситуациях.

contract Payable {
    // owner - владелец контракта, на адрес которого переводится собранный эфир(ETH)
    address payable public owner;

    // конструктор помечен payable, это говорит о том, 
    // что на контракт можно отправить какое-то количество эфира(ETH) во время деплоя
    constructor() payable {
        // при присваивании адреса на который будет переводиться эфир,
        // адрес необходимо явно конвертировать в специальнный тип
        owner = payable(msg.sender);
    }

    // вы можете вызвать эту функцию и одновременно отправить эфир благодаря payable
    // отправленный эфир будет записан на баланс адреса контракта
    function deposit() public payable {}

    // эта функция не задекларирована как payable, 
    // поэтому даже если вы вызовете функцию и отправите эфир,
    // то этот эфир не будет принят, а сама функция откатится(reverted)
    function notPayable() public {}

    // функция перевода эфира владельцу(owner) не нуждается в декларировании payable
    function withdraw() public {
        // записываем баланс контракта
        // чтобы потом перевести всю сумму
        uint amount = address(this).balance;

        // переводим весь баланс владельцу(owner)
        // мы можем это сделать благодаря payable на 3 строке
        (bool success, ) = owner.call{value: amount}("");
        require(success, "Failed to send Ether");
    }

    // функция перевода какой-то суммы(_amount) эфира любому адресу(_to)
    function transfer(address payable _to, uint _amount) public {
        // мы можем перевести эфир адресу _to благодаря декларированию первого аргумента как payable
        (bool success, ) = _to.call{value: _amount}("");
        require(success, "Failed to send Ether");
    }
}

Ключевые слова virtual и override

  • virtual - слово, говорящее о том, что эту функцию можно переопределить ниже по иерархии

  • override - ключевое слово, ставится в контракте потомке, говорит о том, что функция уже переопределена

  • virtual override - функция может иметь одновременно два слова, это говорит о том, что функция была переопределена в текущем контракте и может быть переопределена в контрактах ниже по иерархии наследования

Если функция имеет модификатор видимости private, то её нельзя переписать, так как доступ к приватным функциям мы имеем только внутри того контракта, где она была объявлена.

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

contract Senior { // 'верхний' контракт в иерархии наследовании

    // virtual говорит о том, что функция может быть переписана
    // если от этого контракта будет наследован другой контракт
    function decimals() public view virtual returns (uint8) {
        return 18;
    }
}

contract Middle is Senior { // 'средний' контракт в иерархии наследования

    // virtual override говорит о том, что функция с одной стороны
    // переписана, а с другой, что она снова может быть переписана 
    // в иерархии наследования
    function decimals() public view virtual override returns (uint8) {
        return 8;
    }
}

contract Junior is Middle { // 'младший' контракт в иерархии наследования

    // override говорит о том, что функция переписана, 
    // если мы наследуемся от текущего контракта и 
    // попытаемся переписать нашу единственную функцию
    // то ничего не выйдет, т. к. нет слова virtual
    function decimals() public view override returns (uint8) {
        return 15;
    }
}

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

contract Base1 {
    // в первом контракте наша функция возвращает 18
    function decimals() public view virtual returns (uint8) {
        return 18;
    }
}

contract Base2 {
    // во втором контракте наша функция возвращает 9
    function decimals() public view virtual returns (uint8) {
        return 9;
    }
}

contract Middle is Base1, Base2 {
    // в скобках после override указываются контракты, от которых наследуется текущий контракт
    // в данной ситуации decimals вернет 5
    function decimals() public view override(Base1, Base2) returns (uint8) {
        return 5;
    }

    
}

contract MiddleV2 is Base1, Base2 {
    // здесь decimals вернет 18, потому что мы берем это значение из Base1
    function decimals() public view override(Base1, Base2) returns (uint8) {
        return Base1.decimals();
    }
}

Послесловие

Остались вопросы? С чем-то не согласны? Пишите комментарии

Поддержать автора криптовалютой: 0x021Db128ceab47C66419990ad95b3b180dF3f91F

Tags:
Hubs:
You can’t comment this publication because its author is not yet a full member of the community. You will be able to contact the author only after he or she has been invited by someone in the community. Until then, author’s username will be hidden by an alias.