合约¶
Solidity 合约类似于面向对象语言中的类。合约中有用于数据持久化的状态变量,和可以修改状态变量的函数。 调用另一个合约实例的函数时,会执行一个 EVM 函数调用,这个操作会切换执行时的上下文,这样,前一个合约的状态变量就不能访问了。
创建合约¶
可以通过以太坊交易“从外部”或从 Solidity 合约内部创建合约。
一些集成开发环境,例如 Remix, 通过使用一些UI用户界面使创建合约的过程更加顺畅。 在以太坊上通过编程创建合约最好使用 JavaScript API web3.js。 现在,我们已经有了一个叫做 web3.eth.Contract 的方法能够更容易的创建合约。
创建合约时, 合约的 构造函数 (一个用关键字 constructor
声明的函数)会执行一次。
构造函数是可选的。只允许有一个构造函数,这意味着不支持重载。
构造函数执行完毕后,合约的最终代码将部署到区块链上。此代码包括所有公共和外部函数以及所有可以通过函数调用访问的函数。 部署的代码没有 包括构造函数代码或构造函数调用的内部函数。
在内部,构造函数参数在合约代码之后通过 ABI 编码 传递,但是如果你使用 web3.js
则不必关心这个问题。
如果一个合约想要创建另一个合约,那么创建者必须知晓被创建合约的源代码(和二进制代码)。 这意味着不可能循环创建依赖项。
pragma solidity >=0.4.22 <0.8.0;
contract OwnedToken {
// TokenCreator 是如下定义的合约类型.
// 不创建新合约的话,也可以引用它。
TokenCreator creator;
address owner;
bytes32 name;
// 这是注册 creator 和设置名称的构造函数。
constructor(bytes32 _name) {
// 状态变量通过其名称访问,而不是通过例如 this.owner 的方式访问。
// 这也适用于函数,特别是在构造函数中,你只能像这样(“内部地”)调用它们,
// 因为合约本身还不存在。
owner = msg.sender;
// 从 `address` 到 `TokenCreator` ,是做显式的类型转换
// 并且假定调用合约的类型是 TokenCreator,没有真正的方法来检查这一点。
creator = TokenCreator(msg.sender);
name = _name;
}
function changeName(bytes32 newName) public {
// 只有 creator (即创建当前合约的合约)能够更改名称 —— 因为合约是隐式转换为地址的,
// 所以这里的比较是可行的。
if (msg.sender == address(creator))
name = newName;
}
function transfer(address newOwner) public {
// 只有当前所有者才能发送 token。
if (msg.sender != owner) return;
// 我们也想询问 creator 是否可以发送。
// 请注意,这里调用了一个下面定义的合约中的函数。
// 如果调用失败(比如,由于 gas 不足),会立即停止执行。
if (creator.isTokenTransferOK(owner, newOwner))
owner = newOwner;
}
}
contract TokenCreator {
function createToken(bytes32 name)
public
returns (OwnedToken tokenAddress)
{
// 创建一个新的 Token 合约并且返回它的地址。
// 从 JavaScript 方面来说,返回类型是简单的 `address` 类型,因为
// 这是在 ABI 中可用的最接近的类型。
return new OwnedToken(name);
}
function changeName(OwnedToken tokenAddress, bytes32 name) public {
// 同样,`tokenAddress` 的外部类型也是 `address` 。
tokenAddress.changeName(name);
}
function isTokenTransferOK(address currentOwner, address newOwner)
public
view
returns (bool ok)
{
// 检查一些任意的情况。
address tokenAddress = msg.sender;
return (keccak256(newOwner) & 0xff) == (bytes20(tokenAddress) & 0xff);
}
}
可见性和 getter 函数¶
由于 Solidity 有两种函数调用(内部调用不会产生实际的 EVM 调用或称为“消息调用”,而外部调用则会产生一个 EVM 调用),
函数和状态变量有四种可见性类型。
函数可以指定为 external
,public
,internal
或者 private
。
对于状态变量,不能设置为 external
,默认是 internal
。
external
外部函数作为合约接口的一部分,意味着我们可以从其他合约和交易中调用。 一个外部函数
f
不能从内部调用(即f
不起作用,但this.f()
可以)。 当收到大量数据的时候,外部函数有时候会更有效率,因为数据不会从calldata复制到内存.public
public 函数是合约接口的一部分,可以在内部或通过消息调用。对于 public 状态变量, 会自动生成一个 getter 函数(见下面)。
internal
这些函数和状态变量只能是内部访问(即从当前合约内部或从它派生的合约访问),不使用
this
调用。private
private 函数和状态变量仅在当前定义它们的合约中使用,并且不能被派生合约使用。
备注
合约中的所有内容对外部观察者都是可见的。设置一些 private
类型只能阻止其他合约访问和修改这些信息,
但是对于区块链外的整个世界它仍然是可见的。
可见性标识符的定义位置,对于状态变量来说是在类型后面,对于函数是在参数列表和返回关键字中间。
pragma solidity >=0.4.16 <0.8.0;
contract C {
function f(uint a) private pure returns (uint b) { return a + 1; }
function setData(uint a) internal { data = a; }
uint public data;
}
在下面的例子中,D
可以调用 c.getData()
来获取状态存储中 data
的值,但不能调用 f
。
合约 E
继承自 C
,因此可以调用 compute
。
pragma solidity >=0.4.16 <0.8.0;
contract C {
uint private data;
function f(uint a) private returns(uint b) { return a + 1; }
function setData(uint a) public { data = a; }
function getData() public returns(uint) { return data; }
function compute(uint a, uint b) internal returns (uint) { return a+b; }
}
// 下面代码编译错误
contract D {
function readData() public {
C c = new C();
uint local = c.f(7); // 错误:成员 `f` 不可见
c.setData(3);
local = c.getData();
local = c.compute(3, 5); // 错误:成员 `compute` 不可见
}
}
contract E is C {
function g() public {
C c = new C();
uint val = compute(3, 5); // 访问内部成员(从继承合约访问父合约成员)
}
}
Getter 函数¶
编译器自动为所有 public 状态变量创建 getter 函数。对于下面给出的合约,编译器会生成一个名为 data
的函数,
该函数没有参数,返回值是一个 uint
类型,即状态变量 data
的值。
状态变量的初始化可以在声明时完成。
pragma solidity >=0.4.16 <0.8.0;
contract C {
uint public data = 42;
}
contract Caller {
C c = new C();
function f() public {
uint local = c.data();
}
}
getter 函数具有外部(external)可见性。如果在内部访问 getter(即没有 this.
),它被认为一个状态变量。
如果使用外部访问(即用 this.
),它被认作为一个函数。
pragma solidity >=0.4.16 <0.8.0;
contract C {
uint public data;
function x() public {
data = 3; // 内部访问
uint val = this.data(); // 外部访问
}
}
如果你有一个数组类型的 public
状态变量,那么你只能通过生成的 getter 函数访问数组的单个元素。
这个机制以避免返回整个数组时的高成本gas。 可以使用如 data(0)
用于指定参数要返回的单个元素。
如果要在一次调用中返回整个数组,则需要写一个函数,例如:
pragma solidity >=0.4.0 <0.8.0;
contract arrayExample {
// public state variable
uint[] public myArray;
// 指定生成的Getter 函数
/*
function myArray(uint i) public view returns (uint) {
return myArray[i];
}
*/
// 返回整个数组
function getArray() public view returns (uint[] memory) {
return myArray;
}
}
现在可以使用 getArray()
获得整个数组,而 myArray(i)
是返回单个元素。
下一个例子稍微复杂一些:
pragma solidity ^0.4.0 <0.8.0;
contract Complex {
struct Data {
uint a;
bytes3 b;
mapping (uint => uint) map;
}
mapping (uint => mapping(bool => Data[])) public data;
}
这将会生成以下形式的函数
function data(uint arg1, bool arg2, uint arg3) public returns (uint a, bytes3 b) {
a = data[arg1][arg2][arg3].a;
b = data[arg1][arg2][arg3].b;
}
请注意,因为没有好的方法来提供映射的键,所以结构中的映射被省略。
函数 修改器¶
使用 修改器 可以轻松改变函数的行为。 例如,它们可以在执行函数之前自动检查某个条件。
修改器 是合约的可继承属性,并可能被派生合约覆盖 , 但前提是它们被标记为 virtual
.。 有关详细信息,请参见 Modifier 重载.
// SPDX-License-Identifier: GPL-3.0
pragma solidity >0.7.0 <0.8.0;
contract owned {
constructor() { owner = msg.sender; }
address owner;
// 这个合约只定义一个修改器,但并未使用: 它将会在派生合约中用到。
// 修改器所修饰的函数体会被插入到特殊符号 _; 的位置。
// 这意味着如果是 owner 调用这个函数,则函数会被执行,否则会抛出异常。
modifier onlyOwner {
require(
msg.sender == owner,
"Only owner can call this function."
);
_;
}
}
contract destructible is owned {
// 这个合约从 `owned` 继承了 `onlyOwner` 修饰符,并将其应用于 `destroy` 函数,
// 只有在合约里保存的 owner 调用 `destroy` 函数,才会生效。
function destroy() public onlyOwner {
selfdestruct(owner);
}
}
contract priced {
// 修改器可以接收参数:
modifier costs(uint price) {
if (msg.value >= price) {
_;
}
}
}
contract Register is priced, destructible {
mapping (address => bool) registeredAddresses;
uint price;
constructor(uint initialPrice) { price = initialPrice; }
// 在这里也使用关键字 `payable` 非常重要,否则函数会自动拒绝所有发送给它的以太币。
function register() public payable costs(price) {
registeredAddresses[msg.sender] = true;
}
function changePrice(uint _price) public onlyOwner {
price = _price;
}
}
contract Mutex {
bool locked;
modifier noReentrancy() {
require(
!locked,
"Reentrant call."
);
locked = true;
_;
locked = false;
}
// 这个函数受互斥量保护,这意味着 `msg.sender.call` 中的重入调用不能再次调用 `f`。
// `return 7` 语句指定返回值为 7,但修改器中的语句 `locked = false` 仍会执行。
function f() public noReentrancy returns (uint) {
(bool success,) = msg.sender.call("");
require(success);
return 7;
}
}
如果同一个函数有多个 修改器,它们之间以空格隔开,修改器 会依次检查执行。
警告
在早期的 Solidity 版本中,有 修改器 的函数,return
语句的行为表现不同。
修改器 或函数体中显式的 return 语句仅仅跳出当前的 修改器 和函数体。 返回变量会被赋值,但整个执行逻辑会从前一个 修改器 中的定义的 “_” 之后继续执行。
修改器 的参数可以是任意表达式,在此上下文中,所有在函数中可见的符号,在 修改器 中均可见。 在 修改器 中引入的符号在函数中不可见(可能被重载改变)。
Constant 和 Immutable 状态变量¶
状态变量声明为 constant
(常量)或者 immutable
(不可变量),在这两种情况下,合约一旦部署之后,变量将不在修改。
对于 constant
常量, 他的值在编译器确定,而对于 immutable
, 它的值在部署时确定。
编译器不会为这些变量预留存储位,它们的每次出现都会被替换为相应的常量表达式(它可能被优化器计算为实际的某个值)。
与常规状态变量相比,常量和不可变量的gas成本要低得多。 对于常量,赋值给它的表达式将复制到所有访问该常量的位置,并且每次都会对其进行重新求值。 这样可以进行本地优化。
不可变变量在构造时进行一次求值,并将其值复制到代码中访问它们的所有位置。 对于这些值,将保留32个字节,即使它们适合较少的字节也是如此。 因此,常量有时可能比不可变量更便宜。
不是所有类型的状态变量都支持用 constant 或 immutable
来修饰,当前仅支持 :ref:`字符串 <strings>`_ (仅常量) 和 :ref:`值类型 <value-types>`_.
// SPDX-License-Identifier: GPL-3.0
pragma solidity >0.6.99 <0.8.0;
contract C {
uint constant X = 32**22 + 8;
string constant TEXT = "abc";
bytes32 constant MY_HASH = keccak256("abc");
uint immutable decimals;
uint immutable maxBalance;
address immutable owner = msg.sender;
constructor(uint _decimals, address _reference) {
decimals = _decimals;
// Assignments to immutables can even access the environment.
maxBalance = _reference.balance;
}
function isBalanceTooHigh(address _other) public view returns (bool) {
return _other.balance > maxBalance;
}
}
Constant¶
如果状态变量声明为 constant
(常量)。在这种情况下,只能使用那些在编译时有确定值的表达式来给它们赋值。
任何通过访问 storage,区块链数据(例如 block.timestamp
, address(this).balance
或者 block.number
)或执行数据( msg.value
或 gasleft()
)
或对外部合约的调用来给它们赋值都是不允许的。
允许可能对内存分配产生副作用(side-effect)的表达式,但那些可能对其他内存对象产生副作用的表达式则不允许。
内建(built-in)函数 keccak256
, sha256
, ripemd160
, ecrecover
, addmod
和 mulmod
是允许的(即使他们确实会调用外部合约, keccak256
除外)。
允许内存分配器的副作用的原因是它可以构造复杂的对象,例如: 查找表(lookup-table)。 此功能尚不完全可用。
Immutable¶
声明为不可变量(immutable
)的变量的限制要比声明为常量(constant
) 的变量的限制少:可以在合约的构造函数中或声明时为不可变的变量分配任意值。 不可变量在构造期间无法读取其值,并且只能赋值一次。
编译器生成的合约创建代码将在返回合约之前修改合约的运行时代码,方法是将对不可变量的所有引用替换为分配给它们的值。 如果要将编译器生成的运行时代码与实际存储在区块链中的代码进行比较,则这一点很重要。
备注
译者注:不可变量(Immutable) 是 Solidity 0.6.5 引入的,因此0.6.5 之前的版本不可用。
函数¶
可以在合约内部和外部定义功能。
备注
译者注:函数可以在合约外部定义是从 0.7.0 之后才开始支持的。
合约之外的函数(也称为“自由函数”)始终具有隐式的 internal
可见性。 它们的代码包含在所有调用它们合约中,类似于内部库函数。
// SPDX-License-Identifier: GPL-3.0
pragma solidity >0.8.0 <0.8.0;
function sum(uint[] memory _arr) pure returns (uint s) {
for (uint i = 0; i < _arr.length; i++)
s += _arr[i];
}
contract ArrayExample {
bool found;
function f(uint[] memory _arr) public {
// This calls the free function internally.
// The compiler will add its code to the contract.
uint s = sum(_arr);
require(s >= 10);
found = true;
}
}
函数参数及返回值¶
与 Javascript 一样,函数可能需要参数作为输入; 而与 Javascript 和 C 不同的是,它们可能返回任意数量的参数作为输出。
函数参数(输入参数)¶
函数参数的声明方式与变量相同。不过未使用的参数可以省略参数名。
例如,如果我们希望合约接受有两个整数形参的函数的外部调用,可以像下面这样写:
pragma solidity >=0.4.16 <0.8.0;
contract Simple {
uint sum;
function taker(uint _a, uint _b) public {
sum = _a + _b;
}
}
函数参数可以当作为本地变量,也可用在等号左边被赋值。
返回变量¶
函数返回变量的声明方式在关键词 returns
之后,与参数的声明方式相同。
例如,如果我们需要返回两个结果:两个给定整数的和与积,我们应该写作
pragma solidity >=0.4.16 <0.8.0;
contract Simple {
function arithmetic(uint _a, uint _b)
public
pure
returns (uint o_sum, uint o_product)
{
o_sum = _a + _b;
o_product = _a * _b;
}
}
返回变量名可以被省略。
返回变量可以当作为函数中的本地变量,没有显式设置的话,会使用 :ref:` 默认值 <default-value>`
返回变量可以显式给它附一个值(像上面),也可以使用 return
语句指定,使用 return
语句可以一个或多个值,参阅 multiple ones 。
pragma solidity >=0.4.16 <0.8.0;
contract Simple {
function arithmetic(uint _a, uint _b)
public
pure
returns (uint o_sum, uint o_product)
{
return (_a + _b, _a * _b);
}
}
这个形式等同于赋值给返回参数,然后用 return;
退出。
如果使用 ``return `` 提前退出有返回值的函数, 必须在用 return 时提供返回值。
备注
非内部函数有些类型没法返回,比如限制的类型有:多维动态数组、结构体等。
如果添加 pragma experimental ABIEncoderV2;
启用实验功能 ABIEncoderV2
则是可以的返回更多类型,不过 mapping
仍然是受限的。
返回多个值¶
当函数需要使用多个值,可以用语句 return (v0, v1, ..., vn)
。
参数的数量需要和声明时候一致。
View 视图函数¶
可以将函数声明为 view
类型,这种情况下要保证不修改状态。
备注
如果编译器的 EVM 目标是拜占庭硬分叉( 译者注:Byzantium 分叉发生在2017年10月,这次分叉进加入了4个操作符: REVERT 、RETURNDATASIZE、RETURNDATACOPY 、STATICCALL) 或更新的 (默认), 则操作码 STATICCALL
将用于视图函数, 这些函数强制在 EVM 执行过程中保持不修改状态。
对于库视图函数, 使用 DELLEGATECALL
, 因为没有组合的 DELEGATECALL
和 STATICALL
。这意味着库视图函数不会在运行时检查进而阻止状态修改。
这不会对安全性产生负面影响, 因为库代码通常在编译时知道, 并且静态检查器会执行编译时检查。
下面的语句被认为是修改状态:
修改状态变量。
产生事件。
使用
selfdestruct
。通过调用发送以太币。
调用任何没有标记为
view
或者pure
的函数。使用低级调用。
使用包含特定操作码的内联汇编。
pragma solidity >=0.5.0 <0.8.0;
contract C {
function f(uint a, uint b) public view returns (uint) {
return a * (b + 42) + block.timestamp;
}
}
备注
constant
之前是 view
的别名,不过在0.5.0之后移除了。
备注
Getter 方法自动被标记为 view
。
备注
在0.5.0 版本之前, 编译器没有对 view
函数使用 STATICCALL
操作码。
这样通过使用无效的显式类型转换会启用视图函数中的状态修改。
通过对 view
函数使用 STATICCALL
, 可以防止在 EVM 级别上对状态进行修改。
Pure 纯函数¶
函数可以声明为 pure
,在这种情况下,承诺不读取也不修改状态。
备注
如果编译器的 EVM 目标是 Byzantium 或更新的 (默认), 则使用操作码 STATICCALL
, 这并不保证状态未被读取, 但至少不被修改。
除了上面解释的状态修改语句列表之外,以下被认为是读取状态:
读取状态变量。
访问
address(this).balance
或者<address>.balance
。访问
block
,tx
,msg
中任意成员 (除msg.sig
和msg.data
之外)。调用任何未标记为
pure
的函数。使用包含某些操作码的内联汇编。
pragma solidity >=0.5.0 <0.8.0;
contract C {
function f(uint a, uint b) public pure returns (uint) {
return a * (b + 42);
}
}
纯函数能够使用 revert()
和 require()
在 发生错误 时去还原潜在状态更改。
还原状态更改不被视为 “状态修改”, 因为它只还原以前在没有``view`` 或 pure
限制的代码中所做的状态更改, 并且代码可以选择捕获 revert
并不传递还原。
这种行为也符合 STATICCALL
操作码。
警告
不可能在 EVM 级别阻止函数读取状态, 只能阻止它们写入状态 (即只能在 EVM 级别强制执行 view
, 而 pure
不能强制)。
备注
在0.5.0 版本之前, 编译器没有对 pure
函数使用 STATICCALL
操作码。这样通过使用无效的显式类型转换启用 pure
函数中的状态修改。
通过对 pure
函数使用 STATICCALL
, 可以防止在 EVM 级别上对状态进行修改。
备注
在0.4.17版本之前,编译器不会强制 pure
函数不读取状态。它是一个编译时类型检查, 可以避免在合约类型之间进行无效的显式转换, 因为编译器可以验证合约类型没有状态更改操作, 但它不会在运行时能检查调用实际的类型。
receive 接收以太函数¶
一个合约最多有一个 receive
函数, 声明函数为:
receive() external payable { ... }
不需要 function
关键字,也没有参数和返回值并且必须是 external
可见性和 payable
修饰.
在对合约没有任何附加数据调用(通常是对合约转账)是会执行 receive
函数. 例如 通过 .send()
or .transfer()
如果 receive
函数不存在, 但是有payable 的 fallback 回退函数
那么在进行纯以太转账时,fallback 函数会调用.
如果两个函数都没有,这个合约就没法通过常规的转账交易接收以太(会抛出异常).
更糟的是,fallback函数可能只有 2300 gas 可以使用(如,当使用 send
或 transfer
时), 除了基础的日志输出之外,进行其他操作的余地很小。下面的操作消耗会操作 2300 gas :
写入存储
创建合约
调用消耗大量 gas 的外部函数
发送以太币
警告
一个没有定义 fallback 函数或 receive 函数的合约,直接接收以太币(没有函数调用,即使用 send
或 transfer
)会抛出一个异常,
并返还以太币(在 Solidity v0.4.0 之前行为会有所不同)。
所以如果你想让你的合约接收以太币,必须实现receive函数(使用 payable fallback 函数不再推荐,因为它会让借口混淆)。
警告
一个没有receive函数的合约,可以作为 coinbase 交易 (又名 矿工区块回报 )的接收者或者作为 selfdestruct
的目标来接收以太币。
一个合约不能对这种以太币转移做出反应,因此也不能拒绝它们。这是 EVM 在设计时就决定好的,而且 Solidity 无法绕过这个问题。
这也意味着 address(this).balance
可以高于合约中实现的一些手工记帐的总和(例如在receive 函数中更新的累加器记帐)。
下面是一个例子:
pragma solidity ^0.6.0;
// 这个合约会保留所有发送给它的以太币,没有办法取回。
contract Sink {
event Received(address, uint);
receive() external payable {
emit Received(msg.sender, msg.value);
}
}
Fallback 回退函数¶
合约可以最多有一个回退函数。函数声明为:
fallback () external [payable]
这个函数不能有参数也不能有返回值,也没有 function
关键字. 必须是 external
可见性
如果在一个对合约调用中,没有其他函数与给定的函数标识符匹配fallback会被调用. 或者在没有 receive 函数 时,而没有提供附加数据对合约调用,那么fallback 函数会被执行。
fallback 函数始终会接收数据,但为了同时接收以太时,必须标记为 payable
。
更糟的是,如果回退函数在接收以太时调用,可能只有 2300 gas 可以使用,参考 receive接收函数
与任何其他函数一样,只要有足够的 gas 传递给它,回退函数就可以执行复杂的操作。
警告
payable
的fallback函数也可以在纯以太转账的时候执行, 如果没有 receive 以太函数
推荐总是定义一个receive函数,而不是定义一个``payable`` 的fallback函数,
备注
即使 fallback 函数不能有参数,仍然可以使用 msg.data
来获取随调用提供的任何有效数据。
在检查了 msg.data
的前四个字节之后,
- 您可以用
abi.decode
与数组切片语法一起使用来解码ABI编码的数据: (c, d) = abi.decode(msg.data[4:], (uint256, uint256));
请注意,这仅应作为最后的手段,而应使用对应的函数。
pragma solidity >=0.6.2 <0.8.0;
contract Test {
// 发送到这个合约的所有消息都会调用此函数(因为该合约没有其它函数)。
// 向这个合约发送以太币会导致异常,因为 fallback 函数没有 `payable` 修饰符
fallback() external { x = 1; }
uint x;
}
// 这个合约会保留所有发送给它的以太币,没有办法返还。
contract TestPayable {
// 除了纯转账外,所有的调用都会调用这个函数.
// (因为除了 receive 函数外,没有其他的函数).
// 任何对合约非空calldata 调用会执行回退函数(即使是调用函数附加以太).
fallback() external payable { x = 1; y = msg.value; }
// 纯转账调用这个函数,例如对每个空empty calldata的调用
receive() external payable { x = 2; y = msg.value; }
uint x;
uint y;
}
contract Caller {
function callTest(Test test) public returns (bool) {
(bool success,) = address(test).call(abi.encodeWithSignature("nonExistingFunction()"));
require(success);
// test.x 结果变成 == 1。
// address(test) 不允许直接调用 ``send`` , 因为 ``test`` 没有 payable 回退函数
// 转化为 ``address payable`` 类型 , 然后才可以调用 ``send``
address payable testPayable = payable(address(test));
// 以下将不会编译,但如果有人向该合约发送以太币,交易将失败并拒绝以太币。
// test.send(2 ether);
}
function callTestPayable(TestPayable test) public returns (bool) {
(bool success,) = address(test).call(abi.encodeWithSignature("nonExistingFunction()"));
require(success);
// 结果 test.x 为 1 test.y 为 0.
(success,) = address(test).call{value: 1}(abi.encodeWithSignature("nonExistingFunction()"));
require(success);
// 结果test.x 为1 and test.y 为 1.
// 发送以太币, TestPayable 的 receive 函数被调用.
require(address(test).send(2 ether));
// 结果 in test.x 为 2 and test.y 为 2 ether.
}
}
函数重载¶
合约可以具有多个不同参数的同名函数,称为“重载”(overloading),这也适用于继承函数。以下示例展示了合约 A
中的重载函数 f
。
pragma solidity >=0.4.16 <0.8.0;
contract A {
function f(uint _in) public pure returns (uint out) {
out = _in;
}
function f(uint _in, bool _really) public pure returns (uint out) {
if (_really)
out = _in;
}
}
重载函数也存在于外部接口中。如果两个外部可见函数仅区别于 Solidity 内的类型而不是它们的外部类型则会导致错误。
// 以下代码无法编译
pragma solidity >=0.4.16 <0.8.0;
contract A {
function f(B _in) public pure returns (B out) {
out = _in;
}
function f(address _in) public pure returns (address out) {
out = _in;
}
}
contract B {
}
以上两个 f
函数重载都接受了 ABI 的地址类型,虽然它们在 Solidity 中被认为是不同的。
重载解析和参数匹配¶
通过将当前范围内的函数声明与函数调用中提供的参数相匹配,可以选择重载函数。 如果所有参数都可以隐式地转换为预期类型,则选择函数作为重载候选项。如果一个候选都没有,解析失败。
备注
返回参数不作为重载解析的依据。
pragma solidity >=0.4.16 <0.8.0;
contract A {
function f(uint8 _in) public pure returns (uint8 out) {
out = _in;
}
function f(uint256 _in) public pure returns (uint256 out) {
out = _in;
}
}
调用 f(50)
会导致类型错误,因为 50
既可以被隐式转换为 uint8
也可以被隐式转换为 uint256
。
另一方面,调用 f(256)
则会解析为 f(uint256)
重载,因为 256
不能隐式转换为 uint8
。
事件 Events¶
Solidity 事件是EVM的日志功能之上的抽象。 应用程序可以通过以太坊客户端的RPC接口订阅和监听这些事件。
事件在合约中可被继承。当他们被调用时,会使参数被存储到交易的日志中 —— 一种区块链中的特殊数据结构。 这些日志与地址相关联,被并入区块链中,只要区块可以访问就一直存在(现在开始会被永久保存,在 Serenity 版本中可能会改动)。 日志和事件在合约内不可直接被访问(甚至是创建日志的合约也不能访问)。
如果外部实体需要该日志实际上存在于区块链中的证明,可以请求日志的Merkle证明. 但需要留意的是,由于合约中仅能访问最近的 256 个区块哈希,所以还需要提供区块头信息。
对日志的证明是可能的,如果一个外部实体提供了一个带有这种证明的合约,它可以检查日志是否真实存在于区块链中。
最多三个参数可以接收 indexed
属性(它是一个特殊的名为:ref:”主题” <abi_events> 的数据结构, 而不是日志的数据部分)。
如果数组(包括 string
和 bytes
)类型被标记为索引项,则它们的 keccak-256 哈希值会被作为 主题 保存,这是因为主题仅仅可以保存单个字(32个字节)。
所有非索引 indexed
参数是 ABI-encoded 都将存储在日志的数据部分中。
主题 让我们可以可以搜索事件,比如在为某些事件过滤一些区块,还可以按发起事件的合同地址来过滤事件。
例如, 使用如下的 web3.js subscribe("logs")
方法 去过滤符合特定地址的|topic| :
var options = {
fromBlock: 0,
address: web3.eth.defaultAccount,
topics: ["0x0000000000000000000000000000000000000000000000000000000000000000", null, null]
};
web3.eth.subscribe('logs', options, function (error, result) {
if (!error)
console.log(result);
})
.on("data", function (log) {
console.log(log);
})
.on("changed", function (log) {
});
除非你用 anonymous
说明符声明事件,否则事件签名的哈希值是一个 主题 。
同时也意味着对于匿名事件无法通过名字来过滤。您可以仅按合约地址过滤。 匿名事件的优势是他们部署和调用的成本更低。
pragma solidity >=0.4.21 <0.8.0;
contract ClientReceipt {
event Deposit(
address indexed _from,
bytes32 indexed _id,
uint _value
);
function deposit(bytes32 _id) public payable {
// 事件使用 emit 触发事件。
// 我们可以过滤对 `Deposit` 的调用,从而用 Javascript API 来查明对这个函数的任何调用(甚至是深度嵌套调用)。
emit Deposit(msg.sender, _id, msg.value);
}
}
使用 JavaScript API 调用事件的用法如下:
var abi = /* abi 由编译器产生 */;
var ClientReceipt = web3.eth.contract(abi);
var clientReceipt = ClientReceipt.at("0x1234...xlb67" /* 地址 */);
var event = clientReceipt.Deposit();
// 监听变化
event.watch(function(error, result) {
// 结果包含 非索引参数 以及 主题 topic
if (!error)
console.log(result);
});
// 或者通过传入回调函数,立即开始听监
var event = clientReceipt.Deposit(function(error, result) {
if (!error)
console.log(result);
});
上面的输出如下所示(有删减):
{
"returnValues": {
"_from": "0x1111…FFFFCCCC",
"_id": "0x50…sd5adb20",
"_value": "0x420042"
},
"raw": {
"data": "0x7f…91385",
"topics": ["0xfd4…b4ead7", "0x7f…1a91385"]
}
}
日志的底层接口¶
通过函数 log0
,log1
, log2
, log3
和 log4
也可以访问日志机制的底层接口。
每个函数 logi
接受 i + 1
个 bytes32
类型的参数。其中第一个参数会被用来做为日志的数据部分,
其它的会做为 topic。上面的事件调用可以以如下相同的方式执行:
pragma solidity >=0.4.10 <0.8.0;
contract C {
function f() public payable {
bytes32 _id = 0x420042;
log3(
bytes32(msg.value),
bytes32(0x50cb9fe53daa9737b786ab3646f04d0150dc50ef4e75f59509d83667ad5adb20),
bytes32(msg.sender),
_id
);
}
}
其中长串的十六进制数的计算方法是 keccak256("Deposit(address,hash256,uint256)")
,即事件的签名。
其它学习事件机制的资源¶
继承¶
Solidity 支持多重继承包括多态。
所有的函数调用都是虚拟的,这意味着最终派生的函数会被调用,除非明确给出合约名称或者使用super关键字。
当一个合约从多个合约继承时,在区块链上只有一个合约被创建,所有基类合约(或称为父合约)的代码被编译到创建的合约中。这意味着对基类合约函数的所有内部调用也只是使用内部函数调用(super.f(..)将使用JUMP跳转而不是消息调用)。
状态变量覆盖被视为错误。 派生合约不可以在声明已经是基类合约中可见的状态变量具有相同的名称 x
总的来说,Solidity 的继承系统与 Python的继承系统 非常 相似,特别是多重继承方面, 但是也有一些 不同 。
下面的例子进行了详细的说明。
// SPDX-License-Identifier: GPL-3.0
pragma solidity >0.6.99 <0.8.0;
contract Owned {
constructor() public { owner = msg.sender; }
address payable owner;
}
// 使用 is 从另一个合约派生。派生合约可以访问所有非私有成员,包括内部(internal)函数和状态变量,
// 但无法通过 this 来外部访问。
contract Destructible is Owned {
// 关键字`virtual`表示该函数可以在派生类中“overriding”。
function destroy() virtual public {
if (msg.sender == owner) selfdestruct(owner);
}
}
// 这些抽象合约仅用于给编译器提供接口。
// 注意函数没有函数体。
// 如果一个合约没有实现所有函数,则只能用作接口。
abstract contract Config {
function lookup(uint id) public virtual returns (address adr);
}
abstract contract NameReg {
function register(bytes32 name) public virtual;
function unregister() public virtual;
}
// 可以多重继承。请注意,owned 也是 Destructible 的基类,
// 但只有一个 owned 实例(就像 C++ 中的虚拟继承)。
contract Named is Owned, Destructible {
constructor(bytes32 name) {
Config config = Config(0xD5f9D8D94886E70b06E474c3fB14Fd43E2f23970);
NameReg(config.lookup(1)).register(name);
}
// 函数可以被另一个具有相同名称和相同数量/类型输入的函数重载。
// 如果重载函数有不同类型的输出参数,会导致错误。
// 本地和基于消息的函数调用都会考虑这些重载。
//如果要覆盖函数,则需要使用 `override` 关键字。 如果您想再次覆盖此函数,则需要再次指定`virtual`关键字。
function destroy() public virtual override {
if (msg.sender == owner) {
Config config = Config(0xD5f9D8D94886E70b06E474c3fB14Fd43E2f23970);
NameReg(config.lookup(1)).unregister();
// 仍然可以调用特定的重载函数。
Destructible.destroy();
}
}
}
// 如果构造函数接受参数,
// 则需要在声明(合约的构造函数)时提供,
// 或在派生合约的构造函数位置以修改器调用风格提供(见下文)。
contract PriceFeed is Owned, Destructible, Named("GoldFeed") {
function updateInfo(uint newInfo) public {
if (msg.sender == owner) info = newInfo;
}
// Here, we only specify `override` and not `virtual`.
// This means that contracts deriving from `PriceFeed`
// cannot change the behaviour of `destroy` anymore.
function destroy() public override(Destructible, Named) { Named.destroy(); }
function get() public view returns(uint r) { return info; }
uint info;
}
注意,在上边的代码中,我们调用 Destructible.destroy()
来“转发”销毁请求。
这样做法是有问题的,在下面的例子中可以看到:
// SPDX-License-Identifier: GPL-3.0
pragma solidity >0.6.99 <0.8.0;
contract owned {
constructor() { owner = msg.sender; }
address owner;
}
contract Destructible is owned {
function destroy() public virtual {
if (msg.sender == owner) selfdestruct(owner);
}
}
contract Base1 is Destructible {
function destroy() public virtual override { /* 清除操作 1 */ Destructible.destroy(); }
}
contract Base2 is Destructible {
function destroy() public { /* 清除操作 2 */ Destructible.destroy(); }
}
contract Final is Base1, Base2 {
function destroy() public override(Base1, Base2) { Base2.destroy(); }
}
。 解决此问题的方法是使用超级:
调用 Final.destroy()
时会调用 Base2.destroy
, 因为我们在最终重写中显式指定了它。
但是此函数将绕过 Base1.destroy
, 解决这个问题的方法是使用 super
:
// SPDX-License-Identifier: GPL-3.0
pragma solidity >0.6.99 <0.8.0;
contract owned {
constructor() { owner = msg.sender; }
address owner;
}
contract Destructible is owned {
function destroy() virtual public {
if (msg.sender == owner) selfdestruct(owner);
}
}
contract Base1 is Destructible {
function destroy() public virtual override { /* 清除操作 1 */ super.destroy(); }
}
contract Base2 is Destructible {
function destroy() public virtual override { /* 清除操作 2 */ super.destroy(); }
}
contract Final is Base1, Base2 {
function destroy() public override(Base1, Base2) { super.destroy(); }
}
如果 Base2
调用 super
的函数,它不会简单在其基类合约上调用该函数。
相反,它在最终的继承关系图谱的下一个基类合约中调用这个函数,所以它会调用 Base1.destroy()
(注意最终的继承序列是——从最终派生合约开始:Final, Base2, Base1, Destructible, ownerd)。
在类中使用 super 调用的实际函数在当前类的上下文中是未知的,尽管它的类型是已知的。
这与普通的虚拟方法查找类似。
函数重写(Overriding)¶
父合约标记为 virtual
函数可以在继承合约里重写(overridden)以更改他们的行为。重写的函数需要使用关键字 override
修饰。
重写函数只能将覆盖函数的可见性从 external
更改为 public
。
可变性可以按照以下顺序更改为更严格的一种:
nonpayable
可以被 view
和 pure
覆盖。 view
可以被 pure
覆盖。
payable
是一个例外,不能更改为任何其他可变性。
以下示例演示了可变性和可见性的变化:
// SPDX-License-Identifier: GPL-3.0
pragma solidity >0.6.99 <0.8.0;
contract Base
{
function foo() virtual external view {}
}
contract Middle is Base {}
contract Inherited is Middle
{
function foo() override public pure {}
}
对于多重继承,如果有多个父合约有相同定义的函数, override
关键字后必须指定所有父合约名。
例如:
pragma solidity >=0.6.0 <0.8.0;
contract Base1
{
function foo() virtual public {}
}
contract Base2
{
function foo() virtual public {}
}
contract Inherited is Base1, Base2
{
// 继承自两个基类合约定义的foo(), 必须显示的指定 override
function foo() public override(Base1, Base2) {}
}
不过如果(重写的)函数继承自一个公共的父合约, override
是可以不用显示指定的。
例如:
pragma solidity >=0.6.0 <0.8.0;
contract A { function f() public pure{} }
contract B is A {}
contract C is A {}
// 不用显示 override
contract D is B, C {}
更正式地说,如果存在父合约是签名函数的所有重写路径的一部分,则不需要重写(直接或间接)从多个基础继承的函数,并且(1)父合约实现了该函数,从当前合约到父合约的路径都没有提到具有该签名的函数,或者(2)父合约没有实现该函数,并且存在从当前合约到该父合约的所有路径中,最多只能提及该函数。
从这个意义上说,签名函数的重写路径是通过继承图的路径,该路径始于所考虑的合约,并终止于提及具有该签名的函数的合约。
如果函数没有标记为 virtual
, 那么派生合约将不能更改函数的行为(即不能重写)。
备注
private
的函数是不可以标记为 virtual
的。
备注
除接口之外(因为接口会自动作为 virtual
),没有实现的函数必须标记为 virtual
如果getter 函数的参数和返回值都和外部函数一致时,外部(external)函数是可以被 public 的状态变量被重写的,例如:
pragma solidity >=0.6.0 <0.8.0;
contract A
{
function f() external view virtual returns(uint) { return 5; }
}
contract B is A
{
uint public override f;
}
备注
尽管public 的状态变量可以重写外部函数,但是public 的状态变量不能被重写。
修改器重写¶
修改器重写也可以被重写,工作方式和 :ref:`函数重写 <function-overriding>`_ 类似。
需要被重写的修改器也需要使用 virtual
修饰,override
则同样修饰重载,例如:
pragma solidity >=0.6.0 <0.8.0;
contract Base
{
modifier foo() virtual {_;}
}
contract Inherited is Base
{
modifier foo() override {_;}
}
如果是多重继承,所有直接父合约必须显示指定override, 例如:
pragma solidity >=0.6.0 <0.8.0;
contract Base1
{
modifier foo() virtual {_;}
}
contract Base2
{
modifier foo() virtual {_;}
}
contract Inherited is Base1, Base2
{
modifier foo() override(Base1, Base2) {_;}
}
构造函数¶
构造函数是使用 constructor
关键字声明的一个可选函数, 它在创建合约时执行, 可以在其中运行合约初始化代码。
在执行构造函数代码之前, 如果状态变量可以初始化为指定值; 如果不初始化, 则为零。
构造函数运行后, 将合约的最终代码部署到区块链。代码的部署需要 gas 与代码的长度线性相关。 此代码包括所有函数部分是公有接口以及可以通过函数调用访问的所有函数。它不包括构造函数代码或仅从构造函数调用的内部函数。
如果没有构造函数, 合约将假定采用默认构造函数, 它等效于 constructor() {}
。
举例:
// SPDX-License-Identifier: GPL-3.0
pragma solidity >0.6.99 <0.8.0;
abstract contract A {
uint public a;
constructor(uint _a) {
a = _a;
}
}
contract B is A(1) {
constructor() {}
}
构造函数可以使用内部(internal)参数(例如指向存储的指针),在这个例子中,合约必须标记为 抽象合约 ,因为参数不能赋值被外部赋值,而仅能通过派生的合约。
警告
在 0.4.22 版本之前, 构造函数定义为合约的同名函数,不过语法在0.5.0之后弃用了。
警告
在 0.7.0 版本之前, 你需要通过 internal
或 public
指定构造函数的可见性。
基类构造函数的参数¶
所有基类合约的构造函数将在下面解释的线性化规则被调用。如果基构造函数有参数, 派生合约需要指定所有参数。这可以通过两种方式来实现
// SPDX-License-Identifier: GPL-3.0
pragma solidity >0.6.99 <0.8.0;
contract Base {
uint x;
constructor(uint _x) { x = _x; }
}
// 直接在继承列表中指定参数
contract Derived1 is Base(7) {
constructor() {}
}
// 或通过派生的构造函数中用 修饰符 "modifier"
contract Derived2 is Base {
constructor(uint _y) Base(_y * _y) {}
}
一种方法直接在继承列表中调用基类构造函数(is Base(7)
)。
另一种方法是像 修改器 使用方法一样,
作为派生合约构造函数定义头的一部分,(Base(_y * _y)
)。
如果构造函数参数是常量并且定义或描述了合约的行为,使用第一种方法比较方便。
如果基类构造函数的参数依赖于派生合约,那么必须使用第二种方法。
参数必须在两种方式中(继承列表或派生构造函数修饰符样式)使用一种 。 在这两个位置都指定参数则会发生错误。
如果派生合约没有给所有基类合约指定参数,则这个合约将是抽象合约。
多重继承与线性化¶
编程语言实现多重继承需要解决几个问题。
一个问题是 钻石问题。
Solidity 借鉴了 Python 的方式并且使用“ C3 线性化 ”强制一个由基类构成的 DAG(有向无环图)保持一个特定的顺序。
这最终反映为我们所希望的唯一化的结果,但也使某些继承方式变为无效。尤其是,基类在 is
后面的顺序很重要:列出基类合约的
顺序从 “最基类” 到 “最派生类” 。请注意, 此顺序与 Python 中使用的顺序相反。
另一种简化的解释是, 当一个在不同的合约中多次定义函数被调用时, , 给定的基类以从右到左 (Python 中从左到右) 按深度优先的方式进行搜索,在第一次匹配的时候停止。 如果基类合约已经搜索过, 则跳过该合约。
在下面的代码中,Solidity 会给出“ Linearization of inheritance graph impossible ”这样的错误。
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.0 <0.8.0;
contract X {}
contract A is X {}
// 编译出错
contract C is A, X {}
代码编译出错的原因是 C
要求 X
重写 A
(因为定义的顺序是 A, X
),
但是 A
本身要求重写 X
,无法解决这种冲突。
译者注:可以通过一个简单的规则来记忆: 以从“最接近的基类”(most base-like)到“最远的继承”(most derived)的顺序来指定所有的基类。
由于必须显式覆盖从多个基类继承的函数,因此C3线性化在实践中并不是太重要。
当继承层次结构中有多个构造函数时,继承线性化特别重要。 构造函数将始终以线性化顺序执行,无论在继承合约的构造函数中提供其参数的顺序如何。 例如:
// SPDX-License-Identifier: GPL-3.0
pragma solidity >0.6.99 <0.8.0;
contract Base1 {
constructor() {}
}
contract Base2 {
constructor() {}
}
// 构造函数以以下顺序执行:
// 1 - Base1
// 2 - Base2
// 3 - Derived1
contract Derived1 is Base1, Base2 {
constructor() Base1() Base2() {}
}
// 构造函数以以下顺序执行:
// 1 - Base2
// 2 - Base1
// 3 - Derived2
contract Derived2 is Base2, Base1 {
constructor() Base2() Base1() {}
}
// 构造函数仍然以以下顺序执行:
// 1 - Base2
// 2 - Base1
// 3 - Derived3
contract Derived3 is Base2, Base1 {
constructor() Base1() Base2() {}
}
继承有相同名字的不同类型成员¶
当继承时合约出现了一下相同名字会被认为是一个错误:
函数和 修改器 同名
函数和事件同名
事件和 修改器 同名
有一种例外情况,状态变量的 getter 函数可以覆盖 external 函数。
抽象合约¶
如果未实现合约中的至少一个函数,则需要将合约标记为 abstract。 即使实现了所有功能,合同也可能被标记为abstract。
如下例所示,使用关键字 abstract
定义抽象合约合约, utterance()
函数了,但没有实现.(由 ;
结尾):
pragma solidity >=0.6.0 <0.8.0;
abstract contract Feline {
function utterance() public returns (bytes32);
}
这样的抽象合约不能直接实例化。 如果抽象合约本身确实都有实现所有定义的函数,也是正确的。 下例显示了抽象合约作为基类的用法:
pragma solidity >=0.6.0 <0.8.0;
abstract contract Feline {
function utterance() public pure returns (bytes32);
}
contract Cat is Feline {
function utterance() public pure returns (bytes32) { return "miaow"; }
}
如果合约继承自抽象合约,并且没有通过重写来实现所有未实现的函数, 它依然需要标记为抽象 abstract 合约.
请注意,没有实现的函数与 Function Type 不同,即使它们的语法看起来非常相似。
没有实现的函数示例(函数声明):
function foo(address) external returns (address);
函数类型的示例(变量声明,其中变量的类型为“函数”):
function(address) external returns (address) foo;
抽象合约将合约的定义与其实现脱钩,从而提供了更好的可扩展性和自文档性,并简化了诸如 Template方法 的模式并消除了代码重复。抽象合约的使用方式与接口 interface 中定义方法的使用方式相同。 抽象合约的设计者可以这样说“我的任何继承都必须实施此方法”。
备注
抽象合约不能用一个无实现的函数重写一个实现了的虚函数。
接口¶
接口类似于抽象合约,但是它们不能实现任何函数。还有进一步的限制:
无法继承其他合约,不过可以继承其他接口。
所有的函数都需要是 external
无法定义构造函数。
无法定义状态变量。
将来可能会解除这里的某些限制。
接口基本上基本上仅限于合约 ABI 可以表示的内容,并且 ABI 和接口之间的转换应该不会丢失任何信息。
接口由它们自己的关键字表示:
pragma solidity >=0.6.2 <0.8.0;
interface Token {
enum TokenType { Fungible, NonFungible }
struct Coin { string obverse; string reverse; }
function transfer(address recipient, uint amount) external;
}
就像继承其他合约一样,合约可以继承接口。接口中的函数都会隐式的标记为 virtual
,意味着他们会被重写。
但是不表示重写(overriding)函数可以再次重写,仅仅当重写的函数标记为 virtual
才可以再次重写。
接口可以继承其他的接口,遵循同样继承规则。
pragma solidity >=0.6.2 <0.8.0;
interface ParentA {
function test() external returns (uint256);
}
interface ParentB {
function test() external returns (uint256);
}
interface SubInterface is ParentA, ParentB {
// 必须重新定义 test 函数,以表示兼容父合约含义
function test() external override(ParentA, ParentB) returns (uint256);
}
定义在接口或其他类合约( contract-like)结构体里的类型,可以在其他的合约里用这样的方式访问: Token.TokenType
或 Token.Coin
.
库¶
库与合约类似,它们只需要在特定的地址部署一次,并且它们的代码可以通过 EVM 的 DELEGATECALL
(Homestead 之前使用 CALLCODE
关键字)特性进行重用。
这意味着如果库函数被调用,它的代码在调用合约的上下文中执行,即 this
指向调用合约,特别是可以访问调用合约的存储。
因为每个库都是一段独立的代码,所以它仅能访问调用合约明确提供的状态变量(否则它就无法通过名字访问这些变量)。
因为我们假定库是无状态的,所以如果它们不修改状态(也就是说,如果它们是 view
或者 pure
函数),
库函数仅可以通过直接调用来使用(即不使用 DELEGATECALL
关键字),
特别是,除非能规避 Solidity 的类型系统,否则是不可能销毁任何库的。
库可以看作是使用他们的合约的隐式的基类合约。虽然它们在继承关系中不会显式可见,但调用库函数与调用显式的基类合约十分类似
(可以使用 L.f()
调用)。
当然,需要使用内部调用约定来调用内部函数,这意味着所有内部类型,内存类型都是通过引用而不是复制来传递。
为了在 EVM 中实现这些,内部库函数的代码和从其中调用的所有函数都在编译阶段被包含到调用合约中,然后使用一个 JUMP
调用来代替 DELEGATECALL
。
下面的示例说明如何使用库(但也请务必看看 using for 有一个实现 set 更好的例子)。
pragma solidity >=0.6.0 <0.7.0;
// 我们定义了一个新的结构体数据类型,用于在调用合约中保存数据。
struct Data {
mapping(uint => bool) flags;
}
library Set {
// 注意第一个参数是“storage reference”类型,因此在调用中参数传递的只是它的存储地址而不是内容。
// 这是库函数的一个特性。如果该函数可以被视为对象的方法,则习惯称第一个参数为 `self` 。
function insert(Data storage self, uint value)
public
returns (bool)
{
if (self.flags[value])
return false; // 已经存在
self.flags[value] = true;
return true;
}
function remove(Data storage self, uint value)
public
returns (bool)
{
if (!self.flags[value])
return false; // 不存在
self.flags[value] = false;
return true;
}
function contains(Data storage self, uint value)
public
view
returns (bool)
{
return self.flags[value];
}
}
contract C {
Data knownValues;
function register(uint value) public {
// 不需要库的特定实例就可以调用库函数,
// 因为当前合约就是“instance”。
require(Set.insert(knownValues, value));
}
// 如果我们愿意,我们也可以在这个合约中直接访问 knownValues.flags。
}
当然,你不必按照这种方式去使用库:它们也可以在不定义结构数据类型的情况下使用。 函数也不需要任何存储引用参数,库可以出现在任何位置并且可以有多个存储引用参数。
调用 Set.contains
,Set.insert
和 Set.remove
都被编译为外部调用( DELEGATECALL
)。
如果使用库,请注意实际执行的是外部函数调用。
msg.sender
, msg.value
和 this
在调用中将保留它们的值,
(在 Homestead 之前,因为使用了 CALLCODE
,改变了 msg.sender
和 msg.value
)。
以下示例展示了如何在库中使用内存类型和内部函数来实现自定义类型,而无需支付外部函数调用的开销:
pragma solidity >=0.6.0;
struct bigint {
uint[] limbs;
}
library BigInt {
function fromUint(uint x) internal pure returns (bigint r) {
r.limbs = new uint[](1);
r.limbs[0] = x;
}
function add(bigint _a, bigint _b) internal pure returns (bigint r) {
r.limbs = new uint[](max(_a.limbs.length, _b.limbs.length));
uint carry = 0;
for (uint i = 0; i < r.limbs.length; ++i) {
uint a = limb(_a, i);
uint b = limb(_b, i);
r.limbs[i] = a + b + carry;
if (a + b < a || (a + b == uint(-1) && carry > 0))
carry = 1;
else
carry = 0;
}
if (carry > 0) {
// 太差了,我们需要增加一个 limb
uint[] memory newLimbs = new uint[](r.limbs.length + 1);
for (i = 0; i < r.limbs.length; ++i)
newLimbs[i] = r.limbs[i];
newLimbs[i] = carry;
r.limbs = newLimbs;
}
}
function limb(bigint _a, uint _limb) internal pure returns (uint) {
return _limb < _a.limbs.length ? _a.limbs[_limb] : 0;
}
function max(uint a, uint b) private pure returns (uint) {
return a > b ? a : b;
}
}
contract C {
using BigInt for bigint;
function f() public pure {
bigint memory x = BigInt.fromUint(7);
bigint memory y = BigInt.fromUint(uint(-1));
bigint memory z = x.add(y);
assert(z.limb(1) > 0);
}
}
可以通过类型转换, 将库类型更改为 address
类型, 例如: 使用 address(LibraryName)
由于编译器无法知道库的部署位置,我们需要通过链接器将这些地址填入最终的字节码中
(请参阅 使用命令行编译器 以了解如何使用命令行编译器来链接字节码)。
如果这些地址没有作为参数传递给编译器,编译后的十六进制代码将包含 __Set______
形式的占位符(其中 Set
是库的名称)。
可以手动填写地址来将那 40 个字符替换为库合约地址的十六进制编码。
与合约相比,库的限制:
没有状态变量
不能够继承或被继承
不能接收以太币
不可以被销毁 destroyed
(将来有可能会解除这些限制)
库的函数签名与选择器¶
尽管可以对 public 或 external 的库函数进行外部调用,但此类调用会被视为Solidity的内部调用,与常规的 contract ABI 规则不同。
外部库函数比外部合约函数支持更多的参数类型,例如递归结构和指向存储的指针。
因此,计算用于计算4字节选择器的函数签名遵循内部命名模式以及可对合约ABI中不支持的类型的参数使用内部编码。
以下标识符可以作为函数签名中的类型:
值类型, 非存储的(non-storage)
string
及非存储的bytes
使用和合约 ABI 中同样的标识符。非存储的数组类型遵循合约 ABI 中同样的规则,例如
<type>[]
为动态数组以及<type>[M]
为``M``个元素的动态数组。非存储的结构体使用完整的命名引用,例如
C.S
用于contract C { struct S { ... } }
.存储的映射指针使用
mapping(<keyType> => <valueType>) storage
当<keyType>
和<valueType>
是映射的键和值类型。其他的存储的指针类型使用其对应的非存储类型的类型标识符,但在其后面附加一个空格及``storage``。
除了指向存储的指针以外,参数编码与常规合约ABI相同,存储指针被编码为``uint256``值,指向它们所指向的存储插槽。
与合约 ABI 相似,选择器由签名的Keccak256哈希的前四个字节组成。可以使用 .selector 成员从Solidity中获取其值,如下所示:
pragma solidity >=0.5.14 <0.7.0;
library L {
function f(uint256) external {}
}
contract C {
function g() public pure returns (bytes4) {
return L.f.selector;
}
}
库的调用保护¶
如果库的代码是通过 CALL
来执行,而不是 DELEGATECALL
或者 CALLCODE
那么执行的结果会被回退,
除非是对 view
或者 pure
函数的调用。
EVM 没有为合约提供检测是否使用 CALL
的直接方式,但是合约可以使用 ADDRESS
操作码找出正在运行的“位置”。
生成的代码通过比较这个地址和构造时的地址来确定调用模式。
更具体地说,库的运行时代码总是从一个 push 指令开始,它在编译时是 20 字节的零。当运行部署代码时,这个常数 被内存中的当前地址替换,修改后的代码存储在合约中。在运行时,部署时地址就成为了第一个被 push 到堆栈上的常数, 对于任何 non-view 和 non-pure 函数,调度器代码都将对比当前地址与这个常数是否一致。
这意味着库在链上存储的实际代码与编译器输出的 deployedBytecode
的编码是不同。
Using For¶
在当前的合约上下里, 指令 using A for B;
可用于附加库函数(从库 A
)到任何类型(B
)。
这些函数将接收到调用它们的对象作为它们的第一个参数(像 Python 的 self
变量)。
using A for *;
的效果是,库 A
中的函数被附加在任意的类型上。
在这两种情况下,所有函数都会被附加一个参数,即使它们的第一个参数类型与对象的类型不匹配。 函数调用和重载解析时才会做类型检查。
using A for B;
指令仅在当前作用域有效,目前仅限于在当前合约中,后续可能提升到全局范围。
通过引入一个模块,不需要再添加代码就可以使用包括库函数在内的数据类型。
让我们用这种方式将 库 中的 set 例子重写:
pragma solidity >=0.6.0 <0.8.0;
// 这是和之前一样的代码,只是没有注释。
struct Data { mapping(uint => bool) flags; }
library Set {
function insert(Data storage self, uint value)
public
returns (bool)
{
if (self.flags[value])
return false; // 已经存在
self.flags[value] = true;
return true;
}
function remove(Data storage self, uint value)
public
returns (bool)
{
if (!self.flags[value])
return false; // 不存在
self.flags[value] = false;
return true;
}
function contains(Data storage self, uint value)
public
view
returns (bool)
{
return self.flags[value];
}
}
contract C {
using Set for Data; // 这里是关键的修改
Data knownValues;
function register(uint value) public {
// Here, all variables of type Data have
// corresponding member functions.
// The following function call is identical to
// `Set.insert(knownValues, value)`
// 这里, Data 类型的所有变量都有与之相对应的成员函数。
// 下面的函数调用和 `Set.insert(knownValues, value)` 的效果完全相同。
require(knownValues.insert(value));
}
}
也可以像这样扩展基本类型:
pragma solidity >=0.4.16 <0.8.0;
library Search {
function indexOf(uint[] storage self, uint value)
public
view
returns (uint)
{
for (uint i = 0; i < self.length; i++)
if (self[i] == value) return i;
return uint(-1);
}
}
contract C {
using Search for uint[];
uint[] data;
function append(uint value) public {
data.push(value);
}
function replace(uint _old, uint _new) public {
// 执行库函数调用
uint index = data.indexOf(_old);
if (index == uint(-1))
data.push(_new);
else
data[index] = _new;
}
}
注意,所有 external 库调用都是实际的 EVM 函数调用。这意味着如果传递内存或值类型,都将产生一个副本,即使是 self
变量。
引用存储变量或者 internal 库调用 是唯一不会发生拷贝的情况。