1. 程式人生 > >Solidity原理(一):繼承(Inheritance)

Solidity原理(一):繼承(Inheritance)

首先看一段官網的描述:

Solidity supports multiple inheritance by copying code including polymorphism.

When a contract inherits from multiple contracts, only a single contract is created on the blockchain, and the code from all the base contracts is copied into the created contract.

The general inheritance system is very similar to 

Python’s, especially concerning multiple inheritance.

翻譯過來是:Solidity支援多繼承和多型,實驗的方式是通過直接的程式碼拷貝。當一個合約從多個合約繼承的時候,只有一個合約(子類)會被部署到區塊鏈上,而其他的程式碼會被拷貝到這個單一的合約中去。Solidity的繼承方式和Python的極為類似,主要的區別在與Solidity支援多繼承,而Python不支援。

重點:Solidity的繼承原理是程式碼拷貝,因此換句話說,繼承的寫法總是能夠寫成一個單獨的合約。下面舉幾個例子來說明:(判斷兩個合約完全相同的原理是編譯兩個合約,比較它們的bytecode除了swarm hash部分是否相同)

例子一:沒有重寫和過載,上下兩種情況是等價的,直接把父類(A)的函式拷貝到子類(B)中

contract A{
    function test1(uint[16] a) returns(uint){
       return a[0];
    }
   function test2(bytes a) returns(byte){
       return a[0];
    }
}
contract B is A{ //Solidity的繼承是通過is關鍵字實現的,支援多繼承
    function test3(address a) returns(address){
       return a;
    }
}
==========================
contract B{
    function test1(uint[16] a) returns(uint){
       return a[0];
    }
   function test2(bytes a) returns(byte){
       return a[0];
    }
    function test3(address a) returns(address){
       return a;
    }
}

例子二:沒有重寫,但是有函式過載,上下兩個合約是完全等價的,和例子一其實也是幾乎一樣

原因解釋:首先,先解釋一下EVM如何定位一個hash值。當原始碼編譯成了bytecode後,Solidity對每個public函式會有一個唯一標識的hash,這個hash值是通過函式名,引數型別來確定的(引數順序有關)。函式的hash值與是否有返回值無關,與引數名字無關。因此對於EVM來說,過載的函式如果引數型別不一樣,其實是兩個完全不一樣的函式,哪怕都叫test2。也就和情況一完全相同了。

contract A{
    function test1(uint[16] a) returns(uint){
       return a[0];
    }
   function test2(bytes a) returns(byte){
       return a[0];
    }
}
contract B is A{
    function test2(address a) returns(address){
       return a;
    }
}
==================
contract B {
    function test1(uint[16] a) returns(uint){
       return a[0];
    }
   function test2(bytes a) returns(byte){
       return a[0];
    }
    function test2(address a) returns(address){
       return a;
    }
}

情況三:父類函式被重寫,但是子類未呼叫父類被重寫的函式。下面的例子中,父類A和子類B中有一個完全一樣的test2,也就是說他們有相同的hash值。他們不存在呼叫關係,因此父類A中的test2被拋棄,等同於下面單個函式B的寫法

contract A{
    uint public variable = 0;
    function test1(uint a)  returns(uint){
        variable++;
       return variable;
    }
   function test2(uint a)  returns(uint){
       variable += a;
       return variable;
    }
}
contract B is A{
    function test2(uint a) returns(uint){
        variable++;
        return variable;
    }
}
==========================
contract B {
    uint public variable = 0;
    function test1(uint a)  returns(uint){
       variable++;
       return variable;
    }
    function test2(uint a) returns(uint){
        variable++;
        return variable;
    }
}

情況四:子類重寫父類的函式,並且呼叫重寫的函式。 可以看到子類B重寫了父類A的test1函式。兩個函式是具有相同的hash值的,因此EVM的做法是把父類中被呼叫的函式轉換成一個private函式,copy到子類中。以呼叫private函式的形式來實現繼承。可以看到上下兩種寫法的bytecode是相同的,被呼叫的父類A中的test1函式被改成了private函式(private函式不具備hash值,因此叫test2,test3不對對bytecode有任何影響)。

