Как стать автором
Обновить

Основные модификаторы и ключевые слова в языке 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

Теги:
Хабы:
Данная статья не подлежит комментированию, поскольку её автор ещё не является полноправным участником сообщества. Вы сможете связаться с автором только после того, как он получит приглашение от кого-либо из участников сообщества. До этого момента его username будет скрыт псевдонимом.