Coin Lab

0 488 4
Solidity Часть 3: Интерфейсы, события, типы и обработка ошибок

Часть 3: Характеристики языка Solidity

Автор: Андреас Олофссон, центральный разработчик – Образование и просвещение

Примечание (1): Это третье из серии учебных пособий по языку программирования Solidity. Первую часть пособия можно найти здесь, а вторуюздесь

Примечание (2): Применяются те же оговорки, которые отмечены в Части 1; а именно, эти системы разработаны не для того, чтобы быть автоматическими или ненадежными, они предназначены для функционирования в качестве интерфейсов смарт-контрактов для используемых в коммерческих целях децентрализованных /распределенных приложений.

Эта статья – Часть 4 пособия из 6 частей.

Введение

Данная публикация посвящена языковым характеристикам Solidity. Я рассмотрю несколько основных – типы, интерфейсы, события, ошибки (или их отсутствие), и дам несколько примеров, как это работает на практике.

Типы – основное

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

Solidity — это статически типизированный язык statically typed language, такой, как, например, C/C++ и Java. Он может показаться новым для людей, которые большей частью работали со скриптовыми /интерпретируемыми языками программирования. Статически типизированный statically typed означает, что когда вы декларируете параметр, вы должны также включать его тип. Например:myVar = 55;не разрешается, а int myInt = 55; разрешается. Типы можно выводить при помощи параметра var т.е. var myVar = 55; разрешается и автоматически получает тип uint8. Вам необходимо инициализировать параметр var при его декларировании.

Типы проверяются на стадии компилирования, т.о., если вы допускаете ошибку, то вы видите ошибку компилятора. Например, это невозможно:

contract Test {
bool bVar;

function causesError(address addr){
bVar = addr;
}
}

Ошибка, которую он выдаст, будет выглядеть так: Error: Type address not implicitly convertible to expected type bool.

Преобразование типов

Компилятор позволяет вам совершать преобразования между типами в определенных случаях. Скажем, у вас есть число 1 , которое хранится в параметре uint , и вы хотите использовать его в другом параметре типа int. Это возможно – но обычно вам необходимо самим выполнить преобразования. Вот как вы это сделаете:

 

Преобразование типов также проходит проверку во время компилирования, и в целом, оно вычисляется, но существуют исключения; самое важное из них – при преобразовании адреса в тип контракта. Данный тип приведений может привести к программным ошибкам. Позднее мы рассмотрим несколько примеров.

В итоге преобразование типов – это нечто, требующее очень осторожного использования. В некоторых случаях оно полезно, но чрезмерное и/или неосторожное приведение обычно означает, что код написан неправильно, и это иногда может иметь плохие последствия (например, потеря данных). Помните, что типы присутствуют здесь не просто так.

Контракты и интерфейсы.

Для моделирования смарт-контрактов Solidity использует контрактный contract  тип данных. Он очень похож на class.

Контракт contract имеет несколько полей и методов; например, тип contract может иметь конструктора, он может наследовать от других контрактов, и т.д.

В официальном пособии имеется несколько примеров простых контрактов. Недавно разработчики добавили контракты интерфейса interface contracts. Данные контракты позволяют функциям быть абстрактными (не иметь тела). Технически была возможность использовать контракты “interfaceish” и ранее, но до настоящего времени не было возможности сделать их полностью абстрактными.

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

Вот пример. Это простой интерфейс с единственной функцией.

contract Depositor {
function deposit(uint amount);
}

Данная функция не имеет тела, т.о. она не может запускаться сама по себе. Сейчас мы хотим создать контракт Депозитор Depositor, т.е. что он реализует данный интерфейс.

contract HeyImADepositor is Depositor {

}

Нет, у вас не поучилось. Почему? Вы не реализуете функцию депозита. Если я попробую это скомпилировать, ничего не получится. На момент написания я еще не допустил ошибку компилирования, но контракт не будет работать (отсутствует байтовый код). Чтобы контракт заработал, необходимо создать функцию с той же самой подписью, что и у функции депозита, но с надлежащим телом.

contract OkButNowIAm is Depositor {
function deposit(uint amount) {}
}

Да. Технически сейчас вы — депозитор, т.к. вы реализовали deposit функцию, как этого требовал интерфейс Депозитора.

