solidity语言学习(9)—— 合约(Contract)

在solidity中合约类似于面对对象的语言中的类。他包括了状态变量中那些长期的数据以及能够修改这些状态的函数。在一个合约中调用另一个合约(实例)的函数会执行一个EVM函数调用,它将转换上下文内容是得状态变量不可触及。

创造合约

合约是通过以太坊交易或者来自solidity合约内部,“从外部”产生的。

一些典型的IDE,比如说Remix,能够使用UI元素创造无缝的进程。

在以太坊中程式化的产生一个合约,最好是通过使用JavaScript API web3.js的办法。另外今天有一个新的方法——web3,eth.Contract 来使合约创造变得更加容易。

当一个合约被生成出来,他的构造器(一个声明含 constructor 关键字的函数)已被执行一次。constructor可以是随意的,但是只允许有一个constructor,这意味着重加载(overloading)是不支持的。

在内部,构造参数在传递通过合约代码后被送到ABI 编码处,但如果你使用web3js,你不需要考虑这些。

如果你的合约想要创建另一个合约,生成的新合约的源代码(还有二进制码)必须被创造者提前知晓。这意味着递归循环创造合约是不可能的。

pragma solidity ^0.4.22;

contract OwnedToken {
   // 代币创建(TokenCreator)是下面定义的一种合约类型。
   // 只要它没有被用于创造新合约,引用它都没问题
   TokenCreator creator;
   address owner;
   bytes32 name;

   // 下面这个构造器记录了创造者和赋值名称
   constructor(bytes32 _name) public {
      // 状态变量可以通过它们的名称访问,但不能通过
      // 诸如 this.owner 来访问。这一点也同样适用于函数。而且尤其是在构造器当中,
      // 你只能像这样调用他们(“internally”)(我估摸意思是想内部调用一样调用参数),
      // 因为合约此时还并不存在
      owner = msg.sender;
      // 我们做一个显式的类型转换,将address转换为TokenCreator。并且只能假设调用者合约的
      // 类型为TokenCreator,但实际上并没有真正确定它的办法
      creator = TokenCreator(msg.sender);
      name = _name;
   }

   function changeName(bytes32 newName) public {
      // 只有创造者能够修改名字
      // 下面的比较是可实现的,因为合约被隐式的转化为address
      if (msg.sender == address(creator))
        name = newName;
    }

    function transfer(address newOwner) public {
      //  只有当前的代币拥有者可以交易代币
      if (msg.sender != owner) return;
      // 我们也希望询问代币的创造者这样的交易是否ok。注意这里调用了一个
      // 下面定义的合约中的一个函数。如果这个调用失败了(比如说因为耗尽了gas)
      // 这里的执行将立即停止
      if (creator.isTokenTransferOK(owner,newOwner))
          owner. = newOwner;
     }
}

contract TokenCreator {
    function createToken(byres32 name)
      public
      returns (OwndeToken tokenAddress)
    {
       // 创造一个新的代币合约,并返回它的address。
       // 从JavaScript的角度,返回类型是一个简单的‘address’
       // 但这是在ABI中最严密(closest)的可用类型
       return new OwndeToken(name);
     }

    function changeName(OwndeToken tokenAddress,bytes32 name) public{
       // 同上面一样,‘tokenAddress’的外部类型只是简单的‘address’
       tokenAddress.changeName(name);
     }

     function isTokenTransferOK(address currentOwner,address newOwner)
         public
         view 
         returns (bool ok)
     {
        // 检查一些不确定的(arbitrary)状态
        address tokenAddress = msg.sender;
        return (keccak256(newOwner) & 0xff) == (bytes20(tokenAddress) & 0xff);
     }
}

可见性 和 获得者(Visibility and Getter)

因为solidity知道两种函数调用方式(内部调用不会创造一个实际的EVM调用(也称为信息调用),而外部调用会这样做),对于函数和状态变量,这里提供了四种可见性。

函数有四种特定的可见性,external,public,internal或者private,而默认是public。对于状态变量,external是不可实现的但是默认为internal。

external:
external 函数 是 合约接口的一部分,这意味着我们可以通过其他合约或通过交易来调用它们。一个external函数f不能被从内部调用(比如:f()就没有效果,但是this.f()就有用)。当接收到大的数据数组时,external函数有时会更有效率。

public:
public 函数是合约接口的一部分,而且既可以被内部调用,也可以通过消息调用。对于public状态变量,会自动生成一个获取(它的值)的函数。

internal:
这些函数和状态变量只能在内部被访问(比如说从当前合约或者当前合约的导出合约),而且不需要使用this。

private:
private函数和状态变量仅仅在创建它的合约中可见,即使是导出合约中也不可见。

note:
一个合约中的所有东西在内部都是可见的。让某些量 private 化仅仅是为了阻止其他合约访问或修改这些信息,但是其实他对于整个区块链外的事件依然是可见的。

可见性标示在状态变量的后面标示出来,以及在函数的参数列表和返回参数列表之间标示。

pragma solidity ^0.4.16;

contract C {
    funtion f(uint a) private pure reutrns (uint b) { return a + 1; }
    function setData(uint a) internal { data = a; }
    uint public data;
}

