类似于面向对象的编程,Solidity 作为一种面向合约的语言,继承和多态特性被广泛采用,对语言的演进至关重要。几乎每个 Solidity 开发人员都在他们的合约中使用过这些语言特性来解耦逻辑并提高代码重用率。在语言的 0.6 版本中,引入的主要改进是除了引入接口继承和禁止危险的状态变量隐藏之外,还使现有的规则更加明确。编译器继续使用 C3 线性化,请参见Solidity 文档了解继承。
显式virtual 和 override
函数不再默认是虚拟的。这意味着对非虚拟函数的调用将始终执行该函数,而不管继承层次结构中的任何其他合约。这减少了 0.5 版本中所有函数隐式虚拟带来的歧义,允许在继承结构中进一步覆盖。这在大型继承图中尤其危险,因为这种歧义可能导致意外的行为和错误。
例如,在下面的合约 C 中,调用 setValue 将调用最派生的实现,该实现来自合约 B。但是,从实现上看,这一点并不明显。
pragma solidity ^0.5.17; contract A { uint public x; function setValue(uint _x) public { x = _x; } } contract B { uint public y; function setValue(uint _y) public { y = _y; } } contract C is A, B { }
对于上面的示例,0.6 版本的编译器会引发类型错误:派生合约必须覆盖函数“setValue”。两个或多个基类定义了名称和参数类型相同的函数。以上是多重继承的示例,其中从多个基类(A 和 B)继承了相同的函数。在这种情况下,必须覆盖它,并且必须在覆盖说明符中列出基类。需要注意的是,override(A,B) 中的顺序无关紧要——具体来说,它不会改变 super 的行为——这仍然由继承图的 C3 线性化决定,该线性化由 contract C is A, B { ... } 声明中的顺序决定。
pragma solidity ^0.6.10; contract A { uint public x; function setValue(uint _x) public virtual { x = _x; } } contract B { uint public y; function setValue(uint _y) public virtual { y = _y; } } contract C is A, B { function setValue(uint _x) public override(A,B) { A.setValue(_x); } }
请注意,只有在函数被标记为 virtual 时,才能覆盖它们。此外,任何覆盖函数都必须标记为 override。如果再次需要可覆盖,则也必须将其标记为 virtual。
interface 函数隐式地为 virtual,因此在实现接口时,必须在实现中显式地覆盖其函数。关于此设计的讨论正在进行中,此处。
值得注意的是,关键字 super 的工作方式与之前相同:它在扁平化的继承层次结构中向上调用一个级别的函数。此外,以下内容也保持不变:在 external 函数上仍然不允许使用 super。
接口可以继承
此功能是 0.6 版本的新功能,允许接口继承。生成的接口是所有继承接口函数的组合,合约必须实现这些函数。
pragma solidity ^0.6.10; interface X { function setValue(uint _x) external; } interface Y is X { function getValue() external returns (uint); } contract Z is Y { uint x; function setValue(uint _x) external override { x = _x; } function getValue() external override returns (uint) { return x; } }
请注意,如果合约未实现所有函数,则必须将其标记为 abstract。
pragma solidity ^0.6.10; abstract contract Z is Y { uint x; function setValue(uint _x) external override { x = _x; } }
抽象合约
在 0.5 版本中,未实现所有函数的合约会由编译器隐式地设为抽象。
pragma solidity ^0.5.17; contract X { function setValue(uint _x) public virtual; }
在 0.6 版本中,这种区分必须是显式的,否则编译器会生成错误 合约 X 应设为抽象
pragma solidity ^0.6.10; abstract contract X { function setValue(uint _x) public virtual; }
公共变量更安全的外部函数覆盖
尽管此功能在 0.6 之前就已经存在,但现在更安全,因为增加了检查以确保变量的 getter 函数(由编译器生成)与被覆盖的外部函数的参数和返回类型匹配。在 0.5 版本中,允许这些类型不同,如下例所示
pragma solidity ^0.5.17; interface A { function f() external pure returns(uint8); } contract B is A { uint256 public f = 257; }
合约 A 对底层合约 B 的调用将返回 1,因为 257 值在转换为 uint8 时会溢出。
现在,0.6 版本会生成 TypeError: 覆盖公共状态变量的返回类型不同,迫使我们解决类型冲突,从而避免溢出
pragma solidity ^0.6.10; interface A { function f() external pure returns(uint256); } contract B is A { uint256 public override f = 257; }
请注意,public 状态变量只能覆盖 external 函数,并且变量仍然不允许覆盖 internal 或 public 函数。
禁止状态变量隐藏
在 0.5 版本中,继承具有相同名称的可见状态变量是允许的,并且仅由某些静态分析工具作为问题提出。以下示例演示了此设计的问题
pragma solidity ^0.5.17; contract A { uint public x; function setValue1(uint _x) public { x = _x; } } contract B is A { uint public x; function setValue2(uint _x) public { x = _x; } }
在此上下文中,合约 A 中引用 x 的函数使用的是该状态变量自己的实例——A.x,而对于 B 则是 B.x。因此,调用 B.setValue2(100) 的结果将是 B.x 将被设置为 100,而调用 B.setValue1(200) 将会设置 A.x 为 200。
在 0.6 版本中,这现在是被禁止的,并会引发编译器 DeclarationError: 标识符已声明 错误。