Ethereum学习笔记 ---- 多重继承中的 C3线性化算法
学习 solidity 合约多重继承时,官方文档介绍 solidity 采用 C3线性化算法
来确定多重依赖中的继承顺序。
维基百科上有很好的说明:
C3线性化
C3 linearization
下面通过实验来深入理解一下。
举个反例
在 c3_notok.py 中定义如下类
class Type(type):
def __repr__(cls):
# Will make it so that repr(O) is "O" instead of "<__main__.O>",
# and the same for any subclasses of O.
return cls.__name__
class O(object, metaclass=Type): pass
class A(O): pass
class X(O, A): pass
if __name__ == '__main__':
print(X.mro())
执行会 throw error:
$ python3 c3_notok.py
TypeError: Cannot create a consistent method resolution
order (MRO) for bases O, A
分析错误原因
根据 C3 线性化算法,计算 X 的 mro (Method Resolution Order):
L(O) := [O]
L(A) := [A] + merge(L(O), [O])
:= [A] + merge([O], [O])
:= [A, O]
L(X) := [X] + merge(L[O], L[A], [O, A])
:= [X] + merge([O], [A, O], [O, A])
可以看到在最后一步计算中, O
和 A
在 [A, O]
, [O, A]
两个列表中分别为 head 和 tail,算法无法继续进行下去,所以 class X(O, A)
的定义不合法。
举个正例
在 c3_ok.py 中定义如下
class Type(type):
def __repr__(cls):
# Will make it so that repr(O) is "O" instead of "<__main__.O>",
# and the same for any subclasses of O.
return cls.__name__
class O(object, metaclass=Type): pass
class A(O): pass
class X(A, O): pass
if __name__ == '__main__':
print(X.mro())
执行脚本,可以看到正常的输出结果:
$ python3 c3_ok.py
[X, A, O, <class 'object'>]
分析
根据 C3 线性化算法,计算 X 的 mro:
L(O) := [O]
L(A) := [A] + merge(L(O), [O])
:= [A] + merge([O], [O])
:= [A, O]
L(X) := [X] + merge(L[A], L[O], [A, O])
:= [X] + merge([A, O], [O], [A, O])
:= [X, A] + merge([O], [O], [O])
:= [X, A, O]
solidity 中的多重继承
需要注意的是,在 solidity 中定义合约时,如果是多重继承,parent contracts 的顺序跟 python 中的 parent classes 的顺序是相反的,即 farthest parent contract 应该书写得离当前定义的合约最近,如下:
contract O {}
contract A is O {}
contract X is O, A {}
多重继承合约的 storage layout
在 remix 中编写如下合约
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.8.0 <0.9.0;
import "hardhat/console.sol";
struct UintStruct {
uint value;
}
contract O {
uint v0 = 0;
}
contract A is O {
uint v1= 1;
}
contract B is O {
uint v2= 2;
}
contract C is O {
uint v3= 3;
}
contract D is O {
uint v4= 4;
}
contract X1 is O, A, B, C, D {
uint vx1 = 6;
function showStorageInX1() public view {
UintStruct storage us;
uint i;
for (; i < 6; ++i) {
assembly {
us.slot := i
}
console.log(i, us.value);
}
}
}
contract X2 is A, B, C, D {
uint vx2 = 7;
function showStorageInX2() public view {
UintStruct storage us;
uint i;
for (; i < 6; ++i) {
assembly {
us.slot := i
}
console.log(i, us.value);
}
}
}
可以看到, O
是 A
、B
、C
、D
合约的共同 parent contract,而 X2
比 X1
少继承了一个 O
。
部署两个合约,分别调用两个合约中的函数 showStorageInX1()
和 showStorageInX2()
,结果如下:
可以看到,X1 和 X2 的 storage layout 是一样的,所以,在多重继承定义中,其实可以省略公共 parent contract。