Опубликован: 28.01.2018 | Уровень: для всех | Доступ: платный
Лекция 4:

Обзор всех аспектов среды Solidity. Среда разработки Ethereum, Web3 и Truffle

< Лекция 3 || Лекция 4: 123456 || Лекция 5 >

Наследование и абстрактные контракты

В этой лекции поговорим о наследовании в среде Solidity. Solidity поддерживает множественное наследование. Подробно рассмотрим примеры из документации Solidity: три на наследование и один на конструирование. Примеры можно скопировать из документации в браузер Solidity.


Простой контракт Owned. В нем есть конструктор, у конструктора есть владелец, а адрес владельца устанавливается равным адресу отправителя сообщения. Объект сообщения играет особую роль в среде Solidity и имеет несколько значений. Одно из них - это отправитель. Если кто-то отправляет в контракт средства или создает новый контракт, объект msg.sender будет содержать адрес создателя контракта или отправителя средств.


В контракте Owned конструктор устанавливает адрес владельца равным адресу отправителя сообщения. Посмотрим на контракт mortal. Этот контракт является дочерним по отношению к контракту owned. Он обладает дополнительной функцией kill. Когда контракт mortal выполняется, он автоматически обращается к контракту owned и переменной owner, хранящей адрес пользователя, выполняющего код. При вызове функции kill из контракта mortal будет произведена проверка, является ли пользователь, а именно отправитель сообщения, msg.sender, владельцем, и в случае успешной проверки контракт самоуничтожится и вернет все оставшиеся средства владельцу.


В этом примере два интерфейса, или два абстрактных контракта. В зависимости от ситуации их можно рассматривать по-разному. В данном случае будем считать их интерфейсами. Интерфейсы выглядят так же, как абстрактные контракты. У них есть имя контракта, а также задана функция, но тела функции нет. Это относится и к контракту NameReg.


Рассмотрим контракт named. Он является дочерним сразу для двух контрактов, что демонстрирует множественное наследование в среде Solidity. Сначала наследуются свойства основного контракта, а затем - контракта mortal. Контракт mortal, в свою очередь, является дочерним по отношению к контракту owned. Контракт named - это очередной конструктор, который требует имя в качестве входных данных и выполняет некоторые операции. Он также содержит функцию kill. Эта функция переписывает функцию kill, унаследованную от контракта mortal. Но функция kill выполняет несколько другие операции, чем функция kill, унаследованная от контракта mortal. В итоге она носит название mortal.kill.


Рассмотрим следующий контракт PriceFeed. Он является дочерним сразу для трех контрактов. Один из них - это owned, основной контракт. Второй - это mortal, а третий - named, напрямую задающий имя. Если конструктор требует аргумент, например, контракт named: имя контракта, имя конструктора - это требование должно быть упомянуто в заголовке.


В контракте PriceFeed есть и другая логика. Посмотрим, что произойдет, если другой контракт вызовет PriceFeed и его функцию kill. Вернемся к контракту owned с конструктором. Есть контракт mortal, являющийся дочерним для owned, а также функция kill. Кроме того, есть контракты Base1 и Base2. Оба они являются дочерними для mortal, оба наследуют функцию kill и вызывают в конце унаследованную процедуру mortal.kill. Контракт Final является дочерним для Base1 и Base2.


Что произойдет, если другой контракт вызовет функцию Final.kill Она вызовет функцию Base2.kill, а до контракта Base1 цепочка вызовов так и не дойдет. Эту проблему можно обойти с помощью служебного слова Super. Мы имеем дело с двумя классами, Base1 и Base2, и вместо вызова функции mortal.kill будем вызывать super.kill. Этот способ вызова гарантирует, что если какой-либо другой контракт будет вызывать final.kill, то вначале он вызовет функцию kill из контракта Base1, Base1.kill, а затем - следующий контракт в графе наследования, то есть Base2. Поэтому будет вызвана функция Base2.kill, то есть вызов функции super.kill обеспечивает вызов функции mortal.kill.


Поговорим об абстрактных контрактах. Рассмотрим простой контракт Feline.


Это абстрактный контракт, поскольку в нем есть функция с пустым телом функции. Другой контракт, Cat, является дочерним по отношению к Feline. Он содержит ту же функцию, но в отличие от Feline она содержит тело функции и возвращает некоторое значение. Контракт Feline скомпилировать не удастся, а контракт Cat - наоборот, удастся.

Библиотеки и работа с ними

Библиотеки очень похожи на контракты, но их основная особенность и одновременно задача в том, что они размещаются в сети один раз, имеют фиксированный адрес, а другие контракты используют их код. Обращаться к библиотеке можно так же, как и к контракту.


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


