solidity中的继承杂谈

前言

最近研究了一下solidity这门用于编写智能合约的语言,感觉很多特性还是非常有意思,前两天看了知道创宇发的蜜罐智能合约分析,其中谈到的很多方面都非常有趣,关于里面提到的solidity的变量覆盖的bug我也发文研究过,所以本文就来谈谈solidity里继承的一些tricks

 

讲讲基础

首先我们来认识一下solidity中的继承体系,其实跟python中的继承差不多,只是python中不能多继承,但是solidity的主要特点是它支持多继承。另外我们还得知道在solidity中合约的继承其实类似于代码的拷贝,简单来讲就是将其继承的基合约的代码拷贝过来放在子合约里,这样给我的感觉倒是跟import差不多,当然特性上还是不同的

下面我们来先来看一个简单的例子

pragma solidity ^0.4.24;
contract A {
    uint256 public a=123;
    function test1() {
        a=233;
    }
}
contract B is A{
    uint256 public b=456;
    function test2(){
        b=433;
    }
}

这就是一个最简单的继承关系,下面展示了该合约刚部署和调用test1和test2函数后的状态



可以看到确实就是很直接的调用,A的代码直接拷贝到了B合约里执行,下面我们来看看B合约存储位的分布,这是刚初部署时的结果
很清楚的看到a变量占据着0存储位而b变量则占据1存储位,这也证实继承后合约的代码就是按照由A至B顺序执行的
另外在solidity中也可以快速地使用基合约派生出一个子合约出来,比如下面这个例子

contract S {
    address p;
    uint256 a;
    function S(uint256 _a) {
        p=msg.sender;
        a = _a; 

    }
}
contract  s is S(100) {
    function s()  {
    }
}

这里主要是用构造函数来利用基合约初始化我们的子合约,下面是部署的结果

 

讲讲重点

下面就是我想说的重点了,solidity中关于继承关系还是挺奇妙的,这里推荐看看这里的资料Solidity原理(一):继承(Inheritance),写的还是比较清楚的,下面我也是基于资料里的介绍做一些重点的分析

重名的情况

首先我们来看看如果子合约与基合约存在函数或者变量名称相同将会出现怎样的冲突

pragma solidity ^0.4.24;

contract own {
    address owner;
    function own(){
        owner=msg.sender;
    }
}
contract A is own{
    address owner=msg.sender;
    function change(){
        owner=msg.sender;
    }
}

此处我们的子合约与基合约都有owner变量,然后我们可以在A合约里修改owner变量,下面是部署后的状态

很有意思,我们可以看到有两个存储位被占用了,说明事实上这里是初始化了两个woner变量,然后我们来调用A里的change函数,存储位里变量的变化如下图

其中仅有1号存储位里的owner被修改,证明这里change调用的只是A合约里的owner变量,因为EVM里实际上还是根据storage的位置来分辨变量的,所以这里虽然变量名称在我们看来相同,但在EVM虚拟机里却是完全不同的两个变量,这个特性也在一些蜜罐合约里得到了体现,之前也是看到这种蜜罐合约感觉非常有意思,不熟悉这一特性的人可能就会把基合约跟子合约里的变量搞混结果上当受骗

上面提到的是变量重名,当然还有函数重名,这部分的规则其实也较简单,一种情况是子合约含有与基合约同名的函数,且没有调用基合约的该函数,此时基合约的该同名函数就直接被子合约的重名函数重写,基合约自身的同名函数被抛弃,另一种情况则是子合约在含有同名函数的情况下还调用了基合约的该函数,此时基合约的函数将转换为private函数得到从而得到保留,这跟其它的编程语言性质挺类似的,就不再多说了

此外还有几点值得我们注意,虽然solidity的继承里允许同名的变量与函数,但是如果出现了同名的modifier与函数是不被允许的,在编译器里也会直接报错,如下

另外事件event也是不被允许与modifier和函数同名,如果出现这样的情况同样会直接在编译器里报错,然而凡事总是会有那么几个例外,在solidity中我们就可以利用其为public创建的getter函数来覆盖同名的函数

这里涉及的机制主要是目前solidity的编译器会自动为合约中的public变量来创建一个getter函数,比如下面这个简单的合约

pragma solidity ^0.4.24;

contract A {
    uint256 public a=123;
    uint256 public b;
    function gg(){
        b=this.a();
    }
}

这里的this.a()就是编译器自动创建的getter函数,它是个无参且返回值为a的函数,这里我们是模拟外部调用,所以使用了this来调用它,这种情况下a就是可以作为函数来调用的,而其有趣的地方还在于它可以覆盖继承的基合约里的同名函数,这就相当于拿变量覆盖了函数,我们来看下面这个例子

pragma solidity ^0.4.24;

contract A {
    function D() public returns(uint256){
        return 456;
    }
}
contract B{

        uint public D=123;
}

contract C is A,B{
    uint256 public p;
    function test(){
        p=this.D();
    }
}

在C合约里我们调用了D函数,根据继承关系本来我们此处应该使用的是A合约里的D函数,然而事实上结果如下图

调用了test函数过后p的值却变成了B合约里D变量的值,所以此处实际上调用的是B合约中D变量的getter函数,从而成功实现了使用变量覆盖函数,可以说是非常有趣了,感觉也可以跟前面特性一起应用到蜜罐合约里去

solidity中的多继承

接下来我们再来看看solidity中的多继承,最基本的多继承其实就跟上面的单继承类似,因为没有重写,可以当作就是几段基合约的代码拷贝到子合约里执行

对于复杂点的情况,首先需要我们注意的是多继承里存在的派生子合约里的重写,其实也是上面提到的重名的情况,比如下面这个例子

pragma solidity ^0.4.24;

contract A {
    uint256 public a;
    function test() {
        a=233;
    }
}
contract B {
    uint256 public b;
    function test(){
        b=433;
    }
}
contract C is A,B {
    function C() {
        test();
    }
}

我们拿C合约继承A与B合约后调用了A与B存在的一个同名函数,此时实际上会执行的仅有B合约中的test函数,因为实际上A合约里的test已经直接被B的test覆盖了,这一段在子合约C里已经不存在了,下面是执行的结果当然如果你确实想要同时使用A和B中的同名合约,一个可靠的方法就是使用super,比如我们将上面的合约改造成一个菱形继承的模型

pragma solidity ^0.4.24;
contract S {
    uint256 public s;
    function test(){
        s=111;
    }
}
contract A is S{
    uint256 public a;
    function test() {
        a=233;
        super.test();
    }

}
contract B is S{
    uint256 public b;
    function test(){
        b=433;
        super.test();
    }
}
contract C is A,B {
    function C() {
        test();
    }
}

得到结果此时子合约中的test也都得到了执行,同时通过debug模式可以知道执行的顺序为B->A->S

 

讲讲感想

基本上想说的就这么多,主要也是感觉solidity里关于继承的一些特性还是挺有趣的,同时最近的一些研究也让我感觉solidity确实还是太不成熟,可能也是还比较年轻吧,同时正式版也迟迟未发布,就目前看来存在的神奇的机制还是挺多的,也无怪乎很多人一直在唱衰它,希望后面它的更新能消除存在的这些神奇机制

另外我自己其实也还是处在不断学习的状态,上面写的如有问题还请多多指教,还迎交流想法

审核人:yiwang   编辑:边边

(完)