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