Посмотрим на первую функцию insert.


Она требует указания двух параметров. Первый называется self и имеет тип data storage. Он обеспечивает получение адреса текущего контракта. Второй параметр, value, представляет собой некоторую величину. Вызовем функцию insert из нашего контракта C. Здесь есть переменная knownValues типа Set.Data. В функции register из контракта C мы вызываем функцию insert из типа Set, используя в качестве параметров переменную knownValues, которая берется из собственного хранилища данных, что задано служебными словами Data storage self, и некоторую величину, обозначенную как value.


Стоит отметить, что компилятор не знает, по какому адресу хранится библиотека. Поэтому эти адреса должны быть указаны, чтобы связующая процедура могла к ним обратиться. Можно также указывать адреса вручную. Чтобы узнать больше о библиотеках, ознакомьтесь с документацией Solidity - в разделе Contract and Libraries.

Типы переменных, массивы, структуры и ассоциации

Эта лекция посвящена основным типам переменных, структурам, ассоциациям и способам их применения в среде Solidity. В среде Solidity есть булев тип, тип целых чисел и тип неотрицательных целых чисел, байты, строки, массивы, структуры, ассоциации и пользовательский тип. Величины с фиксированной запятой еще не внедрены. Посмотрим в деталях, как работают основные типы. Их описание можно найти в документации Solidity, в разделе Types.


Переменные булевого типа могут принимать два значения: правда или ложь, как и в других языках программирования. Численные величины бывают типа int - с поддержкой знака минус, или uint - только для неотрицательных значений. Для их определения есть целый ряд ключевых слов, начиная от uint8 и до uint256 с шагом в 8. Это верно и для переменных целочисленного типа.

Рассмотрим пример. В конструкторе, записанном в нашем контракте, назначим несколько величин. Переменной myBool будет присвоено значение "правда", переменной myUint8 - максимальное значение для типа uint8, значение "X" - символьным переменным. Есть также типы массивов символов и строки. В данном случаем две функции, возвращающие значения для символьных и численных переменных. Переменная myUint256 возвращает 256 бит неотрицательного целого числа, а функция getMyBytes32 - 32 бита символов.


С помощью функции setMyString подчеркнем, что в зависимости от длины используемых строк на их обсчет будет уходить разное количество газа. Обратите внимание, что к массивам всегда присоединяется 256 бит.


Поговорим о числах с фиксированной запятой. Они еще не представлены в среде Solidity, но скоро будут включены. Для их описания есть служебные слова ufixed и fixed, требующие указания числа бит в дробной части.


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


Еще один специальный тип в среде Solidity - это адресный тип. Он хранит двадцатибайтное значение адреса в сети Ethereum. Для этого типа есть несколько членов, например, balance, при вызове которого можно узнать баланс на конкретном адресе. Эта функция работает по умолчанию. Еще можно отправлять эфир на адрес с помощью команды address.send.


Рассмотрим на примере. Предположим, что есть адрес 0x123. Можно вызвать процедуры address.balance и address.send, первая вернет величину баланса на этом адресе, а вторая вернет значение "правда" в случае успешного перевода средств. Отдельно стоит отметить, что при отправке эфира необходимо быть внимательным. Если на обсчет транзакции по отправке эфира потратится весь предназначенный для этого газ, она завершится неуспешно и вернет значение "ложь". Поэтому всегда, когда пересылаете эфир, проверяйте код возврата.

Пользовательский тип в среде Solidity объявляется с помощью служебного слова enum.


Скопируем пример с раздела в браузер Solidity. В контракте этот тип назван ActionChoices и включает в себя значения переменных GoLeft, GoRight, GoStraight и SitStill. Для внутренней обработки Solidity переводит подобные массивы в тип uint, сначала применяя тип uint8, и расширяя его до uint16 и так далее по мере роста числа вариантов значения переменных. Скомпилируем код и создадим контракт.


В нем две функции - getChoice и getDefaultChoice. Обратим внимание на возвращаемую величину. Функция getChoice возвращает выбор действий; для целей внутренней обработки список действий, из которых можно выбирать, будет храниться в типе uint8, поскольку список состоит всего из четырех позиций. Одновременно с этим функция getDefaultChoices будет возвращать результат типа uint256, поэтому если потребуется работать с типом полученного результата, его нужно будет переопределить в тип uint256. Посмотрим на полученный результат в правой части браузера Solidity. Функция getDefaultChoice возвращает результат типа uint256, и этот результат - два. (Обратите внимание на порядок перечисления вариантов выбора в определении типа.) Функция getChoice возвращает результат типа uint8, в данном случае - ноль. Напомним, что все вводимые переменные и функции имеют некоторые значения по умолчанию, в данном случае значение по умолчанию - ноль. Значение по умолчанию для булевых переменных - это "ложь".


