1.变量与数据类型
1.1值类型
1.布尔型:bool
// 布尔值
bool public _bool = true;
2.整型:int、uint、uint256
// 整型
int public _int = -1; // 整数,包括负数
uint public _uint = 1; // 无符号整数
uint256 public _number = 20220330; // 256位无符号整数
3.地址类型:
普通地址(address): 存储一个 20 字节的值(以太坊地址的大小)
payable address: 比普通地址多了 transfer 和 send 两个成员方法,用于接收转账。
// 地址
address public _address = 0x7A58c0Be72BE218B41C608b7Fe7C5bB630736C71;
address payable public _address1 = payable(_address); // payable address,可以转账、查余额
// 地址类型的成员
uint256 public balance = _address1.balance; // balance of address
4.定长字节数组:
定长字节数组: 属于值类型,数组长度在声明之后不能改变。根据字节数组的长度分为 bytes1, bytes8, bytes32 等类型。定长字节数组最多存储 32 bytes 数据,即bytes32。
不定长字节数组: 属于引用类型,数组长度在声明之后可以改变,包括 bytes 等。
// 固定长度的字节数组
bytes32 public _byte32 = "MiniSolidity";
bytes1 public _byte = _byte32[0];
5.枚举 enum:
用户自定义类型,本质是 uint,从 0 开始
// 用enum将uint 0, 1, 2表示为Buy, Hold, Sell
enum ActionSet { Buy, Hold, Sell }
// 创建enum变量 action
ActionSet action = ActionSet.Buy;
1.2 引用类型
1.数组:
固定长度数组:在声明时指定数组的长度。用T[k]的格式声明,其中T是元素的类型,k是长度
// 固定长度 Array
uint[8] array1;
bytes1[5] array2;
address[100] array3;
可变长度数组:在声明时不指定数组的长度。用T[]的格式声明,其中T是元素的类型,例如:
// 可变长度 Array
uint[] array4;
bytes1[] array5;
address[] array6;
bytes array7;
动态数组:对于memory修饰的动态数组,可以用new操作符来创建,但是必须声明长度,并且声明后长度不能改变。例子:
// memory动态数组
uint[] memory array8 = new uint[](5);
bytes memory array9 = new bytes(9);
注意:bytes比较特殊,是数组,但是不用加[]。另外,不能用bytes[]声明单字节数组,可以使用bytes或bytes1[]。bytes 比 bytes1[]省gas。
2.结构体
自定义复合数据类型,可包含多个不同类型字段
// 结构体
struct Student{
uint256 id;
uint256 score;
}
Student student; // 初始一个student结构体
1.3映射类型
声明映射的格式为mapping(KeyType => _ValueType),其中KeyType和_ValueType分别是Key和Value的变量类型。例子:
mapping(uint => address) public idToAddress; // id映射到地址
mapping(address => address) public swapPair; // 币对的映射,地址到地址
映射的存储位置必须是storage
映射的原理:
原理1: 映射不储存任何键(Key)的资讯,也没有length的资讯。
原理2: 对于映射使用keccak256(h(key) . slot)计算存取value的位置。
原理3: 因为Ethereum会定义所有未使用的空间为0,所以未赋值(Value)的键(Key)初始值都是各个type的默认值,如uint的默认值是0。
2.函数
2.1.基本结构
function <function name>([parameter types[, ...]]) {internal|external|public|private} [pure|view|payable] [virtual|override] [<modifiers>]
[returns (<return types>)]{ <function body> }
function:声明函数时的固定用法。要编写函数,就需要以 function 关键字开头。
<function name>:函数名。
([parameter types[, ...]]):圆括号内写入函数的参数,即输入到函数的变量类型和名称。
{internal|external|public|private}:函数可见性说明符,共有4种。
public:内部和外部均可见。
private:只能从本合约内部访问,继承的合约也不能使用。
external:只能从合约外部访问(但内部可以通过 this.f() 来调用,f是函数名)。
internal: 只能从合约内部访问,继承的合约可以用。
注意 1:合约中定义的函数需要明确指定可见性,它们没有默认值。 注意 2:public|private|internal 也可用于修饰状态变量。public变量会自动生成同名的getter函数,用于查询数值。未标明可见性类型的状态变量,默认为internal。
[pure|view|payable]:决定函数权限/功能的关键字。payable(可支付的)很好理解,带着它的函数,运行的时候可以给合约转入 ETH。pure 和 view 的介绍见下一节。
[virtual|override]: 方法是否可以被重写,或者是否是重写方法。virtual用在父合约上,标识的方法可以被子合约重写。override用在自合约上,表名方法重写了父合约的方法。
<modifiers>: 自定义的修饰器,可以有0个或多个修饰器。
[returns ()]:函数返回的变量类型和名称。
<function body>: 函数体。
2.2.pure / view / payable
2.2.1. 为什么需要 pure 和 view
修改链上状态 → 需要 gas 不修改链上状态 → 直接调用不消耗 gas
2.2.2. pure 函数:既不能读取,也不能修改状态变量
function addPure(uint x) external pure returns(uint) {
return x + 1;
}
pure 函数中 禁止:1.读取状态变量 2.修改状态变量 3.调用非 pure / view 函数
2.2.3.view 函数
可以读取,但不能修改状态变量
function addView() external view returns(uint) {
return number + 1;
}
view 函数中 禁止:1. 修改状态变量 2.转账 ETH 3.释放事件
以下操作会被视为修改链上状态: 1.写入状态变量 2.释放事件(emit) 3.创建合约 4.使用 selfdestruct 5.发送 ETH 6.调用未标记 view / pure 的函数 7.低级调用(call / delegatecall) 8.内联汇编中的某些操作码
2.3.payable 函数
function minusPayable() external payable returns(uint) {
minus();
return address(this).balance;
}
payable 表示函数可以接收 ETH 未标记 payable 的函数无法接收转账 address(this).balance:合约当前余额
3.变量数据存储和作用域
引用类型比较变量比较复杂,占用空间大,使用时必须要声明数据存储的位置
数据存储位置
1.storage: 链上(合约永久存储,合约状态变量默认为此类型 gas 消耗最高,永久保存
2.memory:临时内存(不上链,函数内参数 / 临时变量;变长返回值必须加(string/bytes/ 数组 / 自定义结构体) 可修改,临时存在,gas 中等
3.calldata:临时内存(不上链,函数参数专用 不可修改(immutable) ,gas 消耗最低,适合只读参数
function fCalldata(uint[] calldata _x) public pure returns(uint[] calldata){
//参数为calldata数组,不能被修改
// _x[0] = 0 //这样修改会报错
return(_x);
}
不同位置 gas 消耗不同,排序:storage > memory > calldata;storage 存链上(硬盘),memory/calldata 存临时内存(不上链)
赋值规则
赋值仅分 「创建引用(修改同步)」和「创建副本(修改互不影响)」两种情况,核心记引用的 2 种场景 ,其余均为副本:
赋值创建引用(修改新变量,原变量同步变化)
storage(合约状态变量)→ 函数内本地 storage 变量
uint[] x = [1,2,3]; // 状态变量:数组 x
function fStorage() public{
//声明一个storage的变量 xStorage,指向x。修改xStorage也会影响x
uint[] storage xStorage = x;
xStorage[0] = 100;
}
memory → memory
变量的作用域
- 状态变量
- 声明位置:合约内、函数外
- 存储位置:默认storage(链上)
- 特性:全合约函数可访问,gas 消耗高,永久保存
- 操作:可在函数内直接修改值
- 局部变量
- 声明位置:函数内
- 存储位置:默认memory(内存)
- 特性:仅函数执行时有效,函数退出后销毁,gas 消耗低,不上链
- 作用:临时计算、存储中间值
- 全局变量
本质:Solidity预留关键字,无需声明,函数内可直接使用
存储:无显式存储位置,由区块链底层提供
核心常用全局变量(重点记):
- msg.sender:交易 / 调用的发起地址(payable)
- msg.value:当前交易发送的 wei 数量
- block.number:当前区块高度
- block.timestamp:当前区块时间戳(unix 秒数)
- gasleft ():合约执行剩余 gas 量
4.构造函数与修饰器
4.1 构造函数
构造函数(constructor)是一种特殊的函数,每个合约可以定义一个,并在部署合约的时候自动运行一次。它可以用来初始化合约的一些参数,例如初始化合约的owner地址:
address owner; // 定义owner变量
// 构造函数
constructor(address initialOwner) {
owner = initialOwner; // 在部署合约的时候,将owner设置为传入的initialOwner地址
}
4.2 修饰器
修饰器(modifier)是Solidity特有的语法,类似于面向对象编程中的装饰器(decorator),声明函数拥有的特性,并减少代码冗余。
// 基本结构
modifier 修饰器名 {
require(条件);
_;
}
//require:前置检查 _:函数主体执行的位置
//onlyOwner 修饰器示例
modifier onlyOwner {
require(msg.sender == owner);
_;
}
//只有 owner 地址才能继续执行函数 否则交易直接 revert
//modifier 的使用方式
function changeOwner(address _newOwner)
external
onlyOwner
{
owner = _newOwner;
}
//只有当前 owner 可以修改 owner 非 owner 调用直接失败
5.事件
Solidity中的事件(event)是EVM上日志的抽象,它具有两个特点: 响应:应用程序(ethers.js)可以通过RPC接口订阅和监听这些事件,并在前端做响应。 经济:事件是EVM上比较经济的存储数据的方式,每个大概消耗2,000 gas;相比之下,链上存储一个新变量至少需要20,000 gas。
5.1声明事件
事件的声明由event关键字开头,接着是事件名称,括号里面写好事件需要记录的变量类型和变量名。以ERC20代币合约的Transfer事件为例:
event Transfer(address indexed from, address indexed to, uint256 value);
我们可以看到,Transfer事件共记录了3个变量from,to和value,分别对应代币的转账地址,接收地址和转账数量,其中from和to前面带有indexed关键字,他们会保存在以太坊虚拟机日志的topics中,方便之后检索。
5.2释放事件
我们可以在函数里释放事件。在下面的例子中,每次用_transfer()函数进行转账操作的时候,都会释放Transfer事件,并记录相应的变量。
// 定义_transfer函数,执行转账逻辑
function _transfer(
address from,
address to,
uint256 amount
) external {
_balances[from] = 10000000; // 给转账地址一些初始代币
_balances[from] -= amount; // from地址减去转账数量
_balances[to] += amount; // to地址加上转账数量
// 释放事件
emit Transfer(from, to, amount);
}
6.合约继承
- virtual:父合约中希望被子合约重写的成员,必须加此关键字;
- override:子合约中重写父合约的成员,必须加此关键字;
注意:override 修饰 public 变量时,实际重写的是变量同名的 getter 函数(如mapping(address => uint) public override balanceOf;)。
6.1简单继承
contract Yeye {
event Log(string msg);
// 定义3个function: hip(), pop(), yeye(),Log值为Yeye。
function hip() public virtual{
emit Log("Yeye");
}
function pop() public virtual{
emit Log("Yeye");
}
function yeye() public virtual {
emit Log("Yeye");
}
}
contract Baba is Yeye{
// 继承两个function: hip()和pop(),输出改为Baba。
function hip() public virtual override{
emit Log("Baba");
}
function pop() public virtual override{
emit Log("Baba");
}
function baba() public virtual{
emit Log("Baba");
}
}
6.2多重继承
继承顺序:必须按辈分从高到低排列(如爷爷→爸爸,不能爸爸→爷爷),否则报错;
重名必重写:若多个父合约有同名的virtual函数,子合约必须重写该函数,否则报错;
override 加父名:重写多父合约的同名函数时,override后需加所有父合约名,格式override(父1, 父2)。
contract Erzi is Yeye, Baba{
// 继承两个function: hip()和pop(),输出值为Erzi。
function hip() public virtual override(Yeye, Baba){
emit Log("Erzi");
}
function pop() public virtual override(Yeye, Baba) {
emit Log("Erzi");
}
}
6.3修饰器的继承
修饰器的继承规则与函数完全一致,支持直接使用和重写两种方式:
Identifier合约可以直接在代码中使用父合约中的exactDividedBy2And3修饰器,也可以利用override关键字重写修饰器:
contract Base1 {
modifier exactDividedBy2And3(uint _a) virtual {
require(_a % 2 == 0 && _a % 3 == 0);
_;
}
}
contract Identifier is Base1 {
//计算一个数分别被2除和被3除的值,但是传入的参数必须是2和3的倍数
function getExactDividedBy2And3(uint _dividend) public exactDividedBy2And3(_dividend) pure returns(uint, uint) {
return getExactDividedBy2And3WithoutModifier(_dividend);
}
//计算一个数分别被2除和被3除的值
function getExactDividedBy2And3WithoutModifier(uint _dividend) public pure returns(uint, uint){
uint div2 = _dividend / 2;
uint div3 = _dividend / 3;
return (div2, div3);
}
}
6.4构造函数的继承
构造函数用于初始化状态变量,父合约有参数的构造函数,子合约必须显式继承(无参构造函数自动继承,无需处理),有两种实现方式,任选其一即可:
方式 1:继承时直接传参
声明继承时,在父合约名后加括号传入固定参数,格式contract 子 is 父(固定参数);
方式 2:子构造函数内传参
子合约自定义构造函数,在自身构造函数后加父合约名(参数/参数表达式),支持动态传参;
abstract contract A { // 父合约:有参构造函数
uint public a;
constructor(uint _a) { a = _a; }
}
contract B is A(1) {} // 方式1:继承时传固定参数1
contract C is A { // 方式2:子构造函数动态传参
constructor(uint _c) A(_c * _c) {} // 父构造函数接收_c的平方
}
6.5子合约调用父合约的函数
子合约可主动调用父合约的函数,有两种方式,适用于不同场景:
方式 1:直接调用
通过父合约名.函数名()直接调用,精准指定要调用的父合约版本,无歧义;
方式 2:super 关键字调用
通过super.函数名()调用,普通多重继承中,会调用最近的父合约的函数;
关键:最近的父合约由继承顺序从右到左决定(如contract Erzi is Yeye, Baba,Baba 是最近父合约)。
contract Erzi is Yeye, Baba {
function callDirect() public { Yeye.pop(); } // 直接调用:指定Yeye的pop
function callSuper() public { super.pop(); } // super调用:调用最近父合约Baba的pop
}
7.抽象合约和接口
7.1抽象合约
抽象合约(abstract):半完成的合约模版
核心:可包含状态变量、已实现函数,仅留待实现的函数标virtual,整体加abstract,自身不能部署,子合约override实现未完成函数即可。
// 抽象合约:有状态变量、已实现函数,1个未实现函数
abstract contract Math {
uint public version = 1; // 状态变量
function add(uint a, uint b) public pure returns(uint) { // 已实现函数
return a + b;
}
function mul(uint a, uint b) public pure virtual returns(uint); // 未实现,加virtual
}
// 子合约实现抽象合约
contract MathImpl is Math {
function mul(uint a, uint b) public pure override returns(uint) { // override实现
return a * b;
}
}
7.2接口
接口(interface):纯功能规范,无任何实现
核心:以I开头命名,无状态变量、无构造函数、无函数体,所有函数必标external,实现合约需写所有函数的具体逻辑,是跨合约交互的标准。
// 接口:仅定义函数签名,无任何实现,函数全为external
interface IMath {
function add(uint a, uint b) external pure returns(uint);
function mul(uint a, uint b) external pure returns(uint);
}
// 实现接口的合约:必须实现所有external函数
contract MathImpl is IMath {
function add(uint a, uint b) external pure returns(uint) {
return a + b;
}
function mul(uint a, uint b) external pure returns(uint) {
return a * b;
}
}
// 接口的核心用法:无源码跨合约调用(假设已部署MathImpl,已知地址)
contract CallMath {
IMath math = IMath(0x123456...); // 用地址创建接口实例
function callAdd(uint a, uint b) external view returns(uint) {
return math.add(a, b); // 直接调用,无需知道MathImpl源码
}
}
8.异常
8.1自定义 error
需先定义后使用(可合约外定义),支持无参 / 带参提示,gas 消耗最低,兼顾可读性和经济性。
// 1. 定义自定义异常(无参/带参二选一)
error TransferNotOwner(); // 无参异常
error TransferNotOwner2(address sender); // 带参异常(提示错误发起地址)
// 2. 函数内用revert抛出异常
function transferByError(uint256 tokenId, address newOwner) public {
if (_owners[tokenId] != msg.sender) {
revert TransferNotOwner(); // 抛无参异常
// revert TransferNotOwner2(msg.sender); // 抛带参异常,可传参调试
}
_owners[tokenId] = newOwner;
}