上次介绍了 Solidity 中的面向对象,这次来详细说说 Solidity 的循环、数组、交易、修饰符、自定义错误类型与事件、合约的回调函数(Fallback / Receive)以及接口(Interface)/ 抽象合约(Abstract Contract)。
数组
第一期只是简单的介绍了一下数组创建与使用,下面来说说如何向数组添加元素以及重置数组。
首先准备一个数组:
address[] tAddress = new address[](0);
构造函数的参数代表默认数组长度。
如果你的编辑器有代码提示,发现这个 tAddress 有一个成员变量 length
以及两个函数:push
和 pop
。
- length:数组长度(uint)
- push:尾部添加元素
- pop:删除尾部元素并且数组长度减一
添加元素
tAddress.push(0x0000000000000000000000000000000000000000);
重置数组
重置的方法很简单,直接赋值一个新的空数组即可。
tAddress = new address[](0);
删除元素
删除元素对于一个需要保持元素顺序的数组来说是比较难实现的,不过也有解决方法。
无序数组
对于无序数组,由于元素顺序可以不同,因此直接用最后一个元素代替要删除的元素,然后使用数组的 pop
方法即可。
address[] tAddress = new address[](0);
function removeElement(uint index) public {
require(index < tAddress.length, "Index out of bounds");
tAddress[index] = tAddress[tAddress.length - 1];
tAddress.pop();
}
有序数组
对于有序的数组,删除某个位置的元素,其后续的所有元素应该向前移动一个位置,然后删除最后一个元素即可。
address[] tAddress = new address[](0);
function removeElement(uint index) public {
uint length = tAddress.length;
require(index < length, "Index out of bounds");
for (uint i = index; i < length - 1; i++) {
tAddress[i] = tAddress[i + 1];
}
tAddress.pop();
}
循环
看完上面的删除有序数组的元素发现,Solidity 中的 for 循环和其他语言的也是大同小异。
但是要注意的是,Solidity 不像 Java 或其他编程语言支持迭代器,因此你只能使用这种传统的 for 循环。
交易操作
交易是智能合约的核心部分,每一个账户地址或者合约地址,都可以拥有ETH。因此,对于合约来说,它们也可以给某个地址进行转账,同样也接受来自其他地址的转账。
在 Solidity 中,payable
是一个关键字,用于标记允许接收以太币(ETH)的函数或地址。只有被标记为 payable
的函数或地址,才能接收来自外部账户的转账或调用中的以太币。
如果一个函数需要接收以太币,就必须加上 payable
关键字,下面来写一个接受其他地址转账的函数:
uint balance = 0;
function donate() public payable {
// TODO
}
这里写了一个捐赠函数,合约接受其他用户或合约捐赠的以太币(ETH),还有一个 balance
变量用于记录合约接收到的总以太币的数量。
uint balance = 0;
function donate() public payable {
uint amount = msg.value;
balance += amount;
}
msg 变量
在 Solidity 中,msg
是一个全局变量,用于提供当前交易或调用的一些信息。msg
包含了当前交易或调用的元数据,比如发送者地址、发送的以太币数量、数据等。它提供了以下几种常用的属性和方法:
- msg.sender(address):代表调用函数的调用者地址。
- msg.value(uint):表示本次交易向合约发送的以太币数量。
- msg.data(bytes):包含了当前交易的输入数据,不包含函数名称和签名。
- msg.sig(bytes4):表示当前交易的函数选择器。它是函数签名的前 4 个字节,用于标识调用的函数。通常情况下,函数签名的前 4 个字节(sig)是通过
keccak256
哈希生成的。
自定义修饰符
第一期介绍了最基本的访问修饰符、变量修饰符以及函数修饰符。在 Solidity 中我们还可以自定义修饰符来实现特定的功能。
现在希望某些函数只允许合约所有者(创建合约者)调用,那就需要一个不可变变量来保存合约所有者的地址,由于 msg 全局变量提供了调用者的地址,因此只要判断调用者与合约所有者地址是否相同即可。
在多个函数开头写重复的代码完全没有必要,你可能会想到用一个函数来判断,这种方法当然没有问题,但有没有更简单的方法呢?使用自定义函数修饰符就能进一步简化这个操作了。
定义修饰符的语法:modifier [修饰符名称] { _; }
注意到里面有一个 _;
,这个并不是随意写的两个符号,而是代表继续执行被这个修饰符修饰的函数的其他代码。
下面来完成这个修饰符,只要调用者不是合约所有者,就回滚本次交易。
modifier contractOwnerOnly {
if (msg.sender != contractOwner) {
revert ();
}
_;
}
下面来看看如何使用吧:
function transferOwnership(address newOwner) contractOwnerOnly public {
contractOwner = newOwner;
}
结合修饰符的代码,当这个函数被调用时,先判断调用者是否是合约所有者,如果 true 则 if 条件不成立,继续调用 transferOwnership()
的代码。如果非合约所有者,则触发 revert
回滚本次交易。
自定义错误
上面提到了可以用 revert()
来回滚交易,但对于调用者来说,有时候他们很难理解为什么交易会被回滚。
因此可以自定义一些错误类型,在交易回滚时可以指定错误类型,这样调用者就能知道具体的原因了。
定义错误的语法如下:
error [错误名称]({[参数类型] [参数名称], ...});
例如定义一个错误告诉调用者,函数只运行所有者调用:
error NotContractOwner();
现在改造一下上面自定义的函数修饰符:
modifier contractOwnerOnly {
if (msg.sender != contractOwner) {
revert NotContractOwner();
}
_;
}
自定义事件
和自定义错误很相似,也是同样的语法:
event [事件名称]({[参数类型] [参数名称], ...});
使用 emit [事件名称]({[参数], ...})
; 发送事件即可。
例如定义一个合约所有者变更的事件:
event ContractOwnerChanged(address originalOwner, address newOwner);
然后改造上面的变更合约所有者函数:
function transferOwnership(address newOwner) contractOwnerOnly public {
emit ContractOwnerChanged(contractOwner, newOwner);
contractOwner = newOwner;
}
合约回调函数
在 Solidity 中,receive
和 fallback
是两种特殊的函数,它们用于处理合约接收到的以太币(ETH)或不匹配的函数调用。
Receive()
receive
是一个特殊的函数,用于接收以太币。它没有参数,也没有返回值,且只能在合约接收以太币时触发。当发送以太币的交易没有携带数据时(即空的调用),receive
函数会被触发。
receive()
的函数定义如下:
receive() external payable {
revert();
}
现在合约拒绝任何空调用且发送了一定数量的以太币的交易,这样可以防止用户直接向合约转账,保证合约的安全性。
Fallback()
fallback
函数是一个更加通用的函数,它用于处理两种情况:
- 当合约接收到以太币且交易携带了数据时(即包含非空的调用数据)。
- 当合约接收到一个没有匹配任何函数的调用时(即调用的函数不存在或者签名不匹配)。
通俗来说,如果用户调用了一个不存在的函数或者直接向合约转账且带有数据,fallback
() 方法就会被触发。
fallback()
的函数定义如下:
fallback() external {
revert();
}
同样的,一般来说会拒绝用户的错误函数调用,保证合约安全性。
接口(Interface)
学过面向对象语言的朋友应该对这个很熟悉了,在 Solidity 中也没什么很大区别,下面来看看接口在 Solidity 中是怎么样的吧。
接口声明
声明一个接口:
interface IDonation {
}
声明接口的方法很简单,而接口内也只能声明没有具体实现的外部函数(External Function)定义,下面来完成这个接口:
interface IDonation {
function donate() external payable;
}
下面的声明方式是不正确的,编译错误:Functions in interfaces must be declared external.solidity(1560)
interface IDonation {
function donate() public payable;
}
还记得合约的继承吗?一个合约也可以使用 is
关键字继承多个接口。
contract Donation is IDonation {
function donate() external payable override {
}
}
这样就完成了对 IDonation
接口的实现,对于实现方法,必须加上 override
关键字。
根据地址引入
有时候可能需要调用其他合约的方法,而这个合约的源码是不知道的,但只要有具体的合约地址和相应的接口源码就能实现对其他已经部署的合约的接口进行调用。
也就是说,如果 Donation 已经部署在区块链上,而它的合约地址假设是 0xd03ba57e60。
因为它实现了 IDonation 接口,因此在其他合约中,只要有 IDonation 接口的源码即可实现对这个 Donation 合约的调用。
引用方法很简单:
IDonation donation = IDonation(0xd03ba57e60);
donation.donate();
抽象合约(Abstract Contract)
也就是抽象类的概念,与接口不同的是,抽象合约可以包含成员变量以及函数实现。
下面来看看如何使用抽象类,实现一个 Ownable
类。
抽象合约声明
先声明一个抽象合约:
abstract contract Ownable {
}
接着定义几个自定义错误和事件:
abstract contract Ownable {
// 非合约所有者
error NotContractOwner();
// 拒绝变更合约所有者
error ContractOwnerChangeRefused();
// 合约所有者变更
event ContractOwnerChanged(address originalOwner, address newOwner);
}
添加变量和构造函数:
// 合约所有者
address private contractOwner;
constructor() {
contractOwner = msg.sender;
}
定义一个合约所有者变更函数以及修饰符:
function transferOwnership(address newOwner) contractOwnerOnly public {
emit ContractOwnerChanged(contractOwner, newOwner);
contractOwner = newOwner;
}
modifier contractOwnerOnly {
if (msg.sender != contractOwner) {
revert NotContractOwner();
}
_;
}
现在完整的代码如下:
abstract contract Ownable {
// 非合约所有者
error NotContractOwner();
// 拒绝变更合约所有者
error ContractOwnerChangeRefused();
// 合约所有者变更
event ContractOwnerChanged(address originalOwner, address newOwner);
// 合约所有者
address internal contractOwner;
constructor() {
contractOwner = msg.sender;
}
function transferOwnership(address newOwner) contractOwnerOnly public {
emit ContractOwnerChanged(contractOwner, newOwner);
contractOwner = newOwner;
}
modifier contractOwnerOnly {
if (msg.sender != contractOwner) {
revert NotContractOwner();
}
_;
}
}
对于继承的合约,直接用 is
修饰符继承即可。
抽象方法
现在需要实现一个功能,在所有者变更之前检查新的所有者的地址,如果是0x00(无所有者),则拒绝变更请求。
这个函数可以认为是 Ownable 的一个抽象方法,对于不同的合约,可以实现不同的检查逻辑。
function preTranferOwnership(address originalOwner, address newOwner) internal pure virtual returns (bool);
当这个函数返回 true 则表示允许变更所有者。
与合约的可覆盖函数一样,加上 virtual
关键字即可。
改造一下变更所有者的函数:
function transferOwnership(address newOwner) contractOwnerOnly public {
if (!preTranferOwnership(contractOwner, newOwner)) {
revert ContractOwnerChangeRefused();
}
emit ContractOwnerChanged(contractOwner, newOwner);
contractOwner = newOwner;
}
对于实现的合约,实现具体的函数即可:
contract Donation is Ownable {
function preTranferOwnership(address originalOwner, address newOwner) internal pure override returns (bool) {
return newOwner != address(0);
}
}
当然,和 Java 一样,这个抽象方法可以有默认实现:
function preTranferOwnership(address originalOwner, address newOwner) internal pure virtual returns (bool) {
return newOwner != address(0);
}
这样继承的合约就不要求一定要实现这个抽象函数了。
构造函数参数
上面介绍的是空构造函数的抽象合约的实现,下面看看有参的抽象合约的构造函数如何被其他合约继承。
现在允许合约创建时指定一个所有者,就需要用一个构造函数参数来接收这个指定的所有者地址:
constructor(address initialOwner) {
if (initialOwner != address(0)) {
contractOwner = initialOwner;
} else {
contractOwner = msg.sender;
}
}
现在对于实现这个抽象合约的合约来说,会提示错误:No arguments passed to the base constructor. Specify the arguments or mark “Donation” as abstract.solidity(3415)
和其他语言不同的是,Solidity 中并不是用 supre() 来调用父构造函数,而是用如下方式:
constructor(address initialOwner) Ownable(initialOwner) {
}
多构造函数继承
上面介绍了单个抽象类已经它的构造函数如何被继承,如果是多个抽象类有多个构造函数呢?
假设现在还有下面这个抽象合约:
abstract contract Ownable2 {
constructor(address testParam2) {
}
}
这样继承合约就要改成:
contract Donation is Ownable, Ownable2 {
constructor(address initialOwner)
Ownable(initialOwner)
Ownable2(address(0))
{
}
}
因此,如果继承多个合约的多个有参构造函数,并列在 constructor()
后面加上对应的抽象类的构造函数并且传入参数即可。
Ownable 合约
利用上面提到的所有知识,现在我们可以来完成这个合约了:
abstract contract Ownable {
// 非合约所有者
error NotContractOwner();
// 拒绝变更合约所有者
error ContractOwnerChangeRefused();
// 合约所有者变更
event ContractOwnerChanged(address originalOwner, address newOwner);
// 合约所有者
address internal contractOwner;
constructor(address initialOwner) {
if (initialOwner != address(0)) {
contractOwner = initialOwner;
} else {
contractOwner = msg.sender;
}
}
function transferOwnership(address newOwner) contractOwnerOnly public {
if (!preTranferOwnership(newOwner)) {
revert ContractOwnerChangeRefused();
}
emit ContractOwnerChanged(contractOwner, newOwner);
contractOwner = newOwner;
}
function preTranferOwnership(address newOwner) internal pure virtual returns (bool) {
return newOwner != address(0);
}
function getContractOwner() public view returns (address) {
return contractOwner;
}
/**
* 仅合约所有者可用
*/
modifier contractOwnerOnly {
if (msg.sender != contractOwner) {
revert NotContractOwner();
}
_;
}
}
结束语
这一期内容比较多,看到这里,Solidity 的基础知识也就学的差不多了,现在你可以正式开始合约开发了。
下一期将开发一个允许任何人发起捐赠项目的智能合约,这个合约将会覆盖之前所有所学的知识。