本节重点介绍了 Solidity 版本 0.5.0 中引入的主要重大变更,以及这些变更背后的原因和如何变更日志受影响的代码。 完整列表请查看 变更日志。
Note
使用 Solidity v0.5.0 编译的合约仍然可以与使用旧版本编译的合约甚至库进行交互, 而无需重新编译或重新部署它们。 只需更改接口以包含数据位置和可见性及可变性说明符即可。 请参见下面的 :ref:`与旧合约的互操作性 <interoperability>` 部分。
本节列出了仅涉及语义的变更,因此可能会隐藏现有代码中的新行为和不同的行为。
- 有符号右移现在使用正确的算术右移,即向负无穷舍入,而不是向零舍入。有符号和无符号移位将在君士坦丁堡中有专用的操作码,目前由 Solidity 模拟。
do...while
循环中的continue
语句现在跳转到条件,这是这种情况下的常见行为。它以前是跳转到循环体。因此,如果条件为假,循环将终止。- 函数
.call()
,.delegatecall()
和.staticcall()
在给定单个bytes
参数时不再进行填充。 - 纯函数和视图函数现在在 EVM 版本为拜占庭或更高时使用操作码
STATICCALL
调用。这禁止在 EVM 级别进行状态更改。 - ABI 编码器现在在外部函数调用和
abi.encode
中正确填充来自 calldata (msg.data
和外部函数参数) 的字节数组和字符串。对于未填充的编码,请使用abi.encodePacked
。 - 如果传递的 calldata 太短或超出边界,ABI 解码器将在函数开始和
abi.decode()
中回退。请注意,脏的高位仍然会被简单忽略。 - 从 Tangerine Whistle 开始,所有可用的 gas 都会在外部函数调用中转发。
本节重点介绍影响语法和语义的变更。
- 函数
.call()
,.delegatecall()
,staticcall()
,``keccak256()``,sha256()
和ripemd160()
现在只接受单个bytes
参数。 此外,参数不再填充。此更改旨在更明确和清晰地说明参数是如何连接的。 将每个.call()
(及其家族)更改为.call("")
,将每个.call(signature, a, b, c)
更改为使用.call(abi.encodeWithSignature(signature, a, b, c))
(最后一个仅适用于值类型)。 将每个keccak256(a, b, c)
更改为keccak256(abi.encodePacked(a, b, c))
。 尽管这不是重大变更,但建议开发者将x.call(bytes4(keccak256("f(uint256)")), a, b)
更改为x.call(abi.encodeWithSignature("f(uint256)", a, b))
。 - 函数
.call()
,.delegatecall()
和.staticcall()
现在返回(bool, bytes memory)
以提供对返回数据的访问。 将bool success = otherContract.call("f")
更改为(bool success, bytes memory data) = otherContract.call("f")
。 - Solidity 现在实现了 C99 风格的作用域规则,对于函数局部变量,即变量只能在声明后使用,并且只能在同一作用域或嵌套作用域中使用。
在
for
循环的初始化块中声明的变量在循环内部的任何位置都是有效的。
本节列出了代码现在需要更明确的变更。对于大多数主题,编译器将提供建议。
- 显式函数可见性现在是强制性的。为每个函数和构造函数添加
public
,并为每个未指定可见性的回退或接口函数添加external
。 - 所有结构、数组或映射类型变量的显式数据位置现在是强制性的。这也适用于函数参数和返回变量。
例如,将
uint[] x = z
更改为uint[] storage x = z
,将function f(uint[][] x)
更改为function f(uint[][] memory x)
, 其中memory
是数据位置,可能会相应地替换为storage
或calldata
。 请注意,external
函数要求参数的数据位置为calldata
。 - 合约类型不再包含
address
成员,以便分离命名空间。因此,现在必须在使用address
成员之前显式将合约类型的值转换为地址。 示例:如果c
是一个合约,将c.transfer(...)
更改为address(c).transfer(...)
,将c.balance
更改为address(c).balance
。 - 现在不允许在不相关的合约类型之间进行显式转换。你只能从合约类型转换为其基类或祖先类型。
如果你确定一个合约与你想要转换的合约类型兼容,尽管它不继承自它,你可以通过先转换为
address
来解决此问题。 示例:如果A
和B
是合约类型,B
不继承自A
,而b
是类型为B
的合约,你仍然可以使用A(address(b))
将b
转换为类型A
。 请注意,你仍然需要注意匹配可支付的回退函数,如下所述。 address
类型被拆分为address
和address payable
,其中只有address payable
提供transfer
函数。一个address payable
可以直接转换为address
,但反向转换是不允许的。 通过uint160
转换address
为address payable
是可能的。 如果c
是一个合约,address(c)
仅在c
具有可支付的回退函数时才会产生address payable
。 如果你使用 :ref:`提取模式<withdrawal_pattern>`,你很可能不需要更改代码,因为transfer
仅在msg.sender
上使用,而不是存储的地址,并且msg.sender
是一个address payable
。- 由于
bytesX
在右侧填充和uintY
在左侧填充可能导致意外的转换结果,因此不同大小的bytesX
和uintY
之间的转换现在不被允许。 现在必须在转换之前在类型内调整大小。例如,你可以将bytes4
(4 字节)转换为uint64
(8 字节),方法是先将bytes4
变量转换为bytes8
,然后再转换为uint64
。 通过uint32
转换时会得到相反的填充。在 v0.5.0 之前,任何bytesX
和uintY
之间的转换都会通过uint8X
进行。例如uint8(bytes3(0x291807))
将被转换为uint8(uint24(bytes3(0x291807)))
结果是0x07
)。 - 在不可支付的函数中使用
msg.value
(或通过修改器引入它)是不允许的,作为安全功能。 将函数转换为payable
或为程序逻辑创建一个新的内部函数,该函数使用msg.value
。 - 出于清晰原因,命令行界面现在要求在使用标准输入作为源时加上
-
。
本节列出了使先前功能或语法过时的更改。请注意,许多这些更改在实验模式 v0.5.0
中已经启用。
- 命令行选项
--formal
(用于生成进一步形式验证的 Why3 输出)已被弃用并且现在已被移除。一个新的形式验证模块 SMTChecker 通过pragma experimental SMTChecker;
启用。 - 命令行选项
--julia
因中间语言Julia
重命名为Yul
而被重命名为--yul
。 --clone-bin
和--combined-json clone-bin
命令行选项已被移除。- 不允许使用空前缀的重映射。
- JSON AST 字段
constant
和payable
已被移除。该信息现在在stateMutability
字段中。 - JSON AST 字段
isConstructor
的FunctionDefinition
节点已被名为kind
的字段替代,该字段可以具有值"constructor"
,"fallback"
或"function"
。 - 在未链接的二进制十六进制文件中,库地址占位符现在是完全限定库名称的 keccak256 哈希的前 36 个十六进制字符,周围用
$...$
包围。之前,仅使用完全限定的库名称。这减少了碰撞的可能性,特别是在使用长路径时。二进制文件现在还包含从这些占位符到完全限定名称的映射列表。
- 现在必须使用
constructor
关键字定义构造函数。 - 不再允许在没有括号的情况下调用基构造函数。
- 在同一继承层次结构中多次指定基构造函数参数现在是不允许的。
- 现在不允许以错误的参数数量调用带参数的构造函数。如果你只想指定继承关系而不提供参数,请完全不提供括号。
- 函数
callcode
现在不被允许(支持delegatecall
)。仍然可以通过内联汇编使用它。 suicide
现在不被允许(支持selfdestruct
)。sha3
现在不被允许(支持keccak256
)。throw
现在不被允许(支持revert
、require
和assert
)。
- 从十进制字面量到
bytesXX
类型的显式和隐式转换现在不被允许。 - 从十六进制字面量到不同大小的
bytesXX
类型的显式和隐式转换现在不被允许。
- 由于对闰年的复杂性和混淆,单位名称
years
现在不被允许。 - 不再允许后面没有数字的尾随点。
- 现在不允许将十六进制数字与单位名称结合(例如
0x1e wei
)。 - 十六进制数字的前缀
0X
不被允许,仅允许0x
。
- 现在不允许声明空结构以提高清晰度。
- 现在不允许使用
var
关键字以支持显式性。 - 不同组件数量的元组之间的赋值现在不被允许。
- 非编译时常量的常量值不被允许。
- 值数量不匹配的多变量声明现在不被允许。
- 未初始化的存储变量现在不被允许。
- 空元组组件现在不被允许。
- 在变量和结构中检测循环依赖的递归限制为 256。
- 长度为零的固定大小数组现在不被允许。
- 现在不允许将
constant
用作函数状态可变性修改器。 - 布尔表达式不能使用算术运算。
- 一元
+
运算符现在不被允许。 - 字面量不能再与
abi.encodePacked
一起使用,而不先转换为显式类型。 - 对于一个或多个返回值的函数,空返回语句现在不被允许。
- “松散汇编”语法现在完全不被允许,即不再允许使用跳转标签、跳转和非功能指令。请改用新的
while
、switch
和if
构造。 - 没有实现的函数不能再使用修改器。
- 带有命名返回值的函数类型现在不被允许。
- 在 if/while/for 体内的单语句变量声明(不是块)现在不被允许。
- 新关键字:
calldata
和constructor
。 - 新保留关键字:
alias
、apply
、auto
、copyof
、define
、immutable
、implements
、macro
、mutable
、override
、partial
、promise
、reference
、sealed
、sizeof
、supports
、typedef
和unchecked
。
仍然可以通过为它们定义接口与编写的 Solidity 版本低于 v0.5.0 的合约进行接口交互(或反之亦然)。假设你已经部署了以下 0.5.0 之前的版本的合约:
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.4.25;
// 这将在编译器版本 0.4.25 之前报告警告
// 这在 0.5.0 之后将无法编译
contract OldContract {
function someOldFunction(uint8 a) {
//...
}
function anotherOldFunction() constant returns (bool) {
//...
}
// ...
}
这在 Solidity v0.5.0 中将不再编译。但是,你可以为其定义一个兼容的接口:
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.5.0 <0.9.0;
interface OldContract {
function someOldFunction(uint8 a) external;
function anotherOldFunction() external returns (bool);
}
请注意,我们没有将 anotherOldFunction
声明为 view
,尽管它在原始合约中被声明为 constant
。
这是因为从 Solidity v0.5.0 开始,使用 staticcall
来调用 view` 函数。
在 v0.5.0 之前,``constant
关键字并未强制执行,因此使用 staticcall
调用声明为 constant
的函数仍可能回退,因为 constant
函数仍可能尝试修改存储。
因此,在为旧合约定义接口时,你应该仅在绝对确定该函数可以与 staticcall
一起使用的情况下,使用 view
替代 constant
。
给定上述定义的接口,你现在可以轻松使用已经部署的 0.5.0 版本之前的合约:
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.5.0 <0.9.0;
interface OldContract {
function someOldFunction(uint8 a) external;
function anotherOldFunction() external returns (bool);
}
contract NewContract {
function doSomething(OldContract a) public returns (bool) {
a.someOldFunction(0x42);
return a.anotherOldFunction();
}
}
同样,可以通过定义库的函数而不实现,并在链接时提供 0.5.0 之前版本的库地址来使用库(请参见 :ref:`commandline-compiler` 以了解如何使用命令行编译器进行链接):
// 这将在 0.6.0 之后无法编译
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.5.0;
library OldLibrary {
function someFunction(uint8 a) public returns(bool);
}
contract NewContract {
function f(uint8 a) public returns (bool) {
return OldLibrary.someFunction(a);
}
}
以下示例展示了一个合约及其针对 Solidity v0.5.0 的变更日志版本,包含本节中列出的一些更改。
旧版本:
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.4.25;
// 这将在 0.5.0 之后无法编译
contract OtherContract {
uint x;
function f(uint y) external {
x = y;
}
function() payable external {}
}
contract Old {
OtherContract other;
uint myNumber;
// 函数可变性未提供,不是错误。
function someInteger() internal returns (uint) { return 2; }
// 函数可见性未提供,不是错误。
// 函数可变性未提供,不是错误。
function f(uint x) returns (bytes) {
// 在这个版本中,变量是可以的。
var z = someInteger();
x += z;
// 抛出在这个版本中是可以的。
if (x > 100)
throw;
bytes memory b = new bytes(x);
y = -3 >> 1;
// y == -1(错误,应该是 -2)
do {
x += 1;
if (x > 10) continue;
// 'Continue' 会导致无限循环。
} while (x < 11);
// 调用只返回一个布尔值。
bool success = address(other).call("f");
if (!success)
revert();
else {
// 局部变量可以在使用后声明。
int y;
}
return b;
}
// 对于 'arr' 不需要显式数据位置
function g(uint[] arr, bytes8 x, OtherContract otherContract) public {
otherContract.transfer(1 ether);
// 由于 uint32(4 字节)小于 bytes8(8 字节), x 的前 4 字节将丢失。
// 这可能导致意外行为,因为 bytesX 是右填充的。
uint32 y = uint32(x);
myNumber += y + msg.value;
}
}
新版本:
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.5.0;
// 这将在 0.6.0 之后无法编译
contract OtherContract {
uint x;
function f(uint y) external {
x = y;
}
function() payable external {}
}
contract New {
OtherContract other;
uint myNumber;
// 必须指定函数可变性。
function someInteger() internal pure returns (uint) { return 2; }
// 必须指定函数可见性。
// 必须指定函数可变性。
function f(uint x) public returns (bytes memory) {
// 现在必须显式给出类型。
uint z = someInteger();
x += z;
// 抛出现在是不允许的。
require(x <= 100);
int y = -3 >> 1;
require(y == -2);
do {
x += 1;
if (x > 10) continue;
// 'Continue' 跳转到下面的条件。
} while (x < 11);
// 调用返回 (bool, bytes)。
// 必须指定数据位置。
(bool success, bytes memory data) = address(other).call("f");
if (!success)
revert();
return data;
}
using AddressMakePayable for address;
// 'arr' 的数据位置必须指定
function g(uint[] memory /* arr */, bytes8 x, OtherContract otherContract, address unknownContract) public payable {
// 'otherContract.transfer' 未提供。
// 由于 'OtherContract' 的代码是已知的并且有回退
// 函数,address(otherContract) 的类型是 'address payable'。
address(otherContract).transfer(1 ether);
// 'unknownContract.transfer' 未提供。
// 'address(unknownContract).transfer' 未提供
// 因为 'address(unknownContract)' 不是 'address payable'。
// 如果函数接受一个接收资金的 'address',你可以通过 'uint160' 转换为 'address payable'。
// 注意:这不推荐,应该尽可能使用显式类型 'address payable'。
// 为了增加清晰度,我们建议使用库来进行转换(在本示例合约后提供)。
address payable addr = unknownContract.makePayable();
require(addr.send(1 ether));
// 由于 uint32(4 字节)小于 bytes8(8 字节),不允许转换。
// 我们需要先转换为相同的大小:
bytes4 x4 = bytes4(x); // 填充发生在右侧
uint32 y = uint32(x4); // 转换是一致的
// 'msg.value' 不能在 'non-payable' 函数中使用。
// 我们需要使函数可支付
myNumber += y + msg.value;
}
}
// 我们可以定义一个库来显式地将 ``address`` 转换为 ``address payable`` 作为解决方法。
library AddressMakePayable {
function makePayable(address x) internal pure returns (address payable) {
return address(uint160(x));
}
}