在下面这个例子中,D函数可以调用c.getData() 来取回状态存储中data的值,但它并不能调用f。合约E是C导出的合约(E继承了C),因此 他可以调用 compute 函数:

// This will not compile
pragma solidity ^0.4.0;

contract C {
    uint private data;

    function f(uint a) private returns(uint b)  {return a + 1; }
    function setData(uint a) public { data = a; }
    function getData() public returns(uint)  { return data; }
    function compute(uint a , uint b) internal returns (uint) { return a+b; )
}

contract D {
     function readData() public {
        C c = new C();
        uint local = c.f(7); // 错误 成员f 是不可见的
        c.setData(3);
        local = c.getData();
        local = c.compute(3,5); // 错误,成员 compute 是不可见的
     }
}

contract E is C {
    function g() public {
       C c = new C();
       uint val = compute(3,5);// 可以访问internal成员(因为是从导出合约向母合约)
    }
}

getter 函数
编译其会为所有public状态变量自动生成一个getter函数。比如为下面给出的这个合约,编译器会生成一个叫data 的函数,他不需要任何参数,但会返回一个uint,这个uint是状态变量data的值。状态变量的初始化可以在声明的时候完成。

pragma solidity ^0.4.0;

contract C {
    uint public data = 42;
}

contract Caller {
     C c = new C();
     function f() public {
        uint local = c.data();
      }
}

getter函数具有外部可见性。如果这个标志形式是内部可见的(没有this.)它会被识别是状态变量。如果它是外部可见的(有 this.),它会被识别为函数

pragma solidity ^0.4.0;

contract C {
    uint public data;
    function x() public {
        data = 3;// 内部访问
        uint val = this.data();// 外部访问
    }
}

下一个例子有一滴滴复杂:

pragma solidity ^0.4.0;

contract Complex {
    struct Data {
        uint a;
        bytes3 b;
        mapping (uint => uint) map;
    }
    mapping (uint => mapping(bool => Data[])) public data;
}

它会生成下面这个形式的一个函数:

function data(uint agr1, bool arg2, uint arg3) public returns (uint a,bytes3 b){
    a = data[arg1][arg2][arg3].a;
    b = data[arg1][arg2][arg3].b;
}

注意在结构体中mapping被省略了,因为没有好的方法去提供mapping的key值。

函数修饰器(Function Modifiers)

修饰器能够被用来简单的改变函数的状态。比如说,他们可以在函数执行之前自动检查环境。修饰器是由可继承性质的合约,而且他可能被继承者直接无视掉。

pragma solidity  ^0.4.22;

contract owned {
   function owned() public { owner = msg.sender;}
   address owner;

   // 这个合约仅仅定义了一个修饰器,但并不使用它:它会被导出合约所使用。
   // 函数体会被插入到修饰器定义代码中 “_” 的位置
   // 这意味着如果owner调用了这个函数,那么函数体要么会执行
   // 要么会抛出一个异常
   modifier onlyOwner {
      require(
          msg.sender == owner,
          "Only owner cal call this function."
        );
        _;
      }
}

contract mortal is owned {
    // 这个合约从’owned‘继承了‘onlyOwner'修饰器并且将其应用在’close‘函数上
    // 这导致在调用close时仅当滴啊用者是owner时才会起作用
    function close() public onlyOwner {
        selfdestruct(owner);
     }
}

contract priced {
    // 修饰器能够接受参数
    modifier costs(uint price) {
      if (msg.value >= price) {
         _;
       }
     }
}

contract Register is priced,owned {
   mapping (address => bool) registeredAddresses;
   uint price;

   function Register(uint initialPrice) public { price = initialPrice; }

   // 在这里,提供’payable‘关键字也非常重要,否则函数
   // 会自动拒绝所有送来的Ether
   function register() public payable costs(price) {
       registeredAddresses[msg.sender] = true;
   }

   function changePrice(uint _price) public onlyOwner {
        price = _price;
   }
}

contract Mutex {
   bool locked;
   modifier noReentrancy() {
      require(
         !locked,
         "Reentrant call."
      };
      locked = true;
      _;
      locked = false;
  }

  /// 函数被一个mutex所保护,这意味着无法从“msg.sender.call"里再次调用’f’函数
  /// 'return 7' 指令将7分配给一个返回值但依然执行了修饰器中
  /// ”locked=false“这条语句。
  function f() public noReentrancy returns (uint) {
      require(msg.sender.call());
      return 7;
  }
}

若要在一个函数中使用多个修饰器,可以通过一个空格分割的list,并且将按照摆放顺序执行

warning:
在早期版本的solidity,return指令在由修饰器的函数中将有不同的表现

修饰器和函数的return函数,都仅仅只是退出了当前函数体。在更上一级的修饰器中,在‘_’后的部分,返回的变量依然会赋值,而控制流也会继续。

在修饰其的阐述和上下文中任意表达式都是允许的,所有在函数中可见的符号都同样在修饰器中可见。然而在修饰器中定义的符号在函数中就不可见了(因为它们可能被重写(overriding)所改变)

重写overriding:Overriding 是指子类有一个函数与父类中的某个虚函数的名字和签名都相同。当一个子类的对象调用该虚函数时,就会执行子类中Overriding 的那个函数。所以Overriding 改变的是类的行为而不是类的接口。

