合约
Solidity 中的合约类似于面向对象语言中的类。它们包含状态变量中的持久数据,以及可以修改这些变量的函数。调用不同合约(实例)上的函数将执行 EVM 函数调用,从而切换上下文,使调用合约中的状态变量不可访问。合约及其函数需要被调用才能发生任何事情。以太坊中没有“cron”概念,无法自动在特定事件调用函数。
创建合约
合约可以通过以太坊交易“从外部”创建,也可以从 Solidity 合约内部创建。
IDE,例如 Remix,使用 UI 元素使创建过程无缝衔接。
在以太坊上以编程方式创建合约的一种方法是通过 JavaScript API web3.js。它有一个名为 web3.eth.Contract 的函数,用于方便创建合约。
创建合约时,会执行一次其 构造函数(使用 constructor
关键字声明的函数)。
构造函数是可选的。只允许一个构造函数,这意味着不支持重载。
构造函数执行后,合约的最终代码将存储在区块链上。此代码包括所有公共和外部函数,以及通过函数调用从这些函数可达的所有函数。已部署的代码不包括构造函数代码或仅从构造函数调用的内部函数。
在内部,构造函数参数在合约本身的代码之后使用 ABI 编码 传递,但如果使用 web3.js
,则无需关心这一点。
如果一个合约要创建另一个合约,则创建的合约的源代码(和二进制文件)必须为创建者已知。这意味着循环创建依赖关系是不可能的。
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.22 <0.9.0;
contract OwnedToken {
// `TokenCreator` is a contract type that is defined below.
// It is fine to reference it as long as it is not used
// to create a new contract.
TokenCreator creator;
address owner;
bytes32 name;
// This is the constructor which registers the
// creator and the assigned name.
constructor(bytes32 name_) {
// State variables are accessed via their name
// and not via e.g. `this.owner`. Functions can
// be accessed directly or through `this.f`,
// but the latter provides an external view
// to the function. Especially in the constructor,
// you should not access functions externally,
// because the function does not exist yet.
// See the next section for details.
owner = msg.sender;
// We perform an explicit type conversion from `address`
// to `TokenCreator` and assume that the type of
// the calling contract is `TokenCreator`, there is
// no real way to verify that.
// This does not create a new contract.
creator = TokenCreator(msg.sender);
name = name_;
}
function changeName(bytes32 newName) public {
// Only the creator can alter the name.
// We compare the contract based on its
// address which can be retrieved by
// explicit conversion to address.
if (msg.sender == address(creator))
name = newName;
}
function transfer(address newOwner) public {
// Only the current owner can transfer the token.
if (msg.sender != owner) return;
// We ask the creator contract if the transfer
// should proceed by using a function of the
// `TokenCreator` contract defined below. If
// the call fails (e.g. due to out-of-gas),
// the execution also fails here.
if (creator.isTokenTransferOK(owner, newOwner))
owner = newOwner;
}
}
contract TokenCreator {
function createToken(bytes32 name)
public
returns (OwnedToken tokenAddress)
{
// Create a new `Token` contract and return its address.
// From the JavaScript side, the return type
// of this function is `address`, as this is
// the closest type available in the ABI.
return new OwnedToken(name);
}
function changeName(OwnedToken tokenAddress, bytes32 name) public {
// Again, the external type of `tokenAddress` is
// simply `address`.
tokenAddress.changeName(name);
}
// Perform checks to determine if transferring a token to the
// `OwnedToken` contract should proceed
function isTokenTransferOK(address currentOwner, address newOwner)
public
pure
returns (bool ok)
{
// Check an arbitrary condition to see if transfer should proceed
return keccak256(abi.encodePacked(currentOwner, newOwner))[0] == 0x7f;
}
}
可见性和 Getter
状态变量可见性
public
公共状态变量与内部状态变量的区别仅在于编译器会自动为它们生成 Getter 函数,这允许其他合约读取它们的值。在同一合约内使用时,外部访问(例如
this.x
)会调用 Getter,而内部访问(例如x
)会直接从存储中获取变量值。不会生成 Setter 函数,因此其他合约无法直接修改它们的值。internal
内部状态变量只能从定义它们的合约以及派生合约内部访问。它们无法从外部访问。这是状态变量的默认可见性级别。
private
私有状态变量类似于内部状态变量,但它们在派生合约中不可见。
警告
将某事物设为 private
或 internal
只能阻止其他合约读取或修改信息,但它仍然对区块链之外的整个世界可见。
函数可见性
Solidity 知道两种类型的函数调用:外部调用会创建实际的 EVM 消息调用,而内部调用不会。此外,内部函数可以设置为无法访问派生合约。这产生了函数的四种可见性类型。
external
外部函数是合约接口的一部分,这意味着可以从其他合约和通过交易调用它们。外部函数
f
无法在内部调用(即f()
不起作用,但this.f()
起作用)。public
公共函数是合约接口的一部分,可以从内部调用或通过消息调用。
internal
内部函数只能从当前合约或派生自它的合约内部访问。它们无法从外部访问。由于它们没有通过合约的 ABI 公开到外部,因此它们可以接收内部类型参数,例如映射或存储引用。
private
私有函数类似于内部函数,但它们在派生合约中不可见。
警告
将某事物设为 private
或 internal
只能阻止其他合约读取或修改信息,但它仍然对区块链之外的整个世界可见。
可见性说明符在状态变量的类型之后给出,在函数的参数列表和返回值参数列表之间给出。
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.16 <0.9.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
。
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.16 <0.9.0;
contract C {
uint private data;
function f(uint a) private pure returns(uint b) { return a + 1; }
function setData(uint a) public { data = a; }
function getData() public view returns(uint) { return data; }
function compute(uint a, uint b) internal pure returns (uint) { return a + b; }
}
// This will not compile
contract D {
function readData() public {
C c = new C();
uint local = c.f(7); // error: member `f` is not visible
c.setData(3);
local = c.getData();
local = c.compute(3, 5); // error: member `compute` is not visible
}
}
contract E is C {
function g() public {
C c = new C();
uint val = compute(3, 5); // access to internal member (from derived to parent contract)
}
}
Getter 函数
编译器会自动为所有 公共 状态变量创建 Getter 函数。对于下面给出的合约,编译器将生成一个名为 data
的函数,该函数不接受任何参数,并返回一个 uint
,即状态变量 data
的值。状态变量可以在声明时初始化。
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.16 <0.9.0;
contract C {
uint public data = 42;
}
contract Caller {
C c = new C();
function f() public view returns (uint) {
return c.data();
}
}
Getter 函数具有外部可见性。如果内部访问符号(即没有 this.
),它将评估为状态变量。如果外部访问它(即使用 this.
),它将评估为函数。
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.0 <0.9.0;
contract C {
uint public data;
function x() public returns (uint) {
data = 3; // internal access
return this.data(); // external access
}
}
如果有一个 public
状态变量的数组类型,那么你只能通过生成的 Getter 函数检索数组的单个元素。这种机制的存在是为了避免在返回整个数组时产生高昂的 gas 成本。可以使用参数指定要返回的单个元素,例如 myArray(0)
。如果你想在一个调用中返回整个数组,那么你需要编写一个函数,例如
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.16 <0.9.0;
contract arrayExample {
// public state variable
uint[] public myArray;
// Getter function generated by the compiler
/*
function myArray(uint i) public view returns (uint) {
return myArray[i];
}
*/
// function that returns entire array
function getArray() public view returns (uint[] memory) {
return myArray;
}
}
现在可以使用 getArray()
来检索整个数组,而不是 myArray(i)
,后者每次调用都会返回单个元素。
下一个示例更复杂
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.0 <0.9.0;
contract Complex {
struct Data {
uint a;
bytes3 b;
mapping(uint => uint) map;
uint[3] c;
uint[] d;
bytes e;
}
mapping(uint => mapping(bool => Data[])) public data;
}
它会生成一个以下形式的函数。结构中的映射和数组(除了字节数组)被省略,因为没有很好的方法来选择单个结构成员或为映射提供键
function data(uint arg1, bool arg2, uint arg3)
public
returns (uint a, bytes3 b, bytes memory e)
{
a = data[arg1][arg2][arg3].a;
b = data[arg1][arg2][arg3].b;
e = data[arg1][arg2][arg3].e;
}
函数修饰符
修饰符可以用来以声明式的方式改变函数的行为。例如,可以使用修饰符在执行函数之前自动检查条件。
修饰符是合约的继承属性,可以被派生合约覆盖,但前提是它们被标记为 virtual
。有关详细信息,请参阅 修饰符覆盖。
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.1 <0.9.0;
contract owned {
constructor() { owner = payable(msg.sender); }
address payable owner;
// This contract only defines a modifier but does not use
// it: it will be used in derived contracts.
// The function body is inserted where the special symbol
// `_;` in the definition of a modifier appears.
// This means that if the owner calls this function, the
// function is executed and otherwise, an exception is
// thrown.
modifier onlyOwner {
require(
msg.sender == owner,
"Only owner can call this function."
);
_;
}
}
contract priced {
// Modifiers can receive arguments:
modifier costs(uint price) {
if (msg.value >= price) {
_;
}
}
}
contract Register is priced, owned {
mapping(address => bool) registeredAddresses;
uint price;
constructor(uint initialPrice) { price = initialPrice; }
// It is important to also provide the
// `payable` keyword here, otherwise the function will
// automatically reject all Ether sent to it.
function register() public payable costs(price) {
registeredAddresses[msg.sender] = true;
}
// This contract inherits the `onlyOwner` modifier from
// the `owned` contract. As a result, calls to `changePrice` will
// only take effect if they are made by the stored owner.
function changePrice(uint price_) public onlyOwner {
price = price_;
}
}
contract Mutex {
bool locked;
modifier noReentrancy() {
require(
!locked,
"Reentrant call."
);
locked = true;
_;
locked = false;
}
/// This function is protected by a mutex, which means that
/// reentrant calls from within `msg.sender.call` cannot call `f` again.
/// The `return 7` statement assigns 7 to the return value but still
/// executes the statement `locked = false` in the modifier.
function f() public noReentrancy returns (uint) {
(bool success,) = msg.sender.call("");
require(success);
return 7;
}
}
如果你想访问合约 C
中定义的修饰符 m
,可以使用 C.m
来引用它,而无需虚拟查找。只能使用当前合约或其基合约中定义的修饰符。修饰符也可以在库中定义,但它们的使用仅限于同一库中的函数。
多个修饰符通过在空格分隔的列表中指定它们来应用于函数,并按呈现的顺序进行评估。
修饰符无法隐式访问或改变它们修改的函数的参数和返回值。它们的值只能在调用点显式传递给它们。
在函数修饰符中,需要指定何时运行应用修饰符的函数。占位符语句(用单个下划线字符 _
表示)用于表示应插入被修改函数的主体的位置。请注意,占位符运算符与在变量名中使用下划线作为前导或尾随字符不同,后者是一种风格选择。
从修饰符或函数主体中显式返回只会离开当前修饰符或函数主体。返回值变量被分配,控制流在前面修饰符中的 _
之后继续。
警告
在早期版本的 Solidity 中,具有修饰符的函数中的 return
语句的行为不同。
从使用 return;
的修饰符中显式返回不会影响函数返回的值。但是,修饰符可以选择完全不执行函数主体,在这种情况下,返回值变量将被设置为它们的 默认值,就像函数具有空主体一样。
_
符号可以在修饰符中多次出现。每次出现都会被替换为函数主体,函数返回最终出现的返回值。
修饰符参数和此上下文中的任意表达式都允许,在此上下文中,函数可见的所有符号都在修饰符中可见。在修饰符中引入的符号在函数中不可见(因为它们可能会通过覆盖而改变)。
常量和不可变状态变量
状态变量可以声明为 constant
或 immutable
。在这两种情况下,变量在合约构造后都不能修改。对于 constant
变量,值必须在编译时固定,而对于 immutable
,它仍然可以在构造时分配。
也可以在文件级别定义 constant
变量。
编译器不会为这些变量保留存储槽,并且每个出现都会被相应的值替换。
与常规状态变量相比,常量和不可变变量的燃气成本要低得多。对于常量变量,分配给它的表达式被复制到访问它的所有地方,并且每次也被重新评估。这允许进行本地优化。不可变变量在构造时评估一次,它们的值被复制到代码中访问它们的所有地方。对于这些值,即使它们可以容纳在更少的字节中,也会保留 32 个字节。因此,常量值有时比不可变值便宜。
目前并非所有常量和不可变类型的类型都已实现。唯一支持的类型是 字符串(仅限常量)和 值类型。
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.21;
uint constant X = 32**22 + 8;
contract C {
string constant TEXT = "abc";
bytes32 constant MY_HASH = keccak256("abc");
uint immutable decimals = 18;
uint immutable maxBalance;
address immutable owner = msg.sender;
constructor(uint decimals_, address ref) {
if (decimals_ != 0)
// Immutables are only immutable when deployed.
// At construction time they can be assigned to any number of times.
decimals = decimals_;
// Assignments to immutables can even access the environment.
maxBalance = ref.balance;
}
function isBalanceTooHigh(address other) public view returns (bool) {
return other.balance > maxBalance;
}
}
常量
对于 constant
变量,值必须在编译时为常量,并且必须在声明变量时分配。任何访问存储、区块链数据(例如 block.timestamp
、address(this).balance
或 block.number
)或执行数据(msg.value
或 gasleft()
)或对外部合约进行调用或对外部合约进行调用的表达式都不允许。可能对内存分配有副作用的表达式是允许的,但那些可能对其他内存对象有副作用的表达式是不允许的。内置函数 keccak256
、sha256
、ripemd160
、ecrecover
、addmod
和 mulmod
是允许的(即使除了 keccak256
之外,它们确实调用了外部合约)。
允许对内存分配器有副作用的原因是,应该能够构造复杂的对象,例如查找表。此功能尚未完全可用。
不可变
声明为 immutable
的变量比声明为 constant
的变量限制少一些:不可变变量可以在构造时分配一个值。该值可以在部署之前随时更改,然后它将变得永久。
另一个限制是不可变变量只能在没有可能在创建后执行的表达式中分配。这排除了所有修饰符定义和构造函数以外的函数。
读取不可变变量没有限制。即使在首次写入变量之前,也允许读取,因为 Solidity 中的变量始终具有明确定义的初始值。因此,也允许永远不要显式地为不可变变量分配值。
警告
在构造时访问不可变变量时,请牢记 初始化顺序。即使您提供了显式初始化程序,某些表达式最终也可能在该初始化程序之前进行评估,尤其是在它们在继承层次结构中处于不同级别时。
注意
在 Solidity 0.8.21 之前,不可变变量的初始化更严格。此类变量必须在构造时恰好初始化一次,并且在此之前不能读取。
编译器生成的合约创建代码将在返回之前修改合约的运行时代码,将对不可变变量的所有引用替换为分配给它们的值。如果您比较编译器生成的运行时代码与实际存储在区块链中的运行时代码,这一点很重要。编译器在 编译器 JSON 标准输出 的 immutableReferences
字段中输出这些不可变变量在部署的字节码中的位置。
函数
可以在合约内部和外部定义函数。
合约外部的函数(也称为“自由函数”)始终具有隐式 internal
可见性。它们的代码包含在调用它们的每个合约中,类似于内部库函数。
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.1 <0.9.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;
}
}
注意
在合约外部定义的函数仍然始终在合约的上下文中执行。它们仍然可以调用其他合约,向它们发送以太坊并销毁调用它们的合约,等等。与在合约内部定义的函数的主要区别在于,自由函数无法直接访问变量 this
、存储变量和不在其范围内的函数。
函数参数和返回值变量
函数接受类型化的参数作为输入,并且可能与许多其他语言不同,还返回任意数量的值作为输出。
函数参数
函数参数的声明方式与变量相同,未使用参数的名称可以省略。
例如,如果您希望您的合约接受具有两个整数的一种外部调用,您将使用类似以下内容
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.16 <0.9.0;
contract Simple {
uint sum;
function taker(uint a, uint b) public {
sum = a + b;
}
}
函数参数可以用作任何其他局部变量,也可以分配给它们。
返回值变量
函数返回值变量在 returns
关键字之后使用相同的语法声明。
例如,假设您想返回两个结果:作为函数参数传递的两个整数的和与积,那么您将使用类似以下内容
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.16 <0.9.0;
contract Simple {
function arithmetic(uint a, uint b)
public
pure
returns (uint sum, uint product)
{
sum = a + b;
product = a * b;
}
}
返回值变量的名称可以省略。返回值变量可以用作任何其他局部变量,它们用它们的 默认值 初始化,并在分配给它们之前具有该值。
您可以显式地分配返回值变量,然后离开函数,如上所示,也可以使用 return
语句直接提供返回值(单个或 多个)。
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.16 <0.9.0;
contract Simple {
function arithmetic(uint a, uint b)
public
pure
returns (uint sum, uint product)
{
return (a + b, a * b);
}
}
如果您使用早期 return
离开具有返回值变量的函数,则必须与 return 语句一起提供返回值。
注意
您不能从非内部函数返回某些类型。这包括下面列出的类型以及递归包含它们的任何复合类型
映射,
内部函数类型,
位置设置为
storage
的引用类型,多维数组(仅适用于 ABI 编码器 v1),
结构体(仅适用于 ABI 编码器 v1)。
此限制不适用于库函数,因为它们有不同的 内部 ABI。
返回多个值
当函数具有多个返回类型时,语句 return (v0, v1, ..., vn)
可用于返回多个值。组件的数量必须与返回值变量的数量相同,并且它们的类型必须匹配,可能在 隐式转换 之后。
状态可变性
视图函数
函数可以声明为 view
,在这种情况下,它们承诺不会修改状态。
注意
如果编译器的 EVM 目标是拜占庭或更新版本(默认),则当调用 view
函数时,将使用 STATICCALL
操作码,这会强制在 EVM 执行过程中状态保持不变。对于库 view
函数,使用 DELEGATECALL
,因为没有组合的 DELEGATECALL
和 STATICCALL
。这意味着库 view
函数没有阻止状态修改的运行时检查。这不会对安全性造成负面影响,因为库代码通常在编译时已知,并且静态检查器会执行编译时检查。
以下语句被认为是修改状态
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.5.0 <0.9.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
函数中修改状态。通过对 view
函数使用 STATICCALL
,在 EVM 级别阻止了对状态的修改。
纯函数
函数可以声明为 pure
,在这种情况下,它们承诺不会从状态读取或修改状态。特别是,应该能够在编译时仅根据其输入和 msg.data
来评估 pure
函数,但没有任何关于当前区块链状态的知识。这意味着从 immutable
变量中读取可能是非纯操作。
注意
如果编译器的 EVM 目标是拜占庭或更新版本(默认),则使用 STATICCALL
操作码,它不保证不会读取状态,但至少保证不会修改状态。
除了上面解释的修改状态语句列表之外,以下被认为是从状态读取
从状态变量读取。
访问
address(this).balance
或<address>.balance
。访问
block
、tx
、msg
的任何成员(msg.sig
和msg.data
除外)。调用任何未标记为
pure
的函数。使用包含某些操作码的内联汇编。
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.5.0 <0.9.0;
contract C {
function f(uint a, uint b) public pure returns (uint) {
return a * (b + 42);
}
}
纯函数能够使用 revert()
和 require()
函数在发生错误时回滚潜在的状态更改。
回滚状态更改不被视为“状态修改”,因为只回滚以前在没有 view
或 pure
限制的代码中做出的状态更改,并且该代码可以选择捕获 revert
而不将其传递下去。
此行为也与 STATICCALL
操作码一致。
警告
无法阻止函数在 EVM 级别读取状态,只能阻止它们写入状态(即,只有 view
可以强制在 EVM 级别执行,pure
无法执行)。
注意
在 0.5.0 版本之前,编译器没有对 pure
函数使用 STATICCALL
操作码。这使得能够通过使用无效的显式类型转换在 pure
函数中修改状态。通过对 pure
函数使用 STATICCALL
,在 EVM 级别阻止了对状态的修改。
注意
在 0.4.17 版本之前,编译器没有强制执行 pure
不读取状态。它是一个编译时类型检查,可以通过对合约类型进行无效的显式转换来规避,因为编译器可以验证合约类型不会执行更改状态的操作,但无法检查在运行时调用的合约实际上是否为该类型。
特殊函数
接收以太坊函数
一个合约最多可以有一个 receive
函数,使用 receive() external payable { ... }
声明(不使用 function
关键字)。此函数不能有参数,不能返回值,并且必须具有 external
可见性和 payable
状态可变性。它可以是虚拟的,可以覆盖,也可以有修饰符。
接收函数在对合约进行空 calldata 的调用时执行。这是在简单以太坊转账(例如通过 .send()
或 .transfer()
)时执行的函数。如果没有这样的函数,但存在可支付的回退函数,则在简单以太坊转账时将调用回退函数。如果既没有接收以太坊函数,也没有可支付的回退函数,则合约无法通过不代表可支付函数调用的交易接收以太坊,并会抛出异常。
在最坏的情况下,receive
函数只能依赖于可用的 2300 气体(例如,当使用 send
或 transfer
时),除了基本日志记录之外,几乎没有空间执行其他操作。以下操作将消耗比 2300 气体津贴更多的气体
写入存储
创建合约
调用消耗大量气体的外部函数
发送以太坊
警告
当以太坊直接发送到合约(没有函数调用,即发送者使用 send
或 transfer
),但接收合约没有定义接收以太坊函数或可支付的回退函数时,将抛出异常,并将以太坊发回(这在 Solidity v0.4.0 之前是不同的)。如果您希望您的合约接收以太坊,则必须实现接收以太坊函数(不建议使用可支付的回退函数接收以太坊,因为回退会被调用,并且不会由于发送者的接口混淆而失败)。
警告
没有接收以太坊函数的合约可以作为coinbase 交易(也称为矿工区块奖励)的接收者或作为selfdestruct
的目的地接收以太坊。
合约无法对这种以太坊转账做出反应,因此也无法拒绝它们。这是 EVM 的设计选择,Solidity 无法绕过它。
这也意味着 address(this).balance
可能高于合约中实现的一些手动记账的总和(例如,在接收以太坊函数中更新计数器)。
下面您可以看到使用函数 receive
的 Sink 合约示例。
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.6.0 <0.9.0;
// This contract keeps all Ether sent to it with no way
// to get it back.
contract Sink {
event Received(address, uint);
receive() external payable {
emit Received(msg.sender, msg.value);
}
}
回退函数
一个合约最多可以有一个 fallback
函数,使用 fallback () external [payable]
或 fallback (bytes calldata input) external [payable] returns (bytes memory output)
声明(两者都不使用 function
关键字)。此函数必须具有 external
可见性。回退函数可以是虚拟的,可以覆盖,也可以有修饰符。
如果其他任何函数都不匹配给定的函数签名,或者根本没有提供数据且没有接收以太坊函数,则会执行回退函数。回退函数始终接收数据,但为了接收以太坊,它必须标记为 payable
。
如果使用带参数的版本,input
将包含发送到合约的完整数据(等于 msg.data
),并且可以在 output
中返回数据。返回的数据不会被 ABI 编码。相反,它将在没有修改的情况下返回(甚至没有填充)。
在最坏的情况下,如果可支付的回退函数也用作接收函数的替代,它只能依赖于可用的 2300 气体(请参阅接收以太坊函数以简要了解其含义)。
与任何函数一样,回退函数可以执行复杂的操作,只要有足够的 gas 传递给它即可。
警告
如果不存在接收以太坊函数,可支付的回退函数也会在简单以太坊转账时执行。建议始终也定义接收以太坊函数,如果您定义了可支付的回退函数,以便区分以太坊转账和接口混淆。
注意
如果您想解码输入数据,您可以检查前四个字节以获取函数选择器,然后您可以使用 abi.decode
以及数组切片语法来解码 ABI 编码的数据:(c, d) = abi.decode(input[4:], (uint256, uint256));
请注意,这应该只用作最后的手段,应该使用适当的函数。
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.6.2 <0.9.0;
contract Test {
uint x;
// This function is called for all messages sent to
// this contract (there is no other function).
// Sending Ether to this contract will cause an exception,
// because the fallback function does not have the `payable`
// modifier.
fallback() external { x = 1; }
}
contract TestPayable {
uint x;
uint y;
// This function is called for all messages sent to
// this contract, except plain Ether transfers
// (there is no other function except the receive function).
// Any call with non-empty calldata to this contract will execute
// the fallback function (even if Ether is sent along with the call).
fallback() external payable { x = 1; y = msg.value; }
// This function is called for plain Ether transfers, i.e.
// for every call with empty calldata.
receive() external payable { x = 2; y = msg.value; }
}
contract Caller {
function callTest(Test test) public returns (bool) {
(bool success,) = address(test).call(abi.encodeWithSignature("nonExistingFunction()"));
require(success);
// results in test.x becoming == 1.
// address(test) will not allow to call ``send`` directly, since ``test`` has no payable
// fallback function.
// It has to be converted to the ``address payable`` type to even allow calling ``send`` on it.
address payable testPayable = payable(address(test));
// If someone sends Ether to that contract,
// the transfer will fail, i.e. this returns false here.
return testPayable.send(2 ether);
}
function callTestPayable(TestPayable test) public returns (bool) {
(bool success,) = address(test).call(abi.encodeWithSignature("nonExistingFunction()"));
require(success);
// results in test.x becoming == 1 and test.y becoming 0.
(success,) = address(test).call{value: 1}(abi.encodeWithSignature("nonExistingFunction()"));
require(success);
// results in test.x becoming == 1 and test.y becoming 1.
// If someone sends Ether to that contract, the receive function in TestPayable will be called.
// Since that function writes to storage, it takes more gas than is available with a
// simple ``send`` or ``transfer``. Because of that, we have to use a low-level call.
(success,) = address(test).call{value: 2 ether}("");
require(success);
// results in test.x becoming == 2 and test.y becoming 2 ether.
return true;
}
}
函数重载
一个合约可以有多个同名但参数类型不同的函数。这个过程被称为“重载”,也适用于继承的函数。以下示例展示了合约 A
范围内函数 f
的重载。
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.16 <0.9.0;
contract A {
function f(uint value) public pure returns (uint out) {
out = value;
}
function f(uint value, bool really) public pure returns (uint out) {
if (really)
out = value;
}
}
重载函数也存在于外部接口中。如果两个外部可见的函数在 Solidity 类型上不同,但在外部类型上相同,则会发生错误。
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.16 <0.9.0;
// This will not compile
contract A {
function f(B value) public pure returns (B out) {
out = value;
}
function f(address value) public pure returns (address out) {
out = value;
}
}
contract B {
}
上面的两个 f
函数重载最终都接受 ABI 的地址类型,尽管它们在 Solidity 内部被认为是不同的。
重载解析和参数匹配
重载函数通过将当前范围内的函数声明与函数调用中提供的参数进行匹配来选择。如果所有参数都可以隐式转换为预期类型,则函数将被选为重载候选者。如果没有正好一个候选者,则解析失败。
注意
返回值参数不会被考虑用于重载解析。
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.16 <0.9.0;
contract A {
function f(uint8 val) public pure returns (uint8 out) {
out = val;
}
function f(uint256 val) public pure returns (uint256 out) {
out = val;
}
}
调用 f(50)
会创建一个类型错误,因为 50
可以隐式转换为 uint8
和 uint256
类型。另一方面,f(256)
将解析为 f(uint256)
重载,因为 256
不能隐式转换为 uint8
。
事件
Solidity 事件在 EVM 的日志记录功能之上提供了一个抽象。应用程序可以通过以太坊客户端的 RPC 接口订阅并监听这些事件。
事件可以在文件级别定义,也可以作为合约(包括接口和库)的可继承成员定义。当您调用它们时,它们会导致参数存储在交易的日志中——区块链中的一个特殊数据结构。这些日志与发出它们的合约的地址相关联,被合并到区块链中,并只要区块可访问就一直存在(到目前为止是永远的,但这在将来可能会改变)。日志及其事件数据无法从合约内部访问(即使是从创建它们的合约也是如此)。
可以为日志请求 Merkle 证明,因此,如果外部实体向合约提供此类证明,它可以检查该日志实际上是否存在于区块链中。您必须提供区块头,因为合约只能看到最后 256 个区块哈希值。
您可以添加属性 indexed
到最多三个参数,这将它们添加到一个称为 “主题” 的特殊数据结构中,而不是日志的数据部分。一个主题只能容纳一个字(32 字节),因此如果您为索引参数使用 引用类型,则值的 Keccak-256 哈希值将作为主题存储。
所有没有 indexed
属性的参数都被 ABI 编码 到日志的数据部分。
主题允许您搜索事件,例如,在过滤一定事件的区块序列时。您还可以按发出事件的合约地址过滤事件。
例如,下面的代码使用 web3.js 的 subscribe("logs")
方法 过滤与具有特定地址值的主题匹配的日志
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
说明符声明事件。这意味着不可能按名称过滤特定的匿名事件,您只能按合约地址过滤。匿名事件的优势是它们部署和调用成本更低。它还允许您声明四个索引参数而不是三个。
注意
由于交易日志只存储事件数据而不存储类型,因此您必须知道事件的类型,包括哪个参数被索引以及事件是否匿名,才能正确解释数据。特别是,可以使用匿名事件“伪造”另一个事件的签名。
事件成员
event.selector
:对于非匿名事件,这是一个包含事件签名的keccak256
哈希值的bytes32
值,如默认主题中所使用。
示例
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.21 <0.9.0;
contract ClientReceipt {
event Deposit(
address indexed from,
bytes32 indexed id,
uint value
);
function deposit(bytes32 id) public payable {
// Events are emitted using `emit`, followed by
// the name of the event and the arguments
// (if any) in parentheses. Any such invocation
// (even deeply nested) can be detected from
// the JavaScript API by filtering for `Deposit`.
emit Deposit(msg.sender, id, msg.value);
}
}
在 JavaScript API 中的使用如下
var abi = /* abi as generated by the compiler */;
var ClientReceipt = web3.eth.contract(abi);
var clientReceipt = ClientReceipt.at("0x1234...ab67" /* address */);
var depositEvent = clientReceipt.Deposit();
// watch for changes
depositEvent.watch(function(error, result){
// result contains non-indexed arguments and topics
// given to the `Deposit` call.
if (!error)
console.log(result);
});
// Or pass a callback to start watching immediately
var depositEvent = 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"]
}
}
了解事件的其他资源
错误和回滚语句
Solidity 中的错误提供了一种方便且节省 gas 的方法来向用户解释操作失败的原因。它们可以在合约内部和外部(包括接口和库)定义。
它们必须与 回滚语句 一起使用,该语句会导致当前调用中的所有更改被回滚,并将错误数据传递回调用方。
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.4;
/// Insufficient balance for transfer. Needed `required` but only
/// `available` available.
/// @param available balance available.
/// @param required requested amount to transfer.
error InsufficientBalance(uint256 available, uint256 required);
contract TestToken {
mapping(address => uint) balance;
function transfer(address to, uint256 amount) public {
if (amount > balance[msg.sender])
revert InsufficientBalance({
available: balance[msg.sender],
required: amount
});
balance[msg.sender] -= amount;
balance[to] += amount;
}
// ...
}
错误不能重载或覆盖,但可以继承。相同的错误可以在多个地方定义,只要范围不同即可。错误的实例只能使用 revert
语句创建。
错误创建数据,然后通过回滚操作将其传递给调用方,以便返回到链下组件或在 try/catch 语句 中捕获它。请注意,错误只能在来自外部调用的情况下被捕获,内部调用或同一函数内部发生的回滚无法被捕获。
如果您不提供任何参数,则错误只需要四个字节的数据,并且可以使用 NatSpec 按照上面所述进一步解释错误背后的原因,这些原因不会存储在链上。这使得此功能成为一种非常廉价且方便的错误报告功能。
更具体地说,错误实例以与对具有相同名称和类型的函数的函数调用相同的方式进行 ABI 编码,然后用作 revert
操作码中的返回值数据。这意味着数据由一个 4 字节选择器组成,后面跟着 ABI 编码 数据。选择器由错误类型的签名的 keccak256 哈希值的前四个字节组成。
注意
一个合约可以回滚不同的同名错误,甚至可以回滚定义在不同地方但对调用方无法区分的错误。对于外部(即 ABI),只有错误的名称是相关的,而不是它所在的合约或文件。
语句 require(condition, "description");
等效于 if (!condition) revert Error("description")
,如果您能够定义 error Error(string)
。但是,请注意,Error
是一种内置类型,不能在用户提供的代码中定义。
类似地,失败的 assert
或类似条件将回滚内置类型 Panic(uint256)
的错误。
注意
错误数据应该只用于指示失败,而不是作为控制流的手段。原因是内部调用的回滚数据默认情况下会通过外部调用的链传播回去。这意味着内部调用可以“伪造”回滚数据,这些数据看起来像是来自调用它的合约。
错误成员
error.selector
:包含错误选择器的bytes4
值。
继承
Solidity 支持多重继承,包括多态性。
多态性意味着函数调用(内部和外部)始终在继承层次结构中最派生的合约中执行同名(和参数类型)的函数。这必须使用 virtual
和 override
关键字在层次结构中的每个函数上显式启用。有关详细信息,请参阅 函数覆盖。
可以通过显式指定合约使用 ContractName.functionName()
或使用 super.functionName()
在内部调用继承层次结构中更上层的函数,如果您想在扁平化继承层次结构(见下文)中向上调用一个级别的函数。
当合约从其他合约继承时,只会在区块链上创建一个合约,并且所有基合约的代码都将编译到创建的合约中。这意味着对基合约函数的所有内部调用也只使用内部函数调用(super.f(..)
将使用 JUMP 而不是消息调用)。
状态变量遮蔽被视为错误。派生合约只能声明一个状态变量 x
,如果其任何基类中都没有同名可见状态变量。
一般的继承系统与Python 的非常相似,尤其是在多重继承方面,但也有一些区别。
以下示例中给出了详细信息。
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.0 <0.9.0;
contract Owned {
address payable owner;
constructor() { owner = payable(msg.sender); }
}
// 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 Emittable is Owned {
event Emitted();
// The keyword `virtual` means that the function can change
// its behavior in derived classes ("overriding").
function emitEvent() virtual public {
if (msg.sender == owner)
emit Emitted();
}
}
// 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.
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;
}
// Multiple inheritance is possible. Note that `Owned` is
// also a base class of `Emittable`, yet there is only a single
// instance of `Owned` (as for virtual inheritance in C++).
contract Named is Owned, Emittable {
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.
// If you want the function to override, you need to use the
// `override` keyword. You need to specify the `virtual` keyword again
// if you want this function to be overridden again.
function emitEvent() public virtual override {
if (msg.sender == owner) {
Config config = Config(0xD5f9D8D94886E70b06E474c3fB14Fd43E2f23970);
NameReg(config.lookup(1)).unregister();
// It is still possible to call a specific
// overridden function.
Emittable.emitEvent();
}
}
}
// 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, Emittable, Named("GoldFeed") {
uint info;
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 behavior of `emitEvent` anymore.
function emitEvent() public override(Emittable, Named) { Named.emitEvent(); }
function get() public view returns(uint r) { return info; }
}
请注意,在上面,我们调用 Emittable.emitEvent()
来“转发”发射事件请求。这种方式存在问题,如以下示例所示
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.0 <0.9.0;
contract Owned {
address payable owner;
constructor() { owner = payable(msg.sender); }
}
contract Emittable is Owned {
event Emitted();
function emitEvent() virtual public {
if (msg.sender == owner) {
emit Emitted();
}
}
}
contract Base1 is Emittable {
event Base1Emitted();
function emitEvent() public virtual override {
/* Here, we emit an event to simulate some Base1 logic */
emit Base1Emitted();
Emittable.emitEvent();
}
}
contract Base2 is Emittable {
event Base2Emitted();
function emitEvent() public virtual override {
/* Here, we emit an event to simulate some Base2 logic */
emit Base2Emitted();
Emittable.emitEvent();
}
}
contract Final is Base1, Base2 {
event FinalEmitted();
function emitEvent() public override(Base1, Base2) {
/* Here, we emit an event to simulate some Final logic */
emit FinalEmitted();
Base2.emitEvent();
}
}
调用 Final.emitEvent()
会调用 Base2.emitEvent
,因为我们在最终覆盖中明确指定了它,但此函数将绕过 Base1.emitEvent
,导致以下事件序列:FinalEmitted -> Base2Emitted -> Emitted
,而不是预期的序列:FinalEmitted -> Base2Emitted -> Base1Emitted -> Emitted
。解决方法是使用 super
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.0 <0.9.0;
contract Owned {
address payable owner;
constructor() { owner = payable(msg.sender); }
}
contract Emittable is Owned {
event Emitted();
function emitEvent() virtual public {
if (msg.sender == owner) {
emit Emitted();
}
}
}
contract Base1 is Emittable {
event Base1Emitted();
function emitEvent() public virtual override {
/* Here, we emit an event to simulate some Base1 logic */
emit Base1Emitted();
super.emitEvent();
}
}
contract Base2 is Emittable {
event Base2Emitted();
function emitEvent() public virtual override {
/* Here, we emit an event to simulate some Base2 logic */
emit Base2Emitted();
super.emitEvent();
}
}
contract Final is Base1, Base2 {
event FinalEmitted();
function emitEvent() public override(Base1, Base2) {
/* Here, we emit an event to simulate some Final logic */
emit FinalEmitted();
super.emitEvent();
}
}
如果 Final
调用 super
的函数,它不会简单地调用其一个基类上的该函数。相反,它会调用最终继承图中下一个基类上的该函数,因此它将调用 Base1.emitEvent()
(注意,最终继承顺序是——从派生程度最高的合约开始:Final、Base2、Base1、Emittable、Owned)。使用 super 时实际调用的函数在使用它的类上下文中是未知的,尽管它的类型是已知的。这与普通虚拟方法查找类似。
函数覆盖
基函数可以通过继承合约进行覆盖,如果它们被标记为 virtual
,则可以更改它们的行为。覆盖函数必须在函数头中使用 override
关键字。覆盖函数只能将被覆盖函数的可见性从 external
更改为 public
。可变性可以更改为更严格的,遵循以下顺序:nonpayable
可以被 view
和 pure
覆盖。 view
可以被 pure
覆盖。 payable
是一个例外,不能更改为任何其他可变性。
以下示例演示了更改可变性和可见性
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.0 <0.9.0;
contract Base
{
function foo() virtual external view {}
}
contract Middle is Base {}
contract Inherited is Middle
{
function foo() override public pure {}
}
对于多重继承,在 override
关键字之后必须明确指定定义相同函数的派生程度最高的基合约。换句话说,您必须指定所有定义相同函数的基合约,并且还没有被另一个基合约(在继承图中的某些路径上)覆盖。此外,如果一个合约从多个(不相关)基类继承相同的函数,则必须显式覆盖它
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.6.0 <0.9.0;
contract Base1
{
function foo() virtual public {}
}
contract Base2
{
function foo() virtual public {}
}
contract Inherited is Base1, Base2
{
// Derives from multiple bases defining foo(), so we must explicitly
// override it
function foo() public override(Base1, Base2) {}
}
如果函数在公共基类中定义,或者如果公共基类中存在一个唯一的函数已经覆盖了所有其他函数,则不需要显式覆盖说明符。
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.6.0 <0.9.0;
contract A { function f() public pure{} }
contract B is A {}
contract C is A {}
// No explicit override required
contract D is B, C {}
更正式地说,如果存在一个基类是签名所有覆盖路径的一部分,并且 (1) 该基类实现该函数,并且从当前合约到该基类的任何路径都没有提及具有该签名的函数,或者 (2) 该基类没有实现该函数,并且在从当前合约到该基类所有路径中最多提及一次该函数,则不需要覆盖从多个基类继承的函数(直接或间接)。
从这个意义上讲,签名的覆盖路径是通过继承图的路径,该路径从正在考虑的合约开始,结束于提及具有该签名的函数的合约,该合约不覆盖。
如果您没有将覆盖的函数标记为 virtual
,则派生合约将无法再更改该函数的行为。
注意
具有 private
可见性的函数不能是 virtual
。
注意
没有实现的函数必须在接口之外被标记为 virtual
。在接口中,所有函数都被自动视为 virtual
。
注意
从 Solidity 0.8.8 开始,覆盖接口函数时不需要 override
关键字,除非函数在多个基类中定义。
公共状态变量可以覆盖外部函数,如果函数的参数和返回类型与变量的 getter 函数匹配
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.6.0 <0.9.0;
contract A
{
function f() external view virtual returns(uint) { return 5; }
}
contract B is A
{
uint public override f;
}
注意
虽然公共状态变量可以覆盖外部函数,但它们本身不能被覆盖。
修饰符覆盖
函数修饰符可以互相覆盖。这与函数覆盖 的工作方式相同(除了修饰符没有重载)。被覆盖的修饰符必须使用 virtual
关键字,覆盖修饰符必须使用 override
关键字
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.6.0 <0.9.0;
contract Base
{
modifier foo() virtual {_;}
}
contract Inherited is Base
{
modifier foo() override {_;}
}
在多重继承的情况下,必须明确指定所有直接基类
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.6.0 <0.9.0;
contract Base1
{
modifier foo() virtual {_;}
}
contract Base2
{
modifier foo() virtual {_;}
}
contract Inherited is Base1, Base2
{
modifier foo() override(Base1, Base2) {_;}
}
构造函数
构造函数是一个可选的函数,用 constructor
关键字声明,在合约创建时执行,您可以在其中运行合约初始化代码。
在执行构造函数代码之前,如果您在内联初始化状态变量,则状态变量将初始化为其指定的值,或者如果您没有初始化,则初始化为其默认值。
构造函数运行后,合约的最终代码将部署到区块链。部署代码需要额外的汽油,与代码长度成线性关系。此代码包括所有作为公共接口一部分的函数,以及所有通过函数调用可从那里访问的函数。它不包括构造函数代码或仅从构造函数调用的内部函数。
如果没有构造函数,合约将假设默认构造函数,它等效于 constructor() {}
。例如
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.0 <0.9.0;
abstract contract A {
uint public a;
constructor(uint a_) {
a = a_;
}
}
contract B is A(1) {
constructor() {}
}
您可以在构造函数中使用内部参数(例如存储指针)。在这种情况下,合约必须被标记为抽象,因为这些参数不能从外部分配有效值,而只能通过派生合约的构造函数分配。
警告
在 0.4.22 版本之前,构造函数被定义为与合约同名的函数。这种语法已被弃用,在 0.5.0 版本中不再允许。
警告
在 0.7.0 版本之前,您必须将构造函数的可见性指定为 internal
或 public
。
基类构造函数的参数
所有基类的构造函数将按照下面解释的线性化规则调用。如果基类构造函数有参数,派生合约需要指定所有参数。这可以通过两种方式完成
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.0 <0.9.0;
contract Base {
uint x;
constructor(uint x_) { x = x_; }
}
// Either directly specify in the inheritance list...
contract Derived1 is Base(7) {
constructor() {}
}
// or through a "modifier" of the derived constructor...
contract Derived2 is Base {
constructor(uint y) Base(y * y) {}
}
// or declare abstract...
abstract contract Derived3 is Base {
}
// and have the next concrete derived contract initialize it.
contract DerivedFromDerived is Derived3 {
constructor() Base(10 + 10) {}
}
一种方式是在继承列表中直接指定(is Base(7)
)。另一种方式是作为派生构造函数一部分调用修饰符的方式(Base(y * y)
)。如果构造函数参数是常量,并且定义了合约的行为或描述了合约,则第一种方式更方便。如果基类的构造函数参数依赖于派生合约的构造函数参数,则必须使用第二种方式。参数必须在继承列表中或在派生构造函数中的修饰符样式中给出。在两个地方都指定参数是错误的。
如果派生合约没有指定所有基类构造函数的参数,则必须将其声明为抽象的。在这种情况下,当另一个合约派生自它时,另一个合约的继承列表或构造函数必须为所有尚未指定参数的基类提供必要的参数(否则,另一个合约也必须被声明为抽象的)。例如,在上面的代码片段中,请参阅 Derived3
和 DerivedFromDerived
。
多重继承和线性化
允许多重继承的语言必须处理几个问题。一个是菱形问题。Solidity 与 Python 类似,它使用“C3 线性化”来强制在基类有向无环图 (DAG) 中指定特定顺序。这产生了单调性的理想属性,但禁止某些继承图。尤其是,在 is
指令中给出基类的顺序很重要:您必须按照从“最基类”到“最派生”的顺序列出直接基类。请注意,此顺序与 Python 中使用的顺序相反。
另一种简化解释方法是,当调用在不同合约中多次定义的函数时,会以深度优先的方式从右到左(Python 中从左到右)搜索给定的基类,并在第一次匹配时停止。如果已经搜索过某个基类,则会跳过它。
在以下代码中,Solidity 将给出错误“继承图的线性化不可能”。
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.0 <0.9.0;
contract X {}
contract A is X {}
// This will not compile
contract C is A, X {}
原因是 C
请求 X
覆盖 A
(通过以这种顺序指定 A, X
),但 A
本身请求覆盖 X
,这是一个无法解决的矛盾。
由于您必须显式覆盖从多个基类继承的函数,而没有唯一的覆盖,因此 C3 线性化在实践中并不那么重要。
继承线性化尤其重要且可能不太清晰的一个领域是在继承层次结构中存在多个构造函数时。无论在继承合约的构造函数中以何种顺序提供其参数,构造函数都将始终按线性化顺序执行。例如
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.0 <0.9.0;
contract Base1 {
constructor() {}
}
contract Base2 {
constructor() {}
}
// Constructors are executed in the following order:
// 1 - Base1
// 2 - Base2
// 3 - Derived1
contract Derived1 is Base1, Base2 {
constructor() Base1() Base2() {}
}
// Constructors are executed in the following order:
// 1 - Base2
// 2 - Base1
// 3 - Derived2
contract Derived2 is Base2, Base1 {
constructor() Base2() Base1() {}
}
// Constructors are still executed in the following order:
// 1 - Base2
// 2 - Base1
// 3 - Derived3
contract Derived3 is Base2, Base1 {
constructor() Base1() Base2() {}
}
继承同名不同类型的成员
由于继承,合约可能包含多个共享相同名称的定义的唯一情况是
函数重载。
虚拟函数的覆盖。
通过状态变量获取器覆盖外部虚拟函数。
虚拟修饰符的覆盖。
事件重载。
抽象合约
当合约至少一个函数未实现或未为所有基类合约构造函数提供参数时,必须将合约标记为抽象。即使并非如此,合约仍然可以标记为抽象,例如当您不打算直接创建合约时。抽象合约类似于接口,但接口在声明内容方面更为有限。
抽象合约使用abstract
关键字声明,如以下示例所示。请注意,此合约需要定义为抽象,因为函数utterance()
已声明,但未提供实现(未给出实现体{ }
)。
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.6.0 <0.9.0;
abstract contract Feline {
function utterance() public virtual returns (bytes32);
}
此类抽象合约不能直接实例化。即使抽象合约本身实现了所有定义的函数,这也是事实。以下示例显示了将抽象合约用作基类的用法
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.6.0 <0.9.0;
abstract contract Feline {
function utterance() public pure virtual returns (bytes32);
}
contract Cat is Feline {
function utterance() public pure override returns (bytes32) { return "miaow"; }
}
如果一个合约继承自一个抽象合约,并且没有通过覆盖来实现所有未实现的函数,那么它也需要标记为抽象。
请注意,没有实现的函数不同于函数类型,即使它们的语法看起来非常相似。
没有实现的函数示例(函数声明)
function foo(address) external returns (address);
类型为函数类型的变量的声明示例
function(address) external returns (address) foo;
抽象合约将合约的定义与其实现分离,从而提供更好的可扩展性和自文档化,并促进模板方法等模式,并消除代码重复。抽象合约在定义接口中的方法有用的方式上也是有用的。这是一种让抽象合约的设计者说“我的任何子类都必须实现此方法”的方式。
注意
抽象合约不能用未实现的函数覆盖已实现的虚拟函数。
接口
接口类似于抽象合约,但它们不能有任何实现的函数。有一些进一步的限制
它们不能继承自其他合约,但可以继承自其他接口。
所有声明的函数必须在接口中是外部的,即使它们在合约中是公有的。
它们不能声明构造函数。
它们不能声明状态变量。
它们不能声明修饰符。
将来可能会取消其中一些限制。
接口基本上限于合约 ABI 可以表示的内容,并且 ABI 和接口之间的转换应该可以在没有任何信息丢失的情况下进行。
接口由其自己的关键字表示
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.6.2 <0.9.0;
interface Token {
enum TokenType { Fungible, NonFungible }
struct Coin { string obverse; string reverse; }
function transfer(address recipient, uint amount) external;
}
合约可以像继承其他合约一样继承接口。
在接口中声明的所有函数都是隐式的virtual
,并且任何覆盖它们的函数不需要override
关键字。这并不自动意味着覆盖函数可以再次被覆盖 - 只有当覆盖函数标记为virtual
时才有可能。
接口可以继承自其他接口。这与普通继承具有相同的规则。
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.6.2 <0.9.0;
interface ParentA {
function test() external returns (uint256);
}
interface ParentB {
function test() external returns (uint256);
}
interface SubInterface is ParentA, ParentB {
// Must redefine test in order to assert that the parent
// meanings are compatible.
function test() external override(ParentA, ParentB) returns (uint256);
}
从其他合约可以访问在接口和其他类似合约的结构中定义的类型:Token.TokenType
或Token.Coin
。
警告
自Solidity 版本 0.5.0起,接口支持enum
类型,请确保 pragma 版本将此版本指定为最小版本。
库
库类似于合约,但它们的目的是它们只部署到特定地址一次,并且它们的代码使用 EVM 的DELEGATECALL
(在 Homestead 之前为CALLCODE
)功能来重复使用。这意味着如果调用库函数,则它们的代码将在调用合约的上下文中执行,即this
指向调用合约,尤其可以访问调用合约的存储。由于库是隔离的源代码,它只能在显式提供的情况下访问调用合约的状态变量(否则它将无法命名它们)。库函数只能在不修改状态的情况下直接调用(即,不使用DELEGATECALL
),因为库被认为是无状态的。特别是,无法销毁库。
注意
在 0.4.20 版之前,可以通过规避 Solidity 的类型系统来销毁库。从该版本开始,库包含一个机制,该机制不允许直接调用状态修改函数(即,不使用DELEGATECALL
)。
库可以被视为使用它们的合约的隐式基类。它们不会在继承层次结构中显式可见,但对库函数的调用看起来就像对显式基类函数的调用一样(使用限定访问,例如L.f()
)。当然,对内部函数的调用使用内部调用约定,这意味着可以传递所有内部类型,并且存储在内存中的类型将通过引用传递而不是复制。为了在 EVM 中实现这一点,在编译时,从合约调用且所有从其中调用的函数的内部库函数的代码将包含在调用合约中,并且将使用常规的JUMP
调用而不是DELEGATECALL
。
注意
当涉及到公共函数时,继承类比会失效。使用L.f()
调用公共库函数会导致外部调用(准确地说,是DELEGATECALL
)。相反,当A
是当前合约的基类时,A.f()
是内部调用。
以下示例说明了如何使用库(但使用手动方法,请务必查看使用 for以获取更高级的示例来实现集合)。
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.6.0 <0.9.0;
// We define a new struct datatype that will be used to
// hold its data in the calling contract.
struct Data {
mapping(uint => bool) flags;
}
library Set {
// Note that the first parameter is of type "storage
// reference" and thus only its storage address and not
// its contents is passed as part of the call. This is a
// special feature of library functions. It is idiomatic
// to call the first parameter `self`, if the function can
// be seen as a method of that object.
function insert(Data storage self, uint value)
public
returns (bool)
{
if (self.flags[value])
return false; // already there
self.flags[value] = true;
return true;
}
function remove(Data storage self, uint value)
public
returns (bool)
{
if (!self.flags[value])
return false; // not there
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 {
// The library functions can be called without a
// specific instance of the library, since the
// "instance" will be the current contract.
require(Set.insert(knownValues, value));
}
// In this contract, we can also directly access knownValues.flags, if we want.
}
当然,您不必遵循这种方式使用库:它们也可以在不定义结构数据类型的情况下使用。函数也可以在没有任何存储引用参数的情况下工作,并且它们可以具有多个存储引用参数,并且可以在任何位置。
对Set.contains
、Set.insert
和Set.remove
的调用都被编译为对外部合约/库的调用(DELEGATECALL
)。如果您使用库,请注意会执行实际的外部函数调用。msg.sender
、msg.value
和this
将在本次调用中保留其值(在 Homestead 之前,由于使用了CALLCODE
,msg.sender
和msg.value
发生了变化,尽管如此)。
以下示例展示了如何在库中使用存储在内存中的类型和内部函数,以在没有外部函数调用开销的情况下实现自定义类型
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.0;
struct bigint {
uint[] limbs;
}
library BigInt {
function fromUint(uint x) internal pure returns (bigint memory r) {
r.limbs = new uint[](1);
r.limbs[0] = x;
}
function add(bigint memory a, bigint memory b) internal pure returns (bigint memory 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 limbA = limb(a, i);
uint limbB = limb(b, i);
unchecked {
r.limbs[i] = limbA + limbB + carry;
if (limbA + limbB < limbA || (limbA + limbB == type(uint).max && carry > 0))
carry = 1;
else
carry = 0;
}
}
if (carry > 0) {
// too bad, we have to add a limb
uint[] memory newLimbs = new uint[](r.limbs.length + 1);
uint i;
for (i = 0; i < r.limbs.length; ++i)
newLimbs[i] = r.limbs[i];
newLimbs[i] = carry;
r.limbs = newLimbs;
}
}
function limb(bigint memory a, uint index) internal pure returns (uint) {
return index < a.limbs.length ? a.limbs[index] : 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(type(uint).max);
bigint memory z = x.add(y);
assert(z.limb(1) > 0);
}
}
可以通过将库类型转换为address
类型来获取库的地址,即使用address(LibraryName)
。
由于编译器不知道库将被部署到的地址,因此编译后的十六进制代码将包含形式为__$30bbc0abd4d6364515865950d3e0d10953$__
的占位符。占位符是完全限定库名称的 keccak256 哈希的十六进制编码的 34 个字符前缀,例如,如果库存储在一个名为bigint.sol
的文件中,则为libraries/bigint.sol:BigInt
libraries/
目录。这种字节码是不完整的,不应该部署。占位符需要用实际地址替换。您可以通过在编译库时将其传递给编译器或使用链接器来更新已编译的二进制文件来执行此操作。有关如何使用命令行编译器进行链接的信息,请参阅库链接。
与合约相比,库在以下方面受到限制
它们不能具有状态变量
它们不能继承或被继承
它们不能接收以太坊
它们不能被销毁
(这些可能会在以后的某个时间点取消。)
库中的函数签名和选择器
虽然可以对公共或外部库函数进行外部调用,但此类调用的调用约定被认为是 Solidity 内部约定,与为常规合约 ABI指定的约定不同。外部库函数支持比外部合约函数更多的参数类型,例如递归结构和存储指针。因此,用于计算 4 字节选择器的函数签名是根据内部命名模式计算的,并且合约 ABI 中不支持的类型参数使用内部编码。
以下标识符用于签名中的类型
值类型、非存储
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 哈希的前四个字节组成。它的值可以使用 Solidity 中的 .selector
成员获取,如下所示
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.5.14 <0.9.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 个字节的零。当部署代码运行时,此常量在内存中被当前地址替换,并且此修改后的代码被存储在合约中。在运行时,这会导致部署时的地址成为第一个被推送到堆栈上的常量,并且分发代码将当前地址与任何非 view 和非 pure 函数的此常量进行比较。
这意味着存储在链上的库的实际代码与编译器报告的 deployedBytecode
代码不同。
使用 for
指令 using A for B
可用于将函数 (A
) 作为运算符附加到用户定义的值类型,或作为成员函数附加到任何类型 (B
)。成员函数将其被调用的对象作为第一个参数接收(类似于 Python 中的 self
变量)。运算符函数接收操作数作为参数。
它在文件级别或合约内(在合约级别)都是有效的。
第一部分 A
可以是以下之一
函数列表,可以选择分配运算符名称(例如
using {f, g as +, h, L.t} for uint
)。如果未指定运算符,则该函数可以是库函数或自由函数,并作为成员函数附加到类型。否则,它必须是自由函数,并且它成为该类型上该运算符的定义。库的名称(例如
using L for uint
) - 库的所有非私有函数都作为成员函数附加到类型。
在文件级别,第二部分 B
必须是显式类型(没有数据位置说明符)。在合约内,您也可以使用 *
来代替类型(例如 using L for *;
),这将使库 L
的所有函数附加到所有类型。
如果您指定了一个库,则库中的所有非私有函数都将被附加,即使那些第一个参数类型与对象类型不匹配的函数也是如此。类型在调用函数时进行检查,并执行函数重载解析。
如果您使用函数列表(例如 using {f, g, h, L.t} for uint
),则类型 (uint
) 必须隐式可转换为这些函数中的每一个的第一个参数。即使没有调用这些函数中的任何一个,也会执行此检查。请注意,私有库函数只能在库内使用 using for
时指定。
如果您定义了一个运算符(例如 using {f as +} for T
),则类型 (T
) 必须是 用户定义的值类型,并且定义必须是 pure
函数。运算符定义必须是全局的。以下运算符可以用这种方式定义
类别 |
运算符 |
可能的签名 |
---|---|---|
按位 |
|
|
|
|
|
|
|
|
|
|
|
算术 |
|
|
|
|
|
|
||
|
|
|
|
|
|
|
|
|
比较 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
请注意,一元和二元 -
需要单独定义。编译器将根据运算符的调用方式选择正确的定义。
指令 using A for B;
仅在当前作用域(合约或当前模块/源单元)内有效,包括其所有函数内,并且在使用它的合约或模块外部没有作用。
当指令在文件级别使用并应用于在同一文件中文件级别定义的用户定义类型时,可以在末尾添加单词 global
。这将使函数和运算符在类型可用的任何地方(包括其他文件)都附加到该类型,而不仅仅是在使用语句的作用域内。
让我们以这种方式重写 库 部分中的集合示例,使用文件级函数而不是库函数。
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.13;
struct Data { mapping(uint => bool) flags; }
// Now we attach functions to the type.
// The attached functions can be used throughout the rest of the module.
// If you import the module, you have to
// repeat the using directive there, for example as
// import "flags.sol" as Flags;
// using {Flags.insert, Flags.remove, Flags.contains}
// for Flags.Data;
using {insert, remove, contains} for Data;
function insert(Data storage self, uint value)
returns (bool)
{
if (self.flags[value])
return false; // already there
self.flags[value] = true;
return true;
}
function remove(Data storage self, uint value)
returns (bool)
{
if (!self.flags[value])
return false; // not there
self.flags[value] = false;
return true;
}
function contains(Data storage self, uint value)
view
returns (bool)
{
return self.flags[value];
}
contract C {
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)`
require(knownValues.insert(value));
}
}
也可以用这种方式扩展内置类型。在这个例子中,我们将使用一个库。
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.13;
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 type(uint).max;
}
}
using Search for uint[];
contract C {
uint[] data;
function append(uint value) public {
data.push(value);
}
function replace(uint from, uint to) public {
// This performs the library function call
uint index = data.indexOf(from);
if (index == type(uint).max)
data.push(to);
else
data[index] = to;
}
}
请注意,所有外部库调用都是实际的 EVM 函数调用。这意味着,如果您传递内存或值类型,即使是 self
变量,也会执行复制。唯一不会执行复制的情况是使用存储引用变量或调用内部库函数时。
另一个示例展示了如何为用户定义类型定义自定义运算符
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.19;
type UFixed16x2 is uint16;
using {
add as +,
div as /
} for UFixed16x2 global;
uint32 constant SCALE = 100;
function add(UFixed16x2 a, UFixed16x2 b) pure returns (UFixed16x2) {
return UFixed16x2.wrap(UFixed16x2.unwrap(a) + UFixed16x2.unwrap(b));
}
function div(UFixed16x2 a, UFixed16x2 b) pure returns (UFixed16x2) {
uint32 a32 = UFixed16x2.unwrap(a);
uint32 b32 = UFixed16x2.unwrap(b);
uint32 result32 = a32 * SCALE / b32;
require(result32 <= type(uint16).max, "Divide overflow");
return UFixed16x2.wrap(uint16(a32 * SCALE / b32));
}
contract Math {
function avg(UFixed16x2 a, UFixed16x2 b) public pure returns (UFixed16x2) {
return (a + b) / UFixed16x2.wrap(200);
}
}