solidity快速入門(擴展篇)

擴展篇

1.繼承、多態、super 與 C3 線性化

solidity支持繼承,並且支持多重繼承。

下面的代碼展示了基本的繼承的作用:代碼重用。

contract ERC20Token{
    string public name;
    string public symbol;
    uint256 public decimals;
	uint256 public totalSupply;
    mapping (address => uint256) public balanceOf;
    mapping (address => mapping (address => uint256)) public allowance;

	event Transfer(address indexed _from, address indexed _to, uint256 _value);
    event Approval(address indexed _owner, address indexed _spender, uint256 _value);
    
    //基類裏可以不給實現,只做函數聲明。也可給一個實現,子類根據情況,選擇直接使用用或者重新寫一個實現覆蓋父類的
    function transfer(address _to, uint256 _value) public returns (bool success);
    function transferFrom(address _from, address _to, uint256 _value) public returns (bool success);
    function approve(address _spender, uint256 _value)public returns (bool success);
}

contract TokenA is ERC20Token {
	function MyToken(string _name, string _symbol, uint256 _decimals) public{
        name = _name;
        symbol = _symbol;
        decimals = _decimals;
    }
    //如果父類只聲明瞭該方法,此處爲實現。若父類實現了,此處會覆蓋父類的實現。
    function transfer(address _to, uint256 _value) public returns (bool success){
        
    }
}

contract TokenB is ERC20Token {
		.....
		.....
}

假如ERC20Token裏的方法只給了聲明,那ERC20Token就是一個抽象合約,該合約不能被部署只能做父類。
接下來就是由繼承+重寫(override)而引出的多態:

contract Base{
    function Func() public pure returns(uint256){
        return 11112222;
    }
}

contract Drived is Base{
    function Func() public pure returns(uint256){
        return 33334444;
    }
}

contract TestContract{
    function TestContract() public{
        
    }
    function TestPolymorphism(address subClassAddr) public pure returns(uint256, uint256){
        Drived objDrived = Drived(subClassAddr);
        Base objBase     = Base(subClassAddr);
        
        return (objDrived.Func(), objBase.Func());
    }
}

我們先部署Base、Drive兩個合約,分別得到合約地址baseAddr、drivedAddr,再部署TestContract合約,分別把兩個地址做參數調用TestPolymorphism方法,結果如下:

  • 使用baseAddr做參數,輸出爲:
    11112222
    11112222
    *使用drivedAddr做參數,輸出爲:
    33334444
    33334444
    有多重繼承,及不可避免地會引起棱形繼承,看下面代碼:

contract Ancestor{
    function Func(uint256 val)public pure returns(uint256){
        return val;
    }
}
contract BaseA is Ancestor{
    function Func(uint256 val)public pure returns(uint256){
        return Ancestor.Func(val * 2);
    }
}
contract BaseB is Ancestor{
    function Func(uint256 val)public pure returns(uint256){
        return Ancestor.Func(val * 4);
    }
}
contract Final is BaseA, BaseB {
    
}

我們部署了Final合約之後,該合約對外只有一個可以調用的方法:Func,使用一個數字N做參數,輸出會是多少呢?答案是4N。如果把繼承的順序改爲 contract Final is BaseB, BaseA ,則輸出的結果便是2N。solidity在處理多繼承的時候,使用C3 Linearization規則來處理多重繼承問題(類似於Python)。有一個結構叫繼承圖(inheritance graph),對於第一種情況,繼承圖是這樣的:Final-BaseB-BaseA-Ancestor。所以調用Final.Func實際上調用的是BaseB裏的Func。(關於C3線性化得另外寫一篇博客了)
接下來看看神奇的super

contract BaseA is Ancestor{
    function Func(uint256 val)public pure returns(uint256){
        return super.Func(val * 2);
    }
}
contract BaseB is Ancestor{
    function Func(uint256 val)public pure returns(uint256){
        return super.Func(val * 4);
    }
}
contract Final is BaseA, BaseB {
    
}

部署了Final之後調用Func方法,傳入N,結果是8N。爲啥?super不是簡單的調用所在的合約的父合約,而是調用繼承圖中的下一個合約裏的方法:繼承圖爲Final-BaseB-BaseA-Ancestor,BaseB中的super.Func實際上調用的是BaseA.Func,所以最終的N被放大了4*2=8倍。