常数状态变量 (Constant State Variable)

状态变量可以被定义为constan。在这种情况下,它们必须被一个表达式赋值,该表达式在编译时就是一个常数。任何用于访问存储,区块链数据(比如 now,this.balance 或者 block.number)或者处理数据(msg.value 或 gasleft() )或者 对外部合约进行调用的 表达式都是不允许的。对于内存分配有副作用的表达式是允许的,但那些或许对其他内存物体由副作用的表达式就不允许了。一些固定的内在函数 比如 keccak256,sha256,ripemd160,ecrecover,addmod和 mulmod都是允许的(即使他们能够调用外部合约)。

允许使用对内存分配由副作用的表达式的原因,在于它可能构建复杂的对象,比如查找表格。这一特性尚未完全可用。

编译器并不会为这些变量提供一个存储匣,所有事件都被各自的常数表达式所替换(这些值可能被最优化器计算为一个单值)

目前并非所有类型的常数都可以实现。目前暂时支持的类型有值类型和字符串类型。

pragma solidity ^0.4.0;

contract C {
    uint constant x = 32**22 + 8;
    string constant text = "abc" ;
    bytes32 constant myHash = keccak256("abc");
}

函数(Function)

View Function
声明为 view 的函数将保证并不会修改状态。

以下的指令将被视为对状态的修改:

  1. 写入状态变量。
  2. 发表事件(Emitting Events)
  3. 生成其他合约
  4. 使用自毁(selfdestruct)
  5. 通过调用传输Ether
  6. 调用其他未标记 view 或 pure 的函数
  7. 使用低阶调用(low-level call)
  8. 使用包含控制代码的在线组件
pragma solidity ^0.4.16;

contract C {
    function f(uint a , uint b) public view returns (uint) {
       return a * (b + 42) = now;
    }
}

note:
1.对于view型函数来说,constant 只是一个(表达式的)别名(alias),但这一点已遭到反对并将在0.5.0中被移除
2.getter方法又被标明 view。
3.如果使用了无效的显式类型转换,即使调用了一个 view 函数,状态修改依然是可行的。当调用这类函数时,你可以通过增加pragma experimental "v0.5.0";改变编译器,使用STATICCALL ,以阻止在EVM层级上状态的改变
warning:
编译器并不会强制使view方法不能改变状态,它只是会弹出一个警告。

Pure Function
被声明为 pure 的函数会承诺不从状态中读取(信息?)(read from)或修改状态。

作为前面解释状态修改指令的补充,以下被认为而是从状态读取的指令:
1. 从状态变量中读取
2. 访问 this.balance 或者 <address>.balance
3. 读取 blocktxmsg的任何成员(但是 msg.sigmsg.data例外)
4. 调用任何没有标记 pure 的函数
5. 使用包含确定的操作代码的在线组件

pragma solidity ^0.4.16;

contract C {
    function f(uint a,uint b) public pure returns (uint) {
          return a * (b + 42);
     }
}

note:
如果使用了无效的显示类型转换,即使标志了 pure 的函数被调用了,状态修改依然有效。同样可以使用上面所说的改变编译器的方法解决此问题
warning:
1.在EVM层面阻止函数读取状态是不可能的,唯一可能的是阻止它们写入状态(比如说,在EVM层面只有 view 可以被强制执行,但pure不行)
2.在 0.4.17之前,编译器并不强制要求pure 不能读取状态

Fallback Function
一个合约只能恰好有一个未命名的函数。该函数不能有参数,也不能返回任何值。如果没有其他函数匹配给予的函数标示(或者压根没有提供任何数据)(这里是说调用命令中没有调用合约中其他有用的函数),它会在调用合约时执行,

此外,无论何时当合约收到plain Ether(这里不太会翻)时(没有数据),这个函数就会执行。补充一点,为了收到Ether,回退函数(Fallback Function)必须被标记为 payable。如果没有这样的函数存在,则该合约无法通过正常的交易收到Ether。

在最坏的情况下,回退函数能够仅仅依赖与可用的2300 gas(比如说当传输或交易需要时),而无法留下足够空间去执行除基本登录以外其他的操作。以下操作会消耗大于2300gas:

  • 写入存储
  • 生成合约
  • 调用一个消耗大量gas的外部函数
  • 传输Ether
    和其他函数一样,回退函数能够处理复杂的操作,只要有足够的gas传输给它。

Note:
即使回退函数不能由参数,但它依然可以使用 msg.data 来提取由调用带来的任何负载。
warning:
1.没有过定义回退函数,但直接收到Ether(如通过send和transfer)的合约将会抛出一个异常并退还收到的Ether(这在0.4.0之前的版本是不同的)所以如果你的合约要收到Ether,你必须有一个回退函数。
2.一个没有payable回退函数的合约可以收到Ether,作为一笔Coinbase交易(比如说矿工挖矿)的收据,或者作为一次自毁(selfdestruct)的目标。
一个合约并不能对这样的Ehter交易做出反应,而且也同样不能拒绝它们。这是EVM设计的选择,Solidity对此并无能为力。
这也意味着this.balance能够比一些合约提供的人工账户之和还高(比如在回退函数中来一个反向更新)

pragma solidity ^0.4.0;