contract A{
    function test1(uint a) returns(uint){
        a += 6;
        return a; 
    }
}
contract B is A{
    uint v = 0;
    function test1(uint a) returns(uint){
        uint tmp = A.test1(a);
        v+=tmp;
    }
}
==========================
contract B {
    uint v = 0;
    function test2(uint a) private returns(uint){
        a += 6;
        return a;
    }
    function test1(uint a) returns(uint){
        uint tmp = test2(a);
        v+=tmp;
    }
}

情況五:子類父類有相同名字的變數。 父類A的test1操縱父類中的variable,子類B中的test2操縱子類中的variable,父類中的test2因為沒被呼叫所以不存在。

解釋:對EVM來說,每個storage variable都會有一個唯一標識的slot id。在下面的例子說,雖然都叫做variable,但是從bytecode角度來看,他們是由不同的slot id來確定的,因此也和變數叫什麼沒有關係。

contract A{
    uint variable = 0;
    function test1(uint a)  returns(uint){
       variable++;
       return variable;
    }
   function test2(uint a)  returns(uint){
       variable += a;
       return variable;
    }
}
contract B is A{
    uint variable = 0;
    function test2(uint a) returns(uint){
        variable++;
        return variable;
    }
}
====================
contract B{
    uint variable1 = 0;
    uint variable2 = 0;
    function test1(uint a)  returns(uint v){
        variable1++;
       return variable1;
    }
    function test2(uint a) returns(uint v){
        variable2++;
        return variable2;
    }
}

多繼承:

Solidity的繼承和Python類似,但是支援多繼承, 接下來用幾個例子分析一下多繼承

第一個例子:

pragma solidity ^0.4.22;

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

// Use `is` to derive from another contract. Derived
// contracts can access all non-private members including
// internal functions and state variables. These cannot be
// accessed externally via `this`, though.
contract mortal is owned {
    function kill() {
        if (msg.sender == owner) selfdestruct(owner);
    }
}

// These abstract contracts are only provided to make the
// interface known to the compiler. Note the function
// without body. If a contract does not implement all
// functions it can only be used as an interface.
contract Config {
    function lookup(uint id) public returns (address adr);
}

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

// Multiple inheritance is possible. Note that `owned` is
// also a base class of `mortal`, yet there is only a single
// instance of `owned` (as for virtual inheritance in C++).
contract named is owned, mortal {
    constructor(bytes32 name) {
        Config config = Config(0xD5f9D8D94886E70b06E474c3fB14Fd43E2f23970);
        NameReg(config.lookup(1)).register(name);
    }

    // Functions can be overridden by another function with the same name and
    // the same number/types of inputs.  If the overriding function has different
    // types of output parameters, that causes an error.
    // Both local and message-based function calls take these overrides
    // into account.
    function kill() public {
        if (msg.sender == owner) {
            Config config = Config(0xD5f9D8D94886E70b06E474c3fB14Fd43E2f23970);
            NameReg(config.lookup(1)).unregister();
            // It is still possible to call a specific
            // overridden function.
            mortal.kill();
        }
    }
}

// If a constructor takes an argument, it needs to be
// provided in the header (or modifier-invocation-style at
// the constructor of the derived contract (see below)).
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;
}

第二個例子:下面的這個例子中,Final同時繼承了Base1和Base2,當final執行test()函式後,v0=1,v1=0,v2=1;也就是說Base1中的test()函式沒有被執行。通過檢視Bytecode發現,Base1中的test()根本沒有被繼承,而是直接被忽略了。為了解決這個問題,可以用super.來解決。請看第三個例子

pragma solidity ^0.4.22;
contract set {
    uint public v0 = 0;
    function test() public {
        v0++;
    }
}

contract Base1 is set {
    uint public v1 = 0;
    function test() public { 
        v1++;
        set.test();
    }
}

contract Base2 is set {
    uint public v2 = 0;
    function test() public {
        v2++;
        set.test();
    }
}

contract Final is Base1, Base2 {
}

例子三:這個例子只是把上個例子的set.set()換成了super.test(),執行後v0,v1,v2都等於1,也就是說全都被執行了1次。通過分析bytecode可知執行順序是Base2->Base1->set
pragma solidity ^0.4.22;
contract set {
    uint public v0 = 0;
    function test() public {
        v0++;
    }
}

contract Base1 is set {
    uint public v1 = 0;
    function test() public { 
        v1++;
        super.test();
    }
}

contract Base2 is set {
    uint public v2 = 0;
    function test() public {
        v2++;
        super.test();
    }
}

contract Final is Base1, Base2 {
}