2. throw、require、assert、revert

require與assert都是用來進行條件過濾的,當裏面的條件表達式爲false時,拋出異常回退整個交易。二者本質的區別在於:

  • require(opcode爲0xfd)會直接異常,已用掉的gas送給曠工,未使用的gas返回給交易發起者。
  • assert(opcode爲0xfe)也是拋出異常,同時消耗掉交易發起者提供的 所有 gas。

使用者可根據以上特點酌情使用兩者。比如,在需要過濾惡意調用的地方使用assert,能夠增加hacker的攻擊成本。
throw和revert兩個同樣使用的是0xfd操作碼,跟require是一樣的。但是在0.4.10版本之前,throw用的是0xfe。所以不推薦使用throw而推薦用revert代之,因爲throw會在不同版本的編譯器上體現不同的行爲。

3. 合約間的訪問:call、callcode、delegatecall

當業務變得複雜時,一個合約很難實現完整,這時就會設計多個合約間的相互調用。通常情況下我們使用的都是 call 這種方式:也就是

contract B{
...
contractA.funcA();//等價於 address(contractA).call(keccak256("funcA()"));
...
}

只是去要注意的是,call方法在執行時,若被調函數出現異常,call方法本身並不異常,而是返回false。

callcode與 call的區別在於被調函數執行的上下文的不同:callcode會讓被調函數的代碼運行在當前合約的上下文中,假如上述例子中funcA改變了狀態,則使用callcode調用contractA.funcA會將改變的狀態寫在當前合約(contractB)中,而不是contractA中。
delegatecall 與 callcode很像,不同點在於,delegatecall 的msg.sender不同:舉例說明:

pragma solidity 0.4.24;
contract A{
	uint256 public data;
	address public sender;
	function setData(uint256 x)public{
		data = x;
		sender = msg.sender;
	}
}
contract B{
    uint256 public data;
    address public sender;
    function invoke_call(address addr)public{
        require(addr.call(bytes4(keccak256("setData(uint256)")), 10));
    }
    function invoke_callcode(address addr)public{
        require(addr.callcode(bytes4(keccak256("setData(uint256)")), 11));
    }
    function invoke_delegatecall(address addr)public{
        require(addr.delegatecall(bytes4(keccak256("setData(uint256)")), 12));
    }
}
  • 使用賬戶owner 部署A合約、B合約
  • 調用 B.invoke_call(addrA),結果: A.data = 10; A.sender = addrB;
  • 調用 B.invoke_callcode(addrA),結果:B.data = 11; B.sender = addrB;
  • 調用 B.invoke_deletgatecall(addrA),結果:B.data = 12; B.sender = owner

結論:callcode已經不建議使用了,官方建議使用deletgatecall代替callcode,但是由於這兩者的行爲與人的直覺嚴重不符,極易造成bug(Parity多籤錢包的bug就是使用delegate的一個例子),所以個人極不推薦這兩者的使用。

4. storage、memory、stack、new、delete

  • storage: 合約的狀態變量(定義在函數外的mapping等變量)存儲的位置,花費gas較高,
  • memory:臨時變量存儲的位置,不會引起合約狀態的變化、gas費較低
  • stack: 函數內局部變量存儲的位置,免費但是有數量限制(16個slot)
  • 在函數內聲明的mapping、string等非值類型,默認是storage存儲
  • 函數內定義的局部變量過多,會導致stack溢出,複雜函數考慮使用結構體代替多個局部變量。

5. library、using for

pragma solidity ^0.4.25;

library Math
{
    function sub(uint256 a, uint256 b) public pure returns (uint256) {
        require(b <= a);
        return a - b;
    }
}

contract Test{
    using Math for uint256;
    uint256 public result;
    
    function calc(uint256 a, uint256 b)public{
        result = a - b;
    }
    
    function safeCalc(uint256 a, uint256 b)public{
        result = a.sub(b);
    }
}

使用using Math for uint256;聲明之後,
safeCalc函數內可以直接使用sub方法,防止溢出。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章