
表达式与控制结构
控制结构
Solidity 提供了大多数常见的控制结构,这些结构在使用大括号的语言中十分常见,包括:if、else、while、do、for、break、continue 和 return,其语义与 C 或 JavaScript 中基本一致。
Solidity 还支持 try/catch 异常处理语句,但仅适用于外部函数调用和合约创建调用。此外,可以使用 revert 语句来主动抛出错误。
条件语句中的括号是必须的,但单条语句体可以省略大括号。
注意:Solidity 不支持从非布尔类型隐式转换为布尔类型,因此诸如 if (1) { … } 这样的写法在 Solidity 中是无效的。
函数调用
内部函数调用
当前合约中的函数可以直接(即“内部”)调用,也可以实现递归调用,例如下方这个无意义但有效的例子:
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.22 <0.9.0;
// 这将触发一个警告
contract C {
function g(uint a) public pure returns (uint ret) { return a + f(); }
function f() internal pure returns (uint ret) { return g(7) + f(); }
}
这些函数调用在 EVM 中会被编译为简单的跳转(jump),意味着内存不会被清空,因此向内部函数传递内存引用是非常高效的。
注意:仅同一个合约实例中的函数才能通过内部调用方式访问。应避免深度递归调用,因为每次内部调用至少占用一个栈槽,而 EVM 中栈槽总数仅为 1024 个。
外部函数调用
函数也可以通过 this.g(8); 或 c.g(2); 的方式调用,其中 c 是某个合约实例,g 是该合约中的函数。这类调用属于外部调用,是通过**消息调用(message call)**实现的,而非简单跳转。
注意:在构造函数中不能调用 this 的函数,因为此时合约尚未部署完成。
对其他合约的函数调用必须使用外部调用方式,并且所有参数都需要复制到内存中。
注意:从一个合约调用另一个合约的函数不会生成新的交易,该调用作为原始交易的一部分通过消息传递机制进行。
调用外部合约时,可以使用 {value: 10, gas: 10000} 这样的选项来显式指定调用时发送的 Wei 金额或 Gas 数量。不过不推荐手动指定 Gas 数值,因为未来操作码的 Gas 成本可能发生变化。任何通过 value 发送的 Wei 都会增加接收合约的总余额。
示例如下:
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.6.2 <0.9.0;
contract InfoFeed {
function info() public payable returns (uint ret) { return 42; }
}
contract Consumer {
InfoFeed feed;
function setFeed(InfoFeed addr) public { feed = addr; }
function callFeed() public { feed.info{value: 10, gas: 800}(); }
}
我们必须在 info 函数上添加 payable 修饰符,否则不能通过 value 向其转账。
注意:表达式 feed.info{value: 10, gas: 800} 只是设置调用参数,并不会执行函数调用。必须在末尾添加 () 才会触发实际调用,即 feed.info{value: 10, gas: 800}() 才是完整的函数调用表达式。
由于 EVM 对不存在的合约地址调用默认视为成功,Solidity 会使用 extcodesize 操作码检查目标地址是否有代码(即是否为有效合约)。若地址无代码,将抛出异常。但若调用后还需解码返回值,该检查将被跳过,因为 ABI 解码器能捕获无代码地址的异常情况。
警告:
与其他合约的交互始终存在潜在风险,特别是当被调用合约的源码未知时。当前合约在执行过程中会将控制权转交给被调用合约,而后者可能会执行任何逻辑。即使被调用合约继承自已知合约,只要接口一致,其具体实现也可以截然不同,因此仍存在风险。
此外,还需防范被调用合约在第一次返回前就回调我们系统中的其他合约,甚至是当前合约本身。这类“重入攻击”可能被用来修改调用合约的状态变量。
因此,强烈建议:将对外部合约的调用放在状态变量更改之后,以防止重入攻击。
使用具名参数的函数调用
函数调用时可以使用具名参数(Named Parameters),即将参数包裹在 {} 中,并通过名称传入。此时参数顺序可以任意,但名称必须与函数声明中的一致:
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.0 <0.9.0;
contract C {
mapping(uint => uint) data;
function f() public {
set({value: 2, key: 3});
}
function set(uint key, uint value) public {
data[key] = value;
}
}
函数定义中省略的名称
函数声明中的参数名和返回值名可以省略。被省略的参数仍存在于栈中,但无法通过名称访问;而被省略名称的返回值仍然可以通过 return 语句返回给调用者:
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.22 <0.9.0;
contract C {
// 参数的名称被省略
function func(uint k, uint) public pure returns(uint) {
return k;
}
}
通过 new 创建合约
合约可以使用 new 关键字来部署其他合约。被创建合约的完整代码必须在编译时已知,因此不支持创建依赖尚未定义的合约(即递归依赖):
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.0 <0.9.0;
contract D {
uint public x;
constructor(uint a) payable {
x = a;
}
}
contract C {
D d = new D(4); // 这将在 C 的构造函数中执行
function createD(uint arg) public {
D newD = new D(arg);
newD.x();
}
function createAndEndowD(uint arg, uint amount) public payable {
// 创建时发送以太币
D newD = new D{value: amount}(arg);
newD.x();
}
}
如上所示,可以通过 value 选项在创建 D 实例时发送以太币。然而,不能显式指定 gas 使用量。
如果合约创建失败(例如因栈溢出、余额不足或其他问题),将抛出异常。
带有 Salt 的合约创建 / create2
通常情况下,合约地址是根据部署者地址和一个递增的计数器(nonce)计算出来的。但如果使用 salt(一个 bytes32 类型的值),则会使用 CREATE2 指令生成合约地址,其地址计算方式如下:
合约地址由以下字段确定:
-
创建者地址(address(this))
-
给定的 salt 值
-
被创建合约的创建字节码(creationCode)和构造函数参数的编码值
这种方式不依赖 nonce,因此允许在链下预计算新合约的部署地址。即使创建者合约中间又部署了其他合约,也不会影响预计算的地址。
这一机制常用于只有在链上出现争议时才需要部署的仲裁型合约:
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.0 <0.9.0;
contract D {
uint public x;
constructor(uint a) {
x = a;
}
}
contract C {
function createDSalted(bytes32 salt, uint arg) public {
// 这个复杂的表达式仅用于说明地址是如何被预计算出来的。
// 实际上我们只需要写 ``new D{salt: salt}(arg)``。
address predictedAddress = address(uint160(uint(keccak256(abi.encodePacked(
bytes1(0xff),
address(this),
salt,
keccak256(abi.encodePacked(
type(D).creationCode,
abi.encode(arg)
))
)))));
D d = new D{salt: salt}(arg);
require(address(d) == predictedAddress);
}
}
注意:通过 CREATE2 创建的合约,即使在销毁后,也可以使用相同地址重新创建。但需谨慎:
-
地址是否复用,取决于新合约的创建字节码与原合约是否完全一致。
-
构造函数若依赖外部状态,不同部署时可能生成不同的运行时代码(runtime bytecode),导致行为不一致。
表达式的求值顺序
Solidity 中表达式的求值顺序是未定义的(更准确地说,表达式树中某个节点的子表达式的求值顺序不确定,但会在父节点求值前完成)。
唯一被明确保证的是:
-
语句按顺序执行
-
布尔表达式采用短路求值(即 && 和 || 表达式遵循从左到右,必要时跳过右侧分支的执行)
1万+

被折叠的 条评论
为什么被折叠?