Теперь мы создадим интерфейс, расширяющий другой интерфейс, но не добавляющий к нему.

contract Depositoror is Depositor{}

Вот реализация

contract DepImpl is Depositoror {
function deposit(uint amount) {}
}

Контракт DepImpl компилируется и будет работать в точности как OkButNowIAm. Далее мы создадим интерфейс, который расширяет два других интерфейса.

contract Depositor {
function deposit(uint amount) constant returns (bool);
}

contract Withdrawer {
function withdraw(uint amount) constant returns (bool);
}

contract BankUser is Depositor, Withdrawer {}

Теперь мы реализуем BankUser, создадим Bank интерфейс, реализуем его, а затем объединим их.

// Интерфейс для банков.
contract Bank {
// Возвращаемые значения должны показывать, что транзакция успешна .
function makeDeposit(uint amount) constant returns (bool);
function makeWithdrawal(uint amount) constant returns (bool);
}

// Фиктивная реализация «Банка»  ‘.
contract UBS is Bank {

function makeDeposit(uint amount) constant returns (bool) {
return true;
}

function makeWithdrawal(uint amount) constant returns (bool) {
return true;
}
}

// Реализация Пользователя банка (BankUser)
contract ABankUser is BankUser {

Bank bank;

function ABankUser(){
bank = new UBS();
}

function deposit(uint amount){
bank.makeDeposit(amount);
}

function withdraw(uint amount){
bank.makeWithdrawal(amount);
}
}

Контракт ABankUser сохраняет ссылку на контракт Bank , чтобы осуществить фактический депозит. Bank  — это интерфейс, что означает, что любой контракт, реализующий данный интерфейс, будет работать. Фактически мы могли бы сделать данный контракт еще более обобщенным, позволив банку установку. Это работает:

contract ARiskyBankUser is BankUser {

Bank bank;

function deposit(uint amount){
bank.makeDeposit(amount);
}

function withdraw(uint amount){
bank.makeWithdrawal(amount);
}

function setBank(address addr){
this.bank = Bank(addr);
}
}

В нем присутствует risky , т.к. он небезопасен. Во-первых, bank запускается не-инициализированным, т.е. функции deposit и withdraw могут не удаться. Во-вторых, setBank имеет адрес в сигнатуре методов, и нет гарантии, что данный контракт является банком. И, наконец, конечно, в целом это плохой контракт, т.к. он не имеет структуры разрешений. Он представляет собой просто демонстрацию интерфейсов, поэтому он и не должен иметь такую структуру, но все же необходимо о нем помнить.

События

События используются для вывода информации из кода контракта Solidity в клиентский журнал блокчейна. Это способ сделать эту информацию доступной «внешнему миру». На верхушке самих событий большинство клиентов также имеют способ сбора данных выведенных файлов и их инкапсуляции в структуры данных событий. Это особенно важно для эффективного взаимодействия между клиентами блокчейна и «внешним миром», от которого будут зависеть другие вещи.

Давайте рассмотрим пример. Начнем с добавления новой функции в интерфейс BankUser:

contract BankUser is Depositor, Withdrawer {
function complain(bytes32 complaint);
}

Теперь реализуем:

contract ABankUser is BankUser {

Bank bank;

event Complain(address indexed userAddress, bytes32 indexed complaint);

function ABankUser(){
bank = new UBS();
}

function deposit(uint amount){
bool result = bank.makeDeposit(amount);
if(!result){
complain(«wtf»);
}
}

function withdraw(uint amount){
bool result = bank.makeWithdrawal(amount);
if(!result){
complain(«wtf»);
}
}

function complain(bytes32 complaint){
Complain(msg.sender, complaint);
}
}

Здесь произойдет то, что контракт ABankUser будет исключаться, а метод complain — запускаться, он сгенерирует событие, которое можно прочитать их журнала. При использовании библиотеки подобной  Ethereums web3, вы можете определить слушателя именно для этого события. Это очень просто. Допустим, что контракт web3 для  ABankUser носит имя bankUser123. Чтобы сгенерировать фильтр для данного события, мы сделаем следующее:

var filter = bankUser123.Complain();

События включаются в контракты json ABI, и, вызывая соответствующую функцию javascript, вы получаете фильтр. Если мы продолжительное время будем слушать и работать с событиями, мы сможем выполнить следующее:

