solidity 103
约 7265 字大约 24 分钟
2022-05-10
constant
constant
concept
在前面已经学了很多变量的定义方式,例如 storage
和 memory
可以定义变量的存储位置,public
和 private
可以定义变量的可见性。
而constant
是一种用于定义常量的关键字。常量是在程序执行期间不会发生变化的值。它们在声明后被固定,并且无法在运行时被修改。
真实用例
例如OpenZepplin中的AccessControl
合约,将变量DEFAULT_ADMIN_ROLE
设为0x00
。 AccessControl
是一个权限控制合约,合约中用bytes32
来表示不同用户的角色。合约默认管理者的角色是0。
bytes32 public constant DEFAULT_ADMIN_ROLE = 0x00;
documentation
要定义 constant
变量,需要在变量类型和变量名中间使用关键字constant
。constant
变量通常使用大写字母表示:
//将1赋值给了常量NUM。
uint256 constant NUM = 1;
constant
只能用于状态变量的定义。因为constant
修饰的变量将会硬编码到字节码中,而字节码是在合约部署时就生成的值,所以不可能在函数运行时再改变字节码。
FAQ
为什么使用 constant ?
constant
申明的变量不会被写入合约的 storage
中,而是直接在编译时被硬编码到字节码中。
这样做的好处是,由于这些常量的值是在字节码中的,所以我们不需要读取存储空间来获取值,从而节省了 gas 消耗。
immutable
concept
另一种不可变的变量的定义方式 immutable
:immutable
与常量( constant
)不同,immutable
变量的值可以在部署合约时确定,但在部署后无法更改。
换句话说 immutable
修饰的变量可以在构造函数中对其赋值,且赋值后不可更改。而 constant
在声明时就必须被赋值。
documentation
在定义 immutable
变量时,需要在变量类型和变量名中间使用关键字 immutable
。
pragma solidity ^0.8.0;
contract Example {
//在这里定义了一个immutable变量,其值需要在构造函数中定义。
address immutable public token;
constructor(address _tokenAddress) {
token = _tokenAddress;
}
}
和 constant
一样,immutable
只能用于状态变量的定义。这是因为 immutable
修饰的变量也会硬编码到合约的字节码中。
只可以在构造函数中对 immutable
的变量赋值。
FAQ
为什么使用 Immutable ?
其相较于 constant
可以在部署时再为变量赋值,而不是在写合约时,这使得 immutable
更加灵活。
immutable
变量在部署时将其硬编码到合约字节码中。这使得访问 immutable
变量的成本较低,因为它们在部署后不需要从 storage 中读取。
flow control
continue
concept
在之前了解了if
、for
、while
和do while
。在这里,学习一下continue 语句。
continue
语句是一种用于控制循环行为的语句。当程序执行到 continue
语句时,会跳过当前循环中剩余的代码,并进入下一次循环的迭代。
真实用例
实现了一个函数,可以把数组中所有的偶数提取出来到新的数组。
function processEvenNumbers(uint[] memory numbers) public pure returns (uint[] memory) {
uint[] memory evenNumbers = new uint[](numbers.length);
uint evenCount = 0;
for (uint i = 0; i < numbers.length; i++) {
if (numbers[i] % 2 != 0) {
continue; //跳过奇数
}
evenNumbers[evenCount] = numbers[i];
evenCount++;
}
assembly { mstore(evenNumbers, evenCount) }
return evenNumbers;
}
documentation
用 continue
可以跳过当前迭代的剩余代码,并立即开始下一次迭代的条件检查。
for (int a = 0; a < 10; a++) {
if (a == 5) {
continue;
}
//如果 a 不等于 5,执行其他操作
}
FAQ
continue
一般在什么时候用?
continue 普遍用于循环语句中,用于跳过该次执行,从而继续执行循环的下一步。
break
concept
break
允许在满足特定条件时完全终止循环,然后继续执行循环后面的代码。
真实用例
在 OpenZepplin 的 Strings
合约中,使用了break
函数来退出while
循环
function toString(uint256 value) internal pure returns (string memory) {
...
while (true) {
ptr--;
...
value /= 10;
if (value == 0) break; // 完全终止循环
}
}
documentation
如果要跳出该循环的执行体,继续执行循环后面的代码。只需使用break
关键字。
for (int a = 0; a < 10; a++) {
//如果a等于5,跳出循环
if (a == 5) { break; }
//如果a不等于5,执行其他操作
}
FAQ
什么时候使用 break ?
在循环中,想要退出该循环时,可以使用 break
语句,一般配合 if
语句一起使用。
pay
payable
concept
函数另一个关键字:payable
,它表示该函数可以接收以太币( ETH )。
通常情况下,智能合约是不会接收任何以太币。如果希望某个函数能够接收以太币,可以使用 payable
修饰符将其标记为可支付的。这样,其他人就可以向该函数发送以太币作为交易。
documentation
在函数名和函数参数后,使用 payable
关键字可以使该函数成为一个可支付函数。
pragma solidity ^0.8.0;
contract PaymentContract {
// 定义一个接收ETH的支付函数
function receivePayment() payable public returns (uint256) {
// 在这里编写逻辑来处理接收到的以太币
// ...
return msg.value; //返回值为调用者附加的ETH大小
}
}
FAQ
什么函数可以使用 payable ?
只有 public
和 external
的函数支持 payable
修饰。因为如果函数在合约外部不可见的话,用户就无法调用函数,也就自然无法给支付以太给函数。
msg.value
concept
之前在 Solidity 101 已经学习了 msg.sender
,它们用于获取与当前函数交互的地址信息。现在继续学习 msg
的另一个全局变量:msg.value
。
msg.value
用于获取当前函数调用时传递给合约的以太币( Ether )数量。它表示当前函数调用中附带的以太币金额,通过读取 msg.value
的值,合约可以确定用户向其发送的以太币数量,进而执行相应的逻辑。
比喻
在现实生活中,当我们支付购买商品时,会交换一定数量的货币。同样,在区块链世界中,当用户向智能合约发送交易时,msg.value
就代表着这笔交易中需要转移的以太币金额。
真实用例
ERC2771Forwarder
合约,由于该函数希望有人传递 ETH 进行调用,就需要知道调用者传递的ETH数量是多少,这时就需要 msg.value
来获取。
function executeBatch(
ForwardRequestData[] calldata requests,
address payable refundReceiver
) public payable virtual {
...
if (requestsValue != msg.value) {
revert ERC2771ForwarderMismatchedValue(requestsValue, msg.value);
}
...
}
documentation
msg.value
作为全局变量,直接使用。
pragma solidity ^0.8.0;
contract Example {
uint256 public totalAmount;
function pay() public payable {
// 增加传递的以太币到总金额
totalAmount += msg.value;
}
}
FAQ
msg.value 的单位是?
msg.value
的单位是 Wei,是以太坊最小的货币单位。1 Eth = 10^18 Wei。
pay ETH
concept
在前面几节中,学习了 payable
修饰的函数以及使用 msg.value
来获取用户调用函数时所附加的 ETH 。
那么接下来,学习如何在调用函数时,附加 ETH 。
比喻
我们可以想象一个购物场景。假设你去商店买一件商品,你需要支付商品的价格给商店。在这个例子中,合约就好比是商店,而附加 ETH 则相当于你支付给商店的现金。
真实用例
在 OpenZepplin 的 Address
合约中,使用了address.call{value : }
的方式在调用函数的同时附加 ETH 。
function functionCallWithValue(address target, bytes memory data, uint256 value) internal returns (bytes memory) {
...
(bool success, bytes memory returndata) = target.call{value: value}(data);
...
}
documentation
在调用函数时,只需要在 函数名 和 () 之间插入一个 {value : xx} 语法即可,其中 xx 代表你需要附加的 ETH 数量。
pragma solidity ^0.8.0;
// 定义 Bank 合约
contract Bank {
mapping(address => uint256) public balances;
// 定义带有 payable 修饰符的 deposit 函数,以便接收以太币
function deposit() public payable {
balances[msg.sender] += msg.value;
}
}
// 定义用于与 Bank 合约进行交互的 User 合约
contract User {
Bank public bank;
// 构造函数,用于设置 Bank 合约地址
constructor(address _bankAddress) {
bank = Bank(_bankAddress);
}
// 调用 Bank 合约的 deposit 函数并发送以太币
function depositToBank() public payable {
// 调用deposit函数并传入ETH
// 调用成功的前提是:在调用该函数时,该合约里有大于5wei的余额。
bank.deposit{value: 5}();
}
}
FAQ
如何附加 ETH ?
调用一个函数并附加 ETH,在调用函数时使用 {value: 发送的以太币数量} 的语法,并确保函数具有 payable
修饰符。
例如,在一个智能合约中有一个接收以太币的存款函数 deposit
, 若要调用这个合约的 deposit
函数,并向其发送5个单位的以太币,可以通过以下语法实现:
deposit{value: 5}();
调用成功的前提是:在调用该函数时,合约 B 中有大于5 wei 的余额。
block
block.number
concept
来学习另一个全局属性 block
,block
属性下的全局变量都和区块信息有关。
block.number
是指当前的区块高度,也就是当前区块在整个区块链中的位置。每个新的区块都会递增这个值,所以它可以用来确定某个区块在区块链中的相对位置。
比喻
block.number
类似于产品的序列号,产品序列号用于唯一标识产品在生产中的顺序编号。每当一个新的区块被添加到区块链上时,它会被分配一个唯一的区块号(又称区块高度,每一个新产生的区块的区块号是递增的),就像产品在生产过程中被分配一个唯一的序列号。
真实用例
在 OpenZepplin 的 GovernorVotes
合约中,clock
函数用于返回时间,如果 token.clock
发生异常,则会将当前的区块高度 block.number
作为返回值。
function clock() public view virtual override returns (uint48) {
try token.clock() returns (uint48 timepoint) {
return timepoint;
} catch {
return SafeCast.toUint48(block.number);
}
}
documentation
使用 block.number
来获取当前区块的高度。
//通过block.number返回当前区块的高度,并赋值给了变量blockNumber。
uint256 blockNumber = block.number;
FAQ
在测试的时候怎么修改 block.number 呢?
在 Truffle 或者 Hardhat 这种框架中,都有专门的修改方式,而在 Remix 里面,你可以通过修改合约状态变量来增加 block.number
。
block.timestamp
concept
另一个 block
的全局变量 timestamp
,它是指当前区块的时间戳,即当前区块生成时距离1970年1月1日的秒数。
比喻
block.timestamp
类似于产品的生产日期,记录了区块被创建的时间戳。它表示了特定区块在区块链上的生成时间,就像产品上标注的生产日期表示了产品的制造时间。
真实用例
在 OpenZepplin 的 ERC20Permit
合约中,设置了一个过期时间,在执行 permit
时会判断当前时间是否已经过期,如果已经过期则将会 revert
回滚。
function permit(
...
) public virtual {
...
if (block.timestamp > deadline) {
revert ERC2612ExpiredSignature(deadline);
}
}
documentation
使用 block.timestamp
来获取当前区块的时间戳。
//通过 block.timestamp 返回当前区块的时间戳,并赋值给了变量 blockTimestamp。
uint256 blockTimestamp = block.timestamp;
FAQ
timestamp 和 block.number 有什么区别?
一个是以秒为单位,一个以区块号为单位,他们度量的单位不一致。
需要注意的是在不同的链上,block.number
不同。
而 timestamp
和现实世界挂钩,所以不管在哪条链上都是统一的。
event
define
concept
solidity 中一个特殊的结构——事件。
事件( Event )是一种用于在智能合约中发布通知和记录信息的机制。它可以在合约执行期间发出消息,允许外部应用程序监听并对这些消息做出响应。
比喻
事件可以被认为是合约的公告板,可以向外部应用程序广播重要的信息。
真实用例
在 IERC20 中,定义了一个 Transfer
事件,该事件记录了转账的发起者和接收者,以及转账的金额信息。
event Transfer(address indexed from, address indexed to, uint256 value);
documentation
在 solidity 中使用event
关键字来声明一个事件,其后是事件名,随后用括号把参数括起来。
//在这里我们定义了一个名为EventName的事件,其有parameter1和parameter2两个参数。
event EventName(
uint256 parameter1,
uint256 parameter2
);
FAQ
什么时候需要使用事件?
假设你是一个电商平台的管理员,你有一个智能合约来处理用户下单的过程。当有人下单时,你需要通知所有相关方,例如买家、卖家和物流公司。
如何使用事件?
1.定义一个名为 Order
的事件,里面包括下单者的地址,下单的物品,下单的数量。
event Order(address sender, string goods, uint count);
2.在有人下单的时候广播 Order
事件,这样所有人都可以收到下单者的地址,下单的物品,下单的数量这三个信息。
emit
concept
在 Solidity 中,要广播一个事件,你需要使用 emit
关键字。emit
用于初始化事件,并根据事件的定义设置所有需要的信息,然后广播这个事件。这样,所有订阅了该事件的人或系统就会收到通知。
真实用例
在新版的 ERC20 中,_update
函数用于在转账时修改账户信息,而所有功能完成后,使用 emit
提交了 Transfer
事件,告诉区块链中的所有人这笔交易完成了。
function _update(address from, address to, uint256 value) internal virtual {
...
emit Transfer(from, to, value);
}
documentation
提交事件使用emit
关键字,其后跟事件名和参数即可。
pragma solidity ^0.8.0;
contract EmitExample {
// 定义事件
event MessageSent(address sender, string message);
// 发送消息函数
function sendMessage(string memory message) public {
// 触发事件
emit MessageSent(msg.sender, message);
}
}
需要注意的是,智能合约是无法监听广播的信息的,只能使用 etherscan 或者别的方式监听,虽然这个事件是广播到区块链网络中的。
FAQ
在哪里可以看到提交的事件?
当提交事件时,会触发参数存储到交易的日志中,事件的参数存放在交易日志里。这些日志与合约的地址关联,并记录到区块链中。你可以通过一些工具辅助查询,例如 etherscan
等等。
indexed
concept
在 Solidity 中,事件的参数默认是不可搜索的,也就是说,你不能直接根据事件参数的值来过滤和搜索事件。然而,当你将某个参数标记为 indexed
时,Solidity 会为该参数创建一个额外的索引,使得你可以根据该参数的值进行过滤和搜索。
真实用例
在上一节的 Transfer
事件中,from
和 to
参数都被 indexed
修饰,这是为了大家可以使用地址来查询该地址的转账记录。
event Transfer(address indexed from, address indexed to, uint256 value);
documentation
在 Solidity 中,可以在事件声明中的参数类型后面添加 indexed
关键字,使参数可搜索。
pragma solidity ^0.8.0;
contract EventExample {
// 定义事件,其中sender可被搜索
event MessageSent(address indexed sender, string message);
// 发送消息函数
function sendMessage(string memory message) public {
// 触发事件
emit MessageSent(msg.sender, message);
}
}
contract
define
concept
合约类型是 Solidity 中的一种变量类型,用于存储对其他合约的引用。
合约类型的变量就是一个合约的实例。这个实例可以访问合约的所有公共函数和变量。
真实用例
在 OpenZepplin 的 GovernorTimelockControl
抽象合约中,定义了一个类型为 TimelockController
的变量 _timelock
。
abstract contract GovernorTimelockControl is Governor {
TimelockController private _timelock;
}
documentation
要定义一个合约类型的变量,需要按照 合约类型 + 合约名称 的格式进行声明。
MyContract mycontract;
//合约实例需要通过new的方式实例化
mycontract = new MyContract();
//同样也可以通过指定地址的方式实例化
MyContract mycontract = MyContract(contractAddress);
比如:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
// 合约A
contract ContractA {
uint256 public data;
}
// 合约B
contract ContractB {
//定义了一个ContractA的合约类型变量
ContractA public contractA;
ContractA public contractAA;
constructor(address _contractA) {
//将传入的合约地址实例化为ContractA合约,并将其赋值给contractA变量
contractA = ContractA(_contractA);
contractAA = new ContractA();
}
}
FAQ
合约类型的变量和其他变量有什么不同吗?
该变量可以调用其对应的合约,并且可以与地址类型 address
相互转换。
从类型来讲,他是一个引用类型的变量。
function call
concept
学习如何调用其他合约的函数,以获取返回值或者修改其他合约的变量。
documentation
使用 ContractName . functionName(parameters) 的形式来调用外部合约的函数。
//在这里我们调用了HQToken这个合约中的transfer函数,并将msg.sender和amount作为参数传入。
HQToken.transfer(msg.sender, amount);
比如:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
// 合约B
contract B {
uint public result;
function foo(uint _input) public {
result = _input * 2;
}
}
// 合约A
contract A {
B public b;
constructor(address _bAddress) {
b = B(_bAddress);
}
//调用B合约的foo函数
function callBFunction(uint _input) public {
b.foo(_input);
}
}
access variables
concept
上一节中,学习了如何调用其他合约的函数。
在这一节中将更进一步,学习如何获取其他合约的状态变量。
在有合约类型的变量后,即可通过该变量与相应的合约交互。而在solidity当中,会自动为每个公共变量生成一个查询函数,命名为变量名本身。
documentation
在获取基础类型时使用 ContractName.stateVariableName() 的形式来获取外部合约的状态变量值。
contract NFTContract {
uint public totalSupply;
}
contract MarketplaceContract {
NFTContract public nft;
function getTotalSupply() public view returns (uint) {
return nft.totalSupply();
}
}
比如:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
// 合约B
contract B {
uint public result;
function foo(uint _input) public {
result = _input * 2;
}
}
// 合约A
contract A {
B public b;
constructor(address _bAddress) {
b = B(_bAddress);
}
function callBFunction(uint _input) public {
b.foo(_input);
}
//接着上一节中的例子,我们来获取B合约的result变量的值
function getBResult() public view returns (uint) {
return b.result();
}
}
FAQ
能获取什么样的状态变量?
值得一提的是,只有 public
的变量是可以通过合约名.参数名() 的方式获取。private
修饰的变量是不可以的,这是因为 private
修饰的变量对外部是不可见的。
enum
define
concept
在 Solidity 中,"enum
"(枚举)是一种用户定义的数据类型,用于创建一组命名的常量。枚举通常用于为合约状态或其他固定值集合定义清晰和易读的名称。
比喻
例如,当人们计划旅行时,季节是一个重要的考虑因素。使用Enum
来表示季节,可以让旅行者了解目的地的气候和天气条件,从而决定最佳的旅行时间。
一年中有春夏秋冬四种季节,我们可以创建一个枚举类型来表示季节的选项:
enum Season {
Spring,
Summer,
Autumn,
Winter
}
真实用例
在OpenZepplin开发的GovernorCountingSimple
合约中,使用了VoteType
枚举,用于表示投票的类型:反对,支持和弃权。
enum VoteType {
Against,
For,
Abstain
}
documentation
在定义枚举类型时,需要使用enum
关键字,其后是枚举的名字,随后用{}将枚举变量括起来,每个枚举值之间用,分隔。
pragma solidity ^0.8.0;
contract Example {
//定义了一个枚举类型City
enum City {
BeiJing,
HangZhou,
ChengDu
}
}
FAQ
为什么使用枚举?
枚举类型的使用可以带来以下好处:
1.可读性: 枚举为取值提供了有意义的名称,使得代码更易读、理解。
2.可维护性: 枚举定义了取值的固定范围,使得代码更易于维护和修改。
3.类型安全: 枚举类型限制了变量的取值范围,避免了无效或不一致的取值,提高了代码的安全性和可靠性。
assign value
concept
学习 solidity 中的枚举类型是如何赋值的。
这种机制提供了一种结构化和易读的方式来管理和表达合约内部的状态或其他固定值集合。它确保只能从已经定义的集合中赋予有效值,为代码增加了额外的安全性和可读性。
比喻
如果还是用春夏秋冬举例子,那么:
1enum Season {
Spring,
Summer,
Autumn,
Winter
}
在上述示例中,Season
枚举类型中的每个枚举值将被映射为一个整数值。在上面的例子中,Spring
的整数值为0,Summer
的整数值为1,Autumn
的整数值为2,Winter
的整数值为3。
documentation
要将值分配给枚举,请引用枚举名称,后跟点.运算符,然后是所需的值。
enum State { Waiting, Ready, Active }
State s = State.Active;
FAQ
枚举能和整型进行类型转换吗?
枚举类型可以与整数进行显式转换,但不能进行隐式转换。
Season public season = 1; // 隐式转换,报错
Season public season = Season(1); // 显式转换
显式转换在运行时会检查数值范围(0-255),如果不匹配,将引发异常。
一个枚举类型最多有多少个值?
256个。因为枚举类型是以uint8
存储的,而uint8
的最大值为2的8次方就是0-255。所以一个枚举类型最多可以定义256个值,分别对应到uint8
的 0到255 。
max & min
concept
接着学习枚举类型的两个自带值——min
/max
。
使用type(NameOfEnum).min
and type(NameOfEnum).max
可以获得给定枚举的第一个值和最后一个值。
真实用例
还是相同的例子,在 GovernorCountingSimple
合约中,如果想要获取 VoteType
的最大值,可以使用 max
语法。
uint256 maxValue = type(VoteType).max;
documentation
使用type
(枚举名).min
/max
的语法来获取一个枚举的最小值和最大值。
//我们使用type(枚举名).max的语法获取到了Color这个枚举的最大值。
Color a = type(Color).max;
Color b = type(Color).min;
FAQ
可以给个示例再讲讲吗?
还是以 Season 枚举类型举例:
enum Season {
Spring,
Summer,
Autumn,
Winter
}
在上述示例中,Season
枚举类型中的最大值为 Winter
,对应的底层值为3;最小值为 Spring
。对应的底层值为0。
function modifier
define
concept
开始学习 solidity 特有的语法——函数修饰符( modifier
)。
函数修饰符允许开发人员在函数执行前后或期间插入代码,以便修改函数的行为或确保特定的条件得到满足,函数修饰符内的代码的不能被独立执行。
函数修饰符在修饰的函数执行之前被调用,允许在函数执行之前进行额外的检查或操作。
比喻
当我们在商店购买商品时,收银员会检查顾客给出的钱款是否足够支付商品的价格,确保顾客给出的钱足够支付商品后,才允许购买。这个检查过程就可以通过一个名为 checkBalance
的修饰符实现:
1modifier checkBalance(uint amount) {
require(balances[msg.sender] >= amount, "Insufficient balance");
_; //表示继续执行被修饰的函数(在下一节中会讲)
}
在上述示例中,如果账户余额满足要求,修饰符允许函数继续执行;否则,将抛出异常并显示" Insufficient balance "错误消息。
真实用例
在 OpenZepplin 给出的 Ownable 合约中,给出了 onlyOwner
修饰符,该修饰符会在合约执行前检查调用者是否是 owner
。
modifier onlyOwner() {
_checkOwner();
_;
}
documentation
在定义函数修饰符时,可以通过modifier
关键字来定义,其定义方式和函数一样,唯一的区别在于modifier
关键字取代了function
关键字。
modifier
相较于 function
而言,没有关键字,返回值,可见性的概念。
pragma solidity ^0.8.0;
contract Example {
address public owner;
uint public value;
// 定义了一个名为onlyOwner的函数修饰符(如果没有参数,可以省略()
modifier onlyOwner {
require(msg.sender == owner, "Only the contract owner can call this function.");
_; // 继续执行被修饰的函数(在下一节中会讲)
}
constructor() {
owner = msg.sender;
}
// 被onlyOwner修饰的函数(后面会讲)
function setValue(uint _newValue) public onlyOwner {
value = _newValue;
}
}
FAQ
函数修饰符在什么时候用?
函数修饰符在 Solidity 中主要用于封装重复的逻辑和进行权限控制,以简化代码并提高可维护性。常用于多个函数需要执行相同的前置检查或条件验证时。
underline
concept
在上一节中,学习了函数修饰符 modifier
的定义,并且在 Example 中提到了一个语法 _;
在这一节中,将深入探讨该语法的作用。
modifier
的执行是在函数执行之前的。_;
表示继续执行被修饰的函数。
在一个 modifier
中,如果缺少_;
语句,编译器会报错。因为你必须明确的告诉编译器在什么时候需要重新执行被修饰的函数代码。
真实用例
还是同样的 Ownable 合约,在 onlyOwner 修饰符中,_;
表示运行函数体的代码,而_checkOwner();
在_;
前,证明该检查是在函数执行前运行的。
modifier onlyOwner() {
_checkOwner();
_;
}
documentation
_;
被用来在 modifier
中指定一个地方执行被修饰的函数的代码。
pragma solidity ^0.8.0;
contract Example {
uint256 public locked;
modifier lock() {
require(locked == 0);
locked = 1;
_;
locked = 0;
}
//该函数使用了lock修饰符
function dosome1() public lock {
//该调用会失败
dosome2();
}
//该函数也使用了lock修饰符,且这两个函数之间不能相互调用。
//因为在一个函数执行时,locked 变量会置为1,导致lock中的require过不了。
function dosome2() public lock {
}
}
FAQ
设计_;
语法的目的是什么?
这样的设计也让 modifier
能够在函数执行结束后执行一段代码,只需要将需要执行的代码放在_;
之后即可。如下代码:
modifier demo() {
... // 函数执行前执行的代码
_; // 执行被修饰的函数
... // 函数执行结束后执行的代码
}
usage
concept
了解了函数修饰符( modifier
)的基础语法,了解了它是如何定义的以及它的一些基础特性。那么接下来,我们将会学习 modifier
的使用方法和应用场景。
修饰符在智能合约中经常被用于实现一些安全和访问控制的功能。通过添加修饰符,可以在执行函数之前添加一些预设条件,从而使得函数的使用更加安全和可控。
documentation
作为关键字出现在函数参数和{}
之间,无参数型不需要括号。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract Example {
uint256 public number;
modifier add() {
number++;
_;
number++;
}
//调用一次该函数 number 的值会增加2,且该函数的返回值总是比number的值小1
//这是因为 number++ 在函数执行前后都执行了一次
function doSomething() public add returns (uint256) {
return number;
}
}
//在这里我们使用 lock 这个 modifier修饰了 transfer 这个函数。
function transfer(address to, uint256 amount) public lock {
}
//作为关键字出现在函数参数和{}之间,有参数型把参数放在括号里即可。
function transfer2(address to, uint256 tokenId) public lock(tokenId) {
}
FAQ
意思是只要函数头中有 modifier,就会执行其中的代码吗?
是的,一旦函数被 modifier
修饰,在调用该函数之前,都会进入此 modifier
中执行代码。
multiple modifier
concept
单个“函数”可以有多个“修饰符”。 修饰符按照它们出现的顺序执行。
比喻
Solidity 函数中的多个修饰符就像不同检查点的安全许可层,其中每个修饰符代表在访问受保护区域(函数执行)之前必须授予的特定授权级别。
真实用例
在这个合约中,changeOwner
“函数”有两个“修饰符”:onlyOwner
和 onlyAfterOneHour
。 onlyOwner
修饰符限制该函数只能由所有者调用。 onlyAfterOneHour
modifier
限制该函数只能在 contract
部署后一小时内调用。 “修饰符”按照列出的顺序执行。
contract MultiModifierContract {
address public owner;
uint public creationTime;
constructor() {
owner = msg.sender;
creationTime = block.timestamp;
}
modifier onlyOwner {
require(msg.sender == owner, "Only owner can execute.");
_;
}
modifier onlyAfterOneHour {
require(block.timestamp >= creationTime + 1 hours, "Function can only be called after one hour.");
_;
}
function changeOwner(address _newOwner) public onlyOwner onlyAfterOneHour {
owner = _newOwner;
}
}
documentation
当一个“函数”有多个“修饰符”时,它们以空格分隔,并按它们出现的顺序应用。
function bid() public payable aboveMinimumBid beforeAuctionEnd {
// Place the bid
}