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
翻譯過來是: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 {
}