Solidity基础

4 阅读1分钟

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:合约中定义的函数需要明确指定可见性,它们没有默认值。 注意 2public|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

变量的作用域

  1. 状态变量
  • 声明位置:合约内、函数外
  • 存储位置:默认storage(链上)
  • 特性:全合约函数可访问,gas 消耗高,永久保存
  • 操作:可在函数内直接修改值
  1. 局部变量
  • 声明位置:函数内
  • 存储位置:默认memory(内存)
  • 特性:仅函数执行时有效,函数退出后销毁,gas 消耗低,不上链
  • 作用:临时计算、存储中间值
  1. 全局变量

本质: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个变量fromtovalue,分别对应代币的转账地址,接收地址和转账数量,其中fromto前面带有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;
}