contract Test {
     // 该函数在所有信息发送给该合约时就被调用(因为这里没有别的函数)
     // 如果向该合约发送Ether,将会引起异常,
     //  因为回退函数没有payable标示
     function() public { x = 1; }
     uint x;
}

// 这个合约保证所有传送给他的Ether没法在拿回去
contract Sink{
     function() public payable { }
}

contract Caller {
   function callTest(Test test) public {
       test.call(0xabcdef01); // 这个哈希并不存在
       // 将导致 test.x 变成 1

       // 下面的并不会被编译,但是即使由任何人传送Ether
       // 给这个合约,交易都会失败,Ether会被拒绝接受
       // test.send(2 ether);
     }
 }

函数重载(Function Overloading)

一个合约可以由许多重名的函数但必须有不同的参数。天河一点同样适用与继承函数上。下面的例子就展示了在A合约的范围内f函数的重载。

pragma solidity ^0.4.16;

contract A {
    function f(uint _in) public pure returns (uint out) {
        out = 1;
    }

    function f(uint _in, bytes32 _key) public pure returns (uint out) {
        out = 2;
    }
}

重载函数也同样会出现在外部接口中。如果两个外部可见的函数无法通过他们的外部类型区分,而只能通过他们的Solidity类型区分的话就导致错误。

//  this will not compile
pragma solidity ^0.4.16;

contract A {
    function f(B _in) public pure returns (B out) {
         out = _in;
    }

    funciton f(address _in) public pure returns (address out) {
          out = _in;
    }
}

contract B {
}

两个重载的 f 函数最终都会接受到ABI的地址类型,尽管他们在Solidity中被认为是不同的。

重载决议和参数匹配(Overload resolution and Argument matching)
重载函数通过匹配 调用提供的参数 和 在当前辖域内声明的参数 来受到选择。如果所有参数都被隐式的转化为期望的类型,函数将被选择为重载的候选。如果没有一个确切 的 候选者,决定将失效。(简单来说就是很多重名函数,调用指令是调用哪一个的问题,如果某函数声明参数可以通过隐式的转换以匹配上调用指令,那么这个函数就是候选函数之一;如果没有一个函数能匹配上,这个调用就失效了)

Note:
对于重载决议来说,返回参数并不在考虑范围

pragma solidity ^0.4.16;

contract A {
     function f(uint _in) public pure returns (uint out) {
           out = _in;
      }

     function f(uint256 _in) public pure returns (uint256 out) {
            out = _in;
      }
}