filter.watch(callbackFun(data));

function callbackFun(data){
var args = data.args;
eMailTheManager(args.userAddress, args.complaint);
}

Объект args будет иметь поля, названные именем индексированных полей в контракте, т.о. вы сможете решать, когда создать событие, как назвать каждое из этих полей, и, конечно, их типы.

Относительно типов: Контракты и «интерфейсы» — одно и то же. Не существует особого типа интерфейса. Единственным отличием является то, что компилятор позволяет контракту интерфейса иметь абстрактные функции. Также, как мы видим, можно сделать контракт супер-контрактом, т.е. нет необходимости создавать явное приведение типа:

Преобразование адресов в контракты

Вы можете выполнять преобразования между контрактами и адресами. Например, разрешается сделать так:

function setB(address addr){
b = B(addr);
}

Хотя нет способа проверить, какой тип контракта фактически находится на данном адресе, и является ли он контрактом вообще. Это означает следующее: Контракт может пройти проверку типов компилятором, но все равно иметь неверный тип. Для этого также не существует ошибки (т.к. ошибки отсутствуют).

Рассмотрим следующее:

contract Greeter {
function hello() returns (bytes32) {
return «Hello!»;
}
}

contract Test {
Greeter greeter;

function setGreeter(address addr) {
// Помните, что это приведение, а не реализация.
greeter = Greeter(addr);
}

function callGreeter() returns (bytes32) {
return greeter.greet();
}
}

contract Tester {
Test t;
bytes32 msg;

function Tester(){
t = new Test();
Greeter greeter = new Greeter();
t.setGreeter(address(greeter));
msg = t.callGreeter();
}
}

Это компилирует, и мы можем проверить и увидеть, что в поле msg  Tester имеется надпись “Hello!”; тем не менее, если заменить конструктор Tester на пересылку его адреса в Test  t.setGreeter(address(this)) – он все равно будет компилировать, но он не будет правильно функционировать. Причина, конечно, в том, что адрес, который мы передаем в setGreeter, не является адресом Greeter. Когда мы вызываем lTest.callGreeter, данные надлежащим образом форматируются, и делается вызов на Greeter, но принимающим контрактом является Tester, и , следовательно, он не имеет представления, как с этим работать.

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

Коротко об ошибках

В Solidity (пока) нет настоящей системы обработки ошибок. Нет утверждений try — catch или throw , или чегото подобного. Многие операции, выдающие ошибки или исключения в большинстве других языков, не выдают ошибки в Solidity (или любом другом языке контрактов), как, например, деление на нуль (сверьтесь с  техническим описанием (ссылка в PDF)).

Разработчикам контрактов необходимо самим обрабатывать ошибки. Solidity выполняет проверки работоспособности в массивах и т.п., но зачастую отвечает просто выполнением инструкции (STOP). По словам разработчиков, это обычное помещение в хранилище в ожидании обработки более сложной ошибки, и становления на место системы восстановления.

Так как и Solidity, и EVM находятся в стадии разработки, лично я полагаю, что на сегодняшний день лучше всего придерживаться нескольких полезных правил и не перестараться. Я обычно использую функции, выполняющие некоторую работу и возвращающие булевое значение, показывающее, правильно или неправильно происходит процесс (для вызова контрактов), и  events для внешних вызывающих.

Булевое значение – это немного лениво, и я собираюсь заменить его на коды состояния, но на данный момент я, в основном, использую такую «систему». Для событий я склонен придерживаться кодов состояний http, и я могу делать то же самое для возвращаемых значений.

Опубликовано: Андреас Олофссон, 17 апреля 2015г. в  tutorials

Теги

Блокчейн глазами IBM: зачем нужен проект HyperLedger и когда мир перейдёт на новую технологию В четверг, 10 ноября, IT-гигант IBM представит своё видение того,…
admin by admin
0 2906 0
Взгляд Microsoft на блокчейн-технологию: от слов к коду Представляем одного из ключевых спикеров Blockchain & Bitcoin Conference Russia…
admin by admin
0 1674 0
Блокчейн в банковской системе: взгляд Сбербанка Сбербанк России – один из лидеров банковского рынка по внедрению…
admin by admin
0 3492 0

Leave a Reply

Войти
Регистрация
Отправить сообщение