Давайте сделаем выбор goStraight с помощью функции setGoStraight и проверим, каков будет результат работы функции getChoice. Теперь он тоже равен двум.

Массивы в среде Solidity бывают фиксированные и динамические.


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


Массив X типа uint - это динамический массив, хранящийся в накопителе. Функция F - это массив типа uint, который хранится в памяти, и называется memoryArray. Теперь присвоим X значение memoryArray, что скопирует весь массив в накопитель. Затем введем новую переменную Y, которой присвоим значение X. Y хранит только ссылку на адрес расположения данных из X. Команда Y[7] вернет нам восьмой элемент X. Поскольку все величины имеют некоторые значения по умолчанию, если ничего не было присвоено этому элементу, то будет возвращен ноль. Теперь изменим что-нибудь в Y, тем самым мы вносим изменения и в X. Удаляем всё, кроме первых двух элементов. А теперь удаляем весь массив.

Вернемся к примеру с массивами.


В контракте C есть функция F с параметром Len типа uint, а именно uint256. Массив a хранится в памяти. Массивы в памяти можно создавать с помощью служебного слова new. Стоит обратить внимание на то, что размер массивов, хранящихся в накопителе, можно легко изменять с помощью процедуры length, просто назначая новое число элементов массива. Такую операцию нельзя выполнять с массивами, хранящимися в памяти. Массив a насчитывает семь элементов. А хранящийся в памяти массив bytes имеет длину len. Все должно работать без ошибок, мы даже назначили седьмому элементу массива a значение восемь.

В Solidity можно работать со вложенными массивами.


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

У типа массива есть два члена. Один, обозначающий длину, вызывается служебным словом length, и возвращает число элементов массива. Размер динамических массивов, хранящихся в накопителе, может быть изменен редактированием члена length. Это не происходит автоматически: перед записью в массив величин необходимо вручную изменить его длину. Размер массивов, хранящихся в памяти, как уже упоминалось ранее, фиксирован. Вторым членом для всех типов массивов, кроме строковых, является push, применяемый для добавления элементов к концу массива. Вызов процедуры push возвращает новую длину массива.

Обратимся к примеру из документации.


Создаем очень крупный массив, хранящийся в накопителе. В нем два в двадцатой степени элементов. Затем создаем еще пару массивов. Функция setAllFlagPairs имеет один параметр - newPairs, который по умолчанию хранится в памяти, как и все параметры функций. Если его значение присвоить m_PairsOfFlags, он будет скопирован в накопитель. Функция setFlagPair имеет три параметра: один - index типа uint, и два булевых параметрах. При попытке записать что-либо в index компилятор выдаст ошибку. Всегда необходимо изменять длину массива перед записью в него данных. Будьте внимательны при редактировании размеров массивов. Если величина length будет больше, чем текущий размер массива, он будет расширен. С другой стороны, если величина length будет меньше, чем размер массива, все элементы после length, будут удалены. Чтобы полностью удалить массив, можно воспользоваться служебным словом delete. Тот же эффект можно получить, выставив размер массива, length, на ноль.

С помощью ассоциаций можно наделить почти все типы свойствами других. Ассоциации можно рассматривать как хэш-таблицы всех возможных комбинаций величин. Работа с ассоциациями похожа на работу с массивами. Давайте рассмотрим простую ассоциацию. Я добавил числовой тип булевым величинам, и в конструкторе назначил первому элементу значение два. Функция getMyMapping возвращает булеву величину в зависимости от номера массива, поэтому она вернет значение "правда" для первого элемента массива и "ложь" для всех остальных.



Структура - это способ создания новых типов, как в следующем примере.


Вот две структуры: одна называется Funder, а другая - Campaign. (Стоит отметить, что этот пример в целом не слишком работоспособен, он приведен в иллюстративных целях.) Число кампаний - это величина типа uint. Ассоциация производится между величинами типа uint и кампаниями. При добавлении новой кампании нужно увеличить число кампаний (величины этого массива названы campaignID), затем назначить новую кампанию. Если работа над этим кодом ведется группой программистов, то можно узнавать названия кампаний из их массива и добавлять нужную логику в программу.

< Лекция 3 || Лекция 4: 123456 || Лекция 5 >
Алексей Миронов
Алексей Миронов

Здравствуйте, сколько стоит курс Работа с Ethereum?

Сергей Домников
Сергей Домников
Россия
Светлана Пузына
Светлана Пузына
Россия, г. Москва