调用 f(50)指令会产生一个类型错误,因为50即可以隐式的转换为uint8,又可以是uint256。另一方面,如果调用f(256就会决定使用f(uint256),因为256不能被隐式转换为uint8.

事件(Events)

Events allow the convenient usage of the EVM logging facilities, which in turn can be used to “call” JavaScript callbacks in the user interface of a dapp, which listen for these events.
事件允许方便的使用EVM的记录设备,这些设施能够在用于监听事件的dapp的用户交互界面中,
被轮流用于调用JavaScript的回调函数。

CALLBACK,即回调函数,是一个通过函数指针调用的函数。如果你把函数的指针(地址)作为参数传递给另一个函数,当这个指针被用为调用它所指向的函数时,我们就说这是回调函数。回调函数不是由该函数的实现方直接调用,而是在特定的事件或条件发生时由另外的一方调用的,用于对该事件或条件进行响应。
实现的机制:
[1]定义一个回调函数;
[2]提供函数实现的一方在初始化的时候,将回调函数的函数指针注册给调用者;
[3]当特定的事件或条件发生的时候,调用者使用函数指针调用回调函数对事件进行处理

事件是合约的可继承成员。当他们被调用时,他的参数将被存放于交易记录(transaction‘s log)——区块链一个特殊的数据结构——当中。这些记录和合约地址相连系,并且会被合并金区块链中,只要该块可以访问,就一直存放在那儿(forever as of Frontier and Homestead, but this might change with Serenity). 记录和事件数据在合约内部是无法访问的(即使是创造他们的合约也不行)

SPV为记录做证明是可行的,所以如果一个外部实体 提供了一个包含这种proof的合约,它将检查该记录是否在区块链中真实存在。但是注意块头部必须提供,因为合约仅能看到最近256个块的哈希值。

至多有3个参数能够获得indexed属性,使得每个参数可以被查询到:由特定值来在用户界面过滤indexed参数称为可能。

如果数列(包括 String和 bytes)被用作indexed参数,它的keccak-356 哈希将代替作为其标签(topic)。

除非你用anonymous标识符来声明一个事件,该事件签名的哈希值将是标签中的一个。这也意味这用名称来赛选特定的匿名事件是不可行的。

所有 非indexed参数都会被存放着记录的数据部分。

Note:
indexed参数不会保持它们本身。你仅能查找到这些值,但它们没办法自己取回这些值。

pragma solidity ^0.4.0;

contract ClientReceipt {
     event Deposit {
         address indexed _from,
         bytes32 indexed _id,
         uint _value
     };

     function deposit(bytes32 _id) public payable {
         // 事件通过使用’emit‘来发布,后接事件名和参数(圆括号中)
         // 任何这种调用(即使在很深的嵌套中)能够被JavaScript API
         // 通过过滤’Deposit‘察觉
         emit Deposit(msg.sender,_id,msg.value);
     }
}

它在JavaScript API中的使用大概是这样的:

var abi = /* abi as generated by the compiler */;
var ClientReceipt = web3.eth.contract(abi);
var clientReceipt = ClientReceipt.at("0x1234...ab67" /* address */);

var event = clientReceipt.Deposit();

// 变化后开始观察
event.watch(function(error,result){
     // 结果中包含许多信息,包括‘Deposit’调用需要的参数
     if (!error) 
           console.log(result);
});

// 或者传一个回调过去,立即开始观察  
var event = clientReceipt.Deposit(function(error, result) {
       if (!error)
           console.log(result);
});        

记录的低阶接口(Low-Level Interface to Logs)
通过函数 log0,log1,log2等等,可以访问到记录机制的低阶接口。logi 需要 i+1 个bytes32类型的参数,其中第一个参数会被用于记录的数据部分,其余的作为标签。上面说道的记录调用将会和下面相同的方式执行:

pragma solidity ^0.4.10;

contract C {
    function f() public payable {
          bytes32 _id = ox420042;
          log3{
              bytes32(msg.value),
              bytes32(0x50cb9fe53daa9737b786ab3646f04d0150dc50ef4e75f59509d83667ad5adb20),
              bytes32(msg.sender),
              _id
          );
     }
}

中间那串长的十六进制数,等同于keccak256("Deposit(address,bytes32,uint256"),是该事件的签名。

其他了解事件的资源
JavaScript文档
events的使用示例
怎么在js中访问他们

继承(Inheritance)

Solidity指出通过复制代码(包括多态)来实现多继承。

All function calls are virtual, which means that the most derived function is called, except when the contract name is explicitly given.
所有函数调用都是虚拟的,这意味这大部分导出函数也被调用了,除非当合约名字已经明确给出的时候。

当一个合约从多个合约继承,只有一个单个合约被生成在区块链上,而来自其他基础合约的代码将被复制到生成的合约上。

继承系统的大部分都类似于Python的继承系统,特别是关于多继承的部分。

下面的例子将给出一些细节

pragma solidity ^0.4.22;

contract owned {
     constructor() { owner = msg.sender; )
     address owner;
}

// 使用‘is’来从其他合约导出。导出合约将访问所有非私有成员,
// 包括internal函数和状态变量。这些通过this都不能被内部访问。
contract mortal is owned {
      function kill() {
          if (msg.sender == owner)  selfdestruct(owner);
     }
}

// 这些抽象合约仅仅被提供用于使接口被编译器知晓。注意该函数没有函数体。
// 如果一个合约并不实现任何函数,它仅仅能被用于一个接口
contract Config {
    function lookup(uint id) public returns (address adr);
}

contract NameReg {
     function register(bytes32 name) public;
     function unregister() public;
}

// 多继承是可以实现的。注意‘ownde’也是‘mortal’的一个基础类型,但是这里只有一个
// ‘owned‘ 的实例(相对于C++的虚拟继承而言)
contract named is owned, mortal {
     constructor(bytes32 name) {
         Config config = Config(0xD5f9D8D94886E70b06E474c3fB14Fd43E2f23970);
         NameReg(config.lokkup(1)).register(name):
     }

    // 函数可能因为另外一个同名且有相同数量/类型的函数而被覆盖。
    // 如果覆盖的函数由不同类型的输出参数,那么可能会引起error。
    // 无论本地调用和基于信息的函数调用,都要将这一点纳入考虑。
    function kill() public {
        if (msg.sender == owner) {
            Config comfig = Config(0xD5f9D8D94886E70b06E474c3fB14Fd43E2f23970);
            NameReg(config.lookup(1)).unregister();
            // 调用一个特定的覆盖函数依然是可能的
            mortal.kill();
         }
    }
}

// 如果一个构造器引入一个参数,它必须被提供在头部中
// (或者像修饰器一样声明导出合约的构造器中)
contract PriceFeed is owned,mortal,named("GoldFeed") {
    function updateInfo(uint newInfo) public {
         if (msg.sender == owner) info = newInfo;
    }

    function get() public view returns(uint r) { return info; )

    uint info;
}

注意上面的程序中,我们调用 mortal.kill() 来加快毁灭进程。这种做法是有可能有问题的,看下面的例子:

pragma solidity ^0.4.22;

contract owned {
    constructor() public { owner = msg.sender; )
    address owner;
}

contract mortal is owned {
     function kill() public {
         if (msg.sender == owner) selfdestruct(owner);
     }
}

contract Base1 is mortal {
    function kill() public ( /* do cleanup 1 */ mortal.kill();  }
}

contract Base2 is mortal {
    function kill() public { /* do cleanup 2 */ mortal.kill();   } 
}

contract Final is Base1,Base2 {
}

调用 Final.kill()将调用Base2.kill作为主要的导出覆盖(override),但这个函数将绕过Base1.kill,主要因为它甚至压根不知道Base1.解决这个问题可以使用 super:

pragma solidity ^0.4.22;

contract owned {
     constructor() public { owner = msg.sender; }
     address owner;
}

contract mortal is owned {
    function kill() public {
         if (msg.sender == owner) selfdestruct(owner);
    }
}

contract Base1 is mortal {
     function kill() public { /* do cleanup 1 */ super.kill(); }

contract Base2 is mortal {
     function kill() public { /* do cleanup 2 */ super.kill();  }
}

contract Final is Base1, Base2 {
}

如果 Base2 调用了一个声明super 的函数,他并不是简单在它的一个基础合约上调用了该函数,而是在最终的继承图上的下一个基础合约上调用该函数。所以他将会调用 Base1.kill()(注意最终继承序列为,从最外层的导出合约:Final,Base2,Base1,mortal,owned)。当使用super时,在使用它的地方的上下文中是不知道实际调用的是哪个函数,尽管它的类型是知道的。这点和普通虚函数查找(ordinary virtual method lookup)是相似的。

构造器(Constructors)
一个构造器是一种可以选择的函数,通过 constructor 关键字声明,将在合约生成后紧接着执行。构造器函数既可以是public,也可以是internal。如果合约没有构造器,合约将执行默认构造器 constructor() public{}.

pragma solidity ^0.4.22;

contract A {
    uint public a;

   constructor(uint _a) internal {
       a = _a;
   }
}

contract B is A(1) {
   constructor() public {} 
}

一个设为internal的构造器将导致合约被标记为抽象的(abstract)(后面会讲)。

Note:
在0.4.22之前,构造器被定义为一个和合约由相同名字的函数。这个语法现在已经被丢弃了。
(注意,网上大部分教程或程序还采用的这种语法)

pragma solidity ^0.4.11;

contract A { 
     uint public a;

     function A(uint _) internal {
         a = _a;
      }
}

contract B is A(1) {
    function B() public {}
}

基础构造器的参数(Arguments for Base Constructor)

所有基础合约的构造器都会被在下面介绍的初始化规则后被调用。如果基础构造器有参数,导出合约需要指定它们。可以用两种方法来实现:

pragma solidity ^0.4.22;

contract Base {
      uint x;
      constructor(uint _x) public { x = _x; }
}

contract Derived1 is Base(7) {
      constructor(uint _y) public {}
}

contract Derived2 is Base {
       constructor(uint _y) Base(_y * _y) public {}
}

第一种方法是直接在继承列表中直接指定(is Base(7))。另一个是通过一个修饰器,作为导出合约的头部的一部分(Base(_y * _y))。第一种方法在构造器参数是常数时,以及定义一个合约的行为或描述它时是更加方便的。第二个方法在基础函数的构造器参数依赖于导出函数时,只能这样使用。参数必须通过要么在继承表中,要么在导出构造器的修饰器风格 的部分中给出。但是如果同时用两种方法就会导致错误。

如果一个导出合约并不指出所有基础合约构造器的参数,他将会是一个抽象合约。

多继承和线性化(Multiple Inheritance and Linearization)
允许多继承的语言都必须面临这几个问题。一个是 Diamond 问题。Solidity和Python在使用”C3 线性化(C3 Linearization)”去强制生成基础类型的有向无环图(DAG)的一个特别顺序这点上是相似的。这一点实现了我们想要的单一化的性质,但使得一些继承图变得不可行。特别是,在is指令中给出的基础类型的顺序变得及其重要:你必须按“从最接近基础的”到“最外层导出的”的顺序列出直接的基础合约。注意这个顺序与Python中使用的顺序不一样。在下面的代码中,Solidity将给出错误“无法实现继承图的线性化(Linearization of inheritance graph impossible)”。

// this will not compile 

pragma solidity ^0.4.0;

contract X {} 
contract A is X {}
contract C is A,X {}

原因在于C要求X去覆盖A(通过按 A,X 的定义顺序),但A它本身要求覆盖X,这就形成了一个无法解决的矛盾。

继承同名函数的不同类型成员(Linearization of inheritance graph impossible Inheriting Different Kinds of Members of the Same Name)
当继承作用与一个有函数的合约和一个同名的修饰器时,会被认为是一个错误。这个错误同样也会因为一个事件和同名的修饰器,或者函数和同名的修饰器而引起。这个异常中,状态变量的getter将会覆盖public函数。

抽象合约(Abstract Contracts)

当一个合约的至少一个函数缺少实现(implementation),该函数将被标记为抽象函数,就像下面这样(注意该函数的声明头部是以;结束的):

pragma solidity ^0.4.0;

contract Feline {
     function utterance() public returns (bytes32);
}

这样的合约是无法被编译的(即使他们同时包括可实现的函数和不可实现的函数),但他们能被用作基础合约:

pragma solidity ^0.4.0;

contract Feline {
   function utterance() public returns (bytes32);
}

contract Cat is Feline {
   function utterance() public returns (bytes32) { return "miaow";  }
}

如果一个合约从一个抽象合约继承并且并不通过覆盖实现将所有非实现函数,那么它自身也是抽象的。
(我理解的非实现函数就是没有函数体的函数,无法真正运行)

注意一个没有实现的函数不同与一个(基本的)函数类型尽管他们语法十分相似。

没有实现的函数例子(函数的声明):

function foo(address) external returns (address);

函数类型的例子(一个变量的声明,变量类型为function):

function(address) external returns (address) foo;

Abstract contracts decouple the definition of a contract from its implementation providing better extensibility and self-documentation and facilitating patterns like the Template method and removing code duplication. Abstract contracts are useful in the same way that defining methods in an interface is useful. It is a way for the designer of the abstract contract to say “any child of mine must implement this method”.
抽象合约将合约的定义和它的实现分离,以提供更好的可扩展性,自文档性,像Template method一样便利的模板,以及移除代码副本。抽象函数和在接口定义函数一样有用。这是抽象函数的设计者传达“我的所有继承者都必须实现这些方法”的手段。

接口(Interface)

接口和抽象合约很像,但它们不能由任何函数实现。以及还有更多的规则:

  1. 不能继承其他的合约或者接口
  2. 不能定义构造器
  3. 不能定义变量
  4. 不能定义结构体
  5. 不能定义元组

未来也许还会提出更多的规范。

接口基本被限制为 ABI合约能够表现什么,而ABI和一个接口的对话应该可以在无信息损失的条件下实现。

接口通过他们自己的关键字来标示:

pragma solidity ^0.4.11;

interface Token {
     function transfer(address recipient, uint amount) public;
}

合约能够向继承其他合约一样继承接口。

库(Libraries)

库近似于合约,但他们的目的是他们仅在特定的地址部署(deployed)一次,而他们的代码可以使用EVM的DELEGATECALL特性来复用(CALLCODEuntil Homestead)。
This means that if library functions are called, their code is executed in the context of the calling contract, i.e. this points to the calling contract, and especially the storage from the calling contract can be accessed这意味这一旦库函数被调用,他们的代码将在调用合约的上环境中执行,比如说用this指向来调用合约,以及尤其是调用合约的storage能够被访问。(这句完全没搞懂)
当一个库是一个孤立的源代码片,它仅能访问调用合约的状态变量,还要在他们被显式提供的前提下(否则它连这些变量的名字都不知道)。如果库函数不修改它们的状态(比如当他们是 view 或 pure 函数),它们只能被直接调用(即不使用DELEGATECALL),因为库被假定为无状态的。特别的,除非Solidity类别系统被绕过,摧毁一个库是不可行的、

库可以看做使用它们的合约的一个隐式的基础合约。它们不会在遗传阶级体系中显式可见,但调用库函数看起来就和调用显式的基础函数(如果L是库名,调用为L.f())一样。此外,库的internal函数在所有合约中都可见,就像库是一个基础合约。当然,对internal函数的调用使用internal调用规范,这意味着所有internal类型的都能被传递,而且内存类型将会被通过引用传递而非复制。要在EVM中实现这一点,internal库函数的代码和所有被从同一点调用的函数将在编译时被放入调用合约,并采用一个常规的JUMP调用替代DELEGATECALL。

下面的例子阐明了怎样使用库(但注意阅读后面的using for 部分来得到更多更进一步的关于实现一个集合的例子):

pragma solidity ^0.4.22;

library Set {
    // 我们定义了一个新的结构数据类型,它将被用于在调用合约中保存其数据
    struct Data { mapping(uint => bool) flags; }

    // 注意下面的第一个参数是“内存引用”类型,而且因此仅仅只有它的storage地址
    // 而非它的内容被作为调用的一部分传递过去。如果该函数能被视为其实例的一种方法
    // 一般惯例将第一个参数称为“self”
    function insert(Data storage self, uint value)
         public 
         returns (bool)
    {
         if (self.flags[value])
            return false;
         self.flags[value] = true;
         return true;
     }   

     function remove(Data storage self, uint value)
          public 
          returns (bool)
     {
          if (!self.flags[value])
                return false;
          self.flags[value] = false;
          return true;
     }

     function contains(Data storage self, uint value)
         public 
         view 
         returns(bool)
      {
         return self.flags[value];
      }
}

contract C {
     Set.Data  knownValues;

     function register(uint value) public {
         // 库函数能够不需要一个库的特定示例就可以调用,
         // 因为这个“实例”就是当前合约
         require(Set.insert(KnownValues, value));
     }
     // 在这个合约中,如果我们想也可以直接访问knownValues.flags
}

当然,我们不必要非要遵从这个方式来使用库:他们也同样能在不定义结构数据类型Data的情况下使用。函数同样可以在没有任何storage索引参数的情况下运行,而且他们也能够有多种storage索引参数,而且可以在任何地方。

调用指令Set.contains,Set.insertSet.remove都是被编译为对外部合约/库的调用(DELEGATECALL)。如果你使用库,注意这里是使用了一个实际的外部函数调用,尽管msg.sender,msg.valuethis这些指令在调用时将保持他们的值。

下面的例子展示了为了实现常见的类型而不经常使用外部函数调用,怎样在库中使用memory类型以及internal函数:

pragma solidity ^0.4.16;

library BigInt {
    struct bigint {
         uint[] limbs;
    }

    function fromUint(uint x) internal pure returns (bigint r)  {
          r.limbs = new uing[](1);
          r.limbs[0] = x;
     }

     function add(bigint _a, bigint _b) internal pure returns (bigint r) {
          r.limbs = new uint[](max(_a.limbs.length,_b.limbs.length));
          uint carry = 0;
          for (uint i = 0; i <r.limbs.length; ++i) {
               uint a = limb(_a, i);
               uint b = limb(_b, i);
               r.limbs[i] = a + b + carry;
               if (a + b < a || (a + b == uint(-1) && carry >0))
                    carry = 1;
               else 
                    carry = 0;
          }
          if (carry > 0) {
              // too bad, we have to add a limb
              uint[] memory newLimbs = new uint[](r.limbs.length + 1);
              for ( i = 0; i < r.limbs.length; ++i)
                    newLimbs[i] = r.limbs[i];
              newLimbs[i] = carry;
              r.limbs = newLimbs;
          }
      }

      function limb(bigint _a, uint _limb) internal pure returns (uint) {
              return _limb < _a.limbs.length ? _a.limbs[_limb] : 0;
      }

      function max(uint a ,uint b) private pure returns (uint) {
           return a > b ? a : b;
      }
 }
contract C {
      using BigInt for BigInt.bigint;

      function f() public pure {
         var x = BigInt.fromUint(7);
         var y = BigInt.fromUint(uin(-1));
         var z = x.add(y);
      }
} 

因为编译器无法知道库会被部署在哪儿,这些地址必须通过一个连接程序全部装入最后的字节码内。如果地址没有被作为参数给于编译器,被编译的十六进制码会包含_set______形式的占位符(Set位置是库的名字)。地址能被通过用十六进制编码的库合约地址替换所有的40个符号来手动填满。

库对比合约由一下几个规范:

  • 没有状态变量
  • 不能继承或被继承
  • 不能收到Ether

库的调用保护(Call Protection For Libraries)
如在介绍中提到的一样,如果一个库的代码使用CALL来执行而不是DELEGATECALL 或者 CALLCODE,除非调用的是 view 或 pure函数,它都会复原。

EVM并不提供一个直接的方法给合约去判断是否使用CALL调用,但是合约可以使用ADDRESS操作代码去了解他到底实际运行在哪里。生成的代码将该地值和构建期间使用的地址进行对比,来了解调用的模式。

More specifically, the runtime code of a library always starts with a push instruction, which is a zero of 20 bytes at compilation time. When the deploy code runs, this constant is replaced in memory by the current address and this modified code is stored in the contract. At runtime, this causes the deploy time address to be the first constant to be pushed onto the stack and the dispatcher code compares the current address against this constant for any non-view and non-pure function.
更具体的,库运行时的代码总是由一个push指令开始,该指令一般在编译阶段为20字节中的0。当部署的代码运行是,在内存中这个实例由当前地址所替换,而且这个修正代码被保存在合约中。运行时,这使得部署时的地址成为第一个被push进栈的实例,而调度代码比较所有非-view和pure函数的当前地址后发现和实例不符。

Using For

指令using A for B;能被用于将库函数(来自库A)依附到任意类型(B)上去。这些函数将收到他们调用的对象作为他们的第一个参数(就像Python中self变量)

using A for *;的作用是将来自库A的函数依附于任意类型上。

在两种情况下,所有函数,即使那些第一个参数并不匹配对象类型的函数,都会被依附。类型将会在函数被调用的地方检查,此时函数重载决定将其作用。

using A for B;指令将在当前辖域内起作用。目前该范围暂时限制在合约内但以后可能扩展到全局,所以包括模型以及库函数在内的数据类型都是可用的,不需要增加更多的代码。

我们来重写开始库那一章的set例子:

pragma solidity ^0.4.16;

//This is the sanme code as before,just without comments
library Set {
    struct Data { mapping(uint => bool) flags; }

    function insert(Data storage self, uint value)
          public
          returns (bool)
    {
       if (self.flags[value])
           return false;
        self.flags[value] = true;
        return true;
     }

    function remove(Data storage self, uint value)
        public 
        retruns (bool)
    {
        if (!self.flags[value])
             return false;
        self.flags[value] = false;
        return true;
     }

     function contains(Data storage self, uint value)
         public 
         view 
         returns (bool)
     {
        return self.flags[value];
     }
}

contract C {
      using Set for Set.Data;// 这里是最初的变化
      Set.Data knownValues;

      function register(uint value) public {
         //  这里,所有Set.Data类型的变量有了相应的成员函数
         //  下面的函数调用被识别为‘Set.insert(knownValues, value)'
         require(kownValue.insert(value));
       }
}

也可以用这样的方法来扩充基本类型:

pragma solidity ^0.4.16;

library Search {
     function indexOf(uint[] storage self, uint value)
           public 
           view
           return (uint)
     {
          for (uint i = 0; i < self.length ; i ++)
               if (self[i] == value) return i;
          return uint(-1);
     }
}

contractr C {
      using Search for uint[];
      uint[] data;

      function append(uint value) public {
           data.push(value);
      }

      function replace(uint _old, uint _new) public {
          // This performs the library function call
          uint index = data.indexOf(_old);
          if (index == uint(-1))
              data.push(_new);
          else
              data[index] = _new;
      }  
}        

注意所有库的调用实际是都是EVM函数调用。这意味着如果你传递memory或value类型,会生成一个拷贝,即使是self变量也会。唯一不会生成拷贝的环境是当使用storage指示变量时。

猜你喜欢

转载自blog.csdn.net/weixin_42595515/article/details/82012662