合约
合约是 Solidity
中我们需要处理的最高级、最复杂,同时也是最重要的类型。我们的大部分功能都是在合约中实现。
日光之下,并无新物。合约其实类似于面向对象编程中的 类
。
合约中定义了一系列的方法以及状态变量。当我们要启用一个合约时,我们将合约的代码部署到某一个以太坊地址上,我们把这个过程称为合约部署。
部署完成后,其他用户就可以通过该地址来跟合约交互,例如进行一次投票。
假设用户 A
需要调用地址 0x01
处的合约中的 vote
方法进行投票,则用户 A
将该请求发送到以太坊网络上,接收到的 EVM
从 0x01
处取出保存的合约代码,执行其中的 vote
方法代码。
合约中的代码以及状态变量(在合约内声明,保存在存储区中的变量)会被永久性地存储在以太坊区块链中。
声明合约
合约通过 contract Name {}
进行声明。在合约内部可以声明:
- 合约状态变量
- 合约函数
- 自定义类型
struct
结构体
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.0 <0.9.0;
contract ContractDeclare {
address[] usersAddress; // 状态变量
mapping (address => uint) tokenCount; // 状态变量
type Age is uint8; // 自定义类型
struct User { // struct 结构体
address addr;
string nickname;
Age age;
}
User[] public users; // 状态变量
// 合约函数
function register(string calldata nickname, Age age) external {
User memory newUser;
newUser.nickname = nickname;
newUser.age = age;
newUser.addr = msg.sender;
users.push(newUser);
}
}
部署合约
我们可以通过 web3.js
、truffle
、Remix IDE
等工具,将合约代码由外部部署到以太坊上。这种部署方式是通过以太坊网络向外暴露的 RPC
接口或 HTTP
接口等实现的。
除此之外,也可以在智能合约 A
内部,通过 new
关键字来实例化另一个合约 B
,从而把合约 B
部署到以太坊上。我们以 合约工厂
为例,不同的用户可以通过该合约工厂的 createContract
函数来创建一张属于该用户的合约:
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.0 <0.9.0;
/// @title 用户合约
contract UserContract {
address owner;
string nickname;
constructor(string memory _nickname) {
owner = msg.sender;
nickname = _nickname;
}
}
/// @title 合约工厂
contract ContractFactory {
mapping(address => UserContract) public contractMap; // 记录每个用户的合约
/// @notice 创建用户合约
function createContract(string calldata nickname) external {
UserContract c = new UserContract(nickname);
contractMap[msg.sender] = c;
}
}
构造函数
在上面的 UserContract
合约中,我们可以看到一个特殊的合约函数 constructor
,该函数称为构造函数,当且仅当合约被创建时调用。在该函数中我们可以对合约做一些初始化工作,例如初始化状态变量。
一个合约只能有一个 constructor
函数。
合约销毁
Solidity
可以通过 selfdestruct
来销毁一个合约,包括它的代码以及存储变量。但是根据 EIP-6049
提案,该方法已经被标志为 deprecated
(过时的),因为未来可能会有破坏性的改动。
selfdestruct(payableAddress)
接受一个可支付地址参数,合约当前的以太币余额会转被该地址,然后销毁当前合约。
状态变量可见性
声明状态变量语法:
状态变量有三种可见性:
- public: 公共
- internal: 内部
- private: 私有
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.0 <0.9.0;
contract StateVarVisibility {
string public nickname; // 公共状态变量
address private owner; // 私有状态变量
uint internal userCount; // 内部状态变量
}
公共状态变量
公共状态变量拥有最大的可见性,无论是合约外部、合约内部或者派生合约,都可以访问该变量。
当从外部访问一个合约的公共状态变量时,需要注意合约对外只暴露函数接口。也就是说外部只能访问合约内的公共函数或外部函数。
因此,为了让外部能够访问公共状态变量,Solidity
会为它们创建一个同名的 getter
函数。对于 Java
程序员可能会对这种方式非常熟悉。
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.0 <0.9.0;
contract Son {
uint public age;
function getAge() external view returns (uint) {
return age; // 内部访问,直接访问状态变量 age
}
}
contract Parent {
function getSonAge() external returns (uint) {
Son s = new Son();
return s.age(); // 外部访问 age,需要调用同名 getter 函数
}
}
内部状态变量
标为 internal
的内部状态变量,它只对合约内部,以及它的派生合约可见。对于派生合约的内容,可以参阅 《派生与继承》。
私有状态变量
私有状态变量,只对当前合约可见,即使是派生合约也不能访问它。
函数可见性
合约内的函数有四种可见性:
- public:公共函数
- external:外部函数
- internal:内部函数
- private:私有函数
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.0 <0.9.0;
contract FunctionVisibility {
// 公共函数
function publicFunc(string memory name) public {}
// 外部函数
function externalFunc(string calldata name) external {}
// 内部函数
function internalFunc(string memory name) internal {}
// 私有函数
function privateFunc(string memory name) private {}
}
公共函数
公共函数对合约内部、外部、派生合约可见。
外部函数
外部函数可以视为合约的对外接口,它只对外部可见。
内部函数
内部函数对当前合约内部、派生合约可见。
私有函数
私有函数只对当前合约内部可见,即使是派生合约该函数也是不可见的。
this
Solidity
中同样使用 this
关键字代表当前合约实例。
在 Solidity
中,当在合约内部,访问当前合约中的状态变量或者函数时,例如调用当前合约中的函数 f
,不需要使用 this.f()
,只需要直接写 f()
。
当 f
是一个公共函数时,在合约内部,f()
表示直接通过内部调用来调用 f
;this.f()
则表示通过外部调用调用 f
。
同样道理,如果在当前合约内部要使用外部调用的方式访问公共状态变量,则应该调用其 getter
函数:this.var()
。