2024-02-13
Web3
00

目录

HelloWorld
变量类型
数值类型
地址类型address
定长字节数组
枚举类型
函数类型
引用类型
数据位置
数据位置与赋值规则
变量的作用域
数组Array
结构体struct
映射类型
变量的初始值
常数
控制流
构造函数和装饰器
事件
继承
抽象合约和接口
异常

本文是基于WTF学院的Solidity学习笔记, 推荐还是去官网学习, 教程写的很友好,章节划分很有节奏,学完一节还可以做题检测,我这里只是摘抄一部分内容,仅为了个人学习与掌握,不推荐阅读。

HelloWorld

  • Solidity是以太坊虚拟机(EVM)智能合约的语言。
  • Solidity语言的开发工具是remix,remix是以太坊官方推荐的智能合约开发IDE,你也可以使用在线IDE
  • 安装了remixIDE后有三个教程 Remix的基础使用 Solidity入门介绍 在Remix中部署,建议新手体验一下。

HelloWorld

solidity
// SPDX-License-Identifier: MIT pragma solidity ^0.8.4; contract HelloWeb3 { string public _string = "Hello Web3!"; }

从helloWorld中可以看出

  • 注释 单行注释 // 多行注释 /* */
  • 通过注释声明软件协议
  • pragma solidity ^0.8.4; 声明软件版本,采用语意版本号
  • cantract 定义一个智能合约, 采用面向对象的编程范式
  • string public _string = 'Hello Web3'; 定义成员变量,与java语法很像,分号结束一条语句。

变量类型

Solidity中的变量类型有四大分类:数值类型、函数类型、引用类型、映射类型

  1. 数值类型Value Type 包括布尔型、整数型、无符号整型、地址类型
  2. 引用类型Reference Type 包含数组和结构体,这类变量占空间大,赋值的时候直接传递地址(类似指针)
  3. 映射类型MappingType Solidity里的哈希表
  4. 函数类型Function Type Solidity文档里把函数归到数值类型,但实际区别比较大

运算符与JavaScript一致,区别是Solidity是强类型语言,所有没有 ===!==运算符

数值类型

包括布尔型、整数型、无符号整型、地址类型

地址类型address

地址类型存储一个20字节的值。地址类型也有成员变量,并作为所有合约的基础。有普通的地址和可转账ETH的地址payable。其中payable修饰的地址相对普通地址多了transfersend两个成员。

payable修饰的地址中,send执行失败不会影响当前合约的执行,但是返回false值需要开发人员检查send的返回值

定长字节数组

字节数组bytes分两种,一种定长(byte、bytes8、bytes32),另一种不定长。定长的属于数值类型,不定长的是引用类型。定长字节数组消耗的gas比较少

solidity
bytes32 public _byte32 = "MiniSolidity"; // 0x4d....0000 bytes1 public _byte = _byte32[0]; // 0x4d

枚举类型

枚举 是solidity中用户定义的数据类型,它主要用于为unit分配名称,使程序易于阅读和维护。它与TypeScript中的enum类似

solidity
enum ActionSet { Buy, Hold, Sell } // 不能加; ActionSet action = ActionSet.Buy;

它可以显示的转换为 unit类型 示例 unit(action)

函数类型

形式: function <functionName>(<parameter types>) {internal|external|public|private [pure|view|payable] [returns (<returnTypes>)]}

  • {internal|external|public|private} 函数的可见性,一共有4种,默认是public
    • public 内部外部均可见
    • private 只能从合约内部访问,继承的合约也不能用
    • external 只能从合约外部访问
    • internal 只能从合约内部访问,继承的合约可以用
  • [pure|view|payable] 决定函数权限/功能的关键字。
    • payable 可支付的,可以给合约转入ETH. 默认值
    • pureview与汽油费有关
    • pure 不能读不能写
    • view 能读取但不能写入状态变量
  • [returns ()] 函数返回的变量类型和名称。

函数输出

solidity
// 命名式返回 function returnNamed() public pure returns( uint256 _number, bool _bool, uint256[3] memory _array ) { _number = 2; _bool = false; _array = [uint256(3),2,1]; } // 命名式返回,依然支持return function returnNamed2() public pure returns( uint256 _number, bool _bool ) { return (1, true); }

解构式赋值

solidity
function getVal() public payable { uint256 _number; bool _bool; uint256[3] memory _array; (_number, _bool, _array) = returnNamed(); }

引用类型

引用类型,包括数组、结构体和映射,这三类变量占空间大,赋值的时候直接传递地址(类似指针)。由于这类变量比较复杂,占用存储空间大,我们在使用时必须声明数据存储的位置。

数据位置

solidity数据存储的位置有三类: storagememorycalldata。不同的存储位置的gas成本不同。

  • storage 数据存储在链上,类似计算机硬盘,消耗gas多; 合约中的状态变量默认都是storage
  • memory 函数里的参数和临时变量一般用memory,存储在内存中,消耗gas
  • calldatamemory类似,存储在内存中,区别是calldata 变量不能修改 immutable,一般用于函数的参数。

数据位置与赋值规则

在不同存储类型相互赋值时,有时产生独立的副本,有时产生引用。规则如下

  1. storage (合约的状态变量) 赋值给本地storage(函数里)的时候,会创建引用,改变新变量会影响原变量。
solidity
uint[] x = [1,2,3]; // 状态变量:数组 x function fStorage() public{ //声明一个storage的变量 xStorage,指向x。修改xStorage也会影响x uint[] storage xStorage = x; xStorage[0] = 100; }
  1. storage 赋值给memory,会创建独立的副本,修改其中一个不会影响另一个,反之亦然。
  2. memory赋值给memory,会创建引用,改变新变量会影响原变量
  3. 其它情况,变量赋值给storage, 会创建独立的副本,求改其中一个不会影响另一个

总结, storagememory相互赋值会创建副本,其它情况时引用类型赋值

变量的作用域

  1. 状态变量
    • 在合约内函数外声明,gas消耗高
  2. 局部变量
    • 仅在函数执行过程中有效的变量,函数退出后,变量无效。局部变量的数据存储在内存里,不上链,gas低。局部变量在函数内声明
  3. 全局变量
    • 全局变量时全局范围工作的变量,都是solidity预留的关键字。
    • 他们可以在函数内不声明直接使用
solidity
function global() external view returns( address, uint, bytes memory ) { address sender = msg.sender; uint blockNum = block.number; bytes memory data = msg.data; return(sender, blockNum, data); }

下面是一些常用的全局变量,更完整的列表请看这个

  • blockhash(uint blockNumber): (bytes32)给定区块的哈希值只适用于256最近区块, 不包含当前区块。
  • block.coinbase: (address payable) 当前区块矿工的地址
  • block.gaslimit: (uint) 当前区块的gaslimit
  • block.number: (uint) 当前区块的number
  • block.timestamp: (uint) 当前区块的时间戳,为unix纪元以来的秒
  • gasleft(): (uint256) 剩余 gas
  • msg.data: (bytes calldata) 完整call data
  • msg.sender: (address payable) 消息发送者 (当前 caller)
  • msg.sig: (bytes4) calldata的前四个字节 (function identifier)
  • msg.value: (uint) 当前交易发送的wei值

数组Array

数组是Solidity常用的一种变量类型,用来存储一组数据(整数、字节、地址等等)。数组分为固定长度数组和可变长度数组两种

  1. 固定长度数组

T[K]的格式声明,其中T是元素的类型,k是长度。

solidity
// 固定长度 Array uint[8] array1; bytes1[5] array2; address[100] array3;
  1. 可变长度数组(动态数组)

在声明时不指定数组长度。用T[]的格式声明

solidity
// 可变长度 Array uint[] array4; bytes1[] array5; address[] array6; bytes array7; // 特殊情况

注意:bytes比较特殊,是数组,但不用加[]。另外,不能用byte[]声明单字节数组,可以使用bytesbytes1[]。在gas上,bytesbytes1[]便宜。因为bytes1[]memory中要增加31个字节进行填充,会产生额外的gas。但是在storage中,由于内存紧密打包,不存在字节填充。

创建数组的规则

  1. 对于memory修饰的动态数组,可以用new操作符来创建,但是必须声明长度,并且声明后的长度不能改变
  2. 数组字面常数
    • 例如[1,2,3]里面所有的元素都是uint8类型,因为在solidity中如果一个值没有指定type的话,默认就是最小单位的该type,这里int的默认最小单位类型就是uint8
    • [uint(1),2,3]里面的元素都是uint类型,因为第一个元素指定了是uint类型了,我们都以第一个元素为准。
    • 如果创建的是动态数组,你需要一个一个元素的赋值
solidity
uint[] memory x = new uint[](2); x[0] = 1; x[1] = 3;

数组成员属性和方法

  • length memory数组的长度在创建后是固定的
  • push()
  • push(x)
  • pop()

结构体struct

solidity
// 结构体 struct Student{ uint256 id; uint256 score; } Student student; // 初始一个student结构体

给结构体赋值有两种方式

solidity
function initStudent1() external{ Student storage _student = student; // 引用 _student.id = 11; _student.score = 100; } // 2 直接赋值 function initStudent2() external{ student.id = 11; student.score = 100; }

映射类型

声明映射类型的格式mapping(_KeyType => _ValueType)

solidity
mapping(uint => address) public idToAddress; // id映射到地址 mapping(address => address) public swapPair; // 币对的映射,地址到地址

映射的规则:

  1. _KeyType 只能是solidity默认的类型,_ValueType类型可以是任意类型,比如结构体
  2. 映射的存储位置必须是storage,因此可以用于合约的状态变量,函数中的storage变量,和library函数的参数。不能用于public函数的参数或返回结果中,因为mapping记录到的是一种关系(key - value pair)
  3. 如果映射类型声明为public,那么solidity会自动给你创建一个getter函数,可以通过Key来查询对应的Value
  4. 给映射新增的键值对语法为 _Var[_Key] = _Value

映射的原理

  1. 映射不储存任何键(Key)的资讯,也没有length的资讯。
  2. 映射使用keccak256(key)当成offset存取value。
  3. 因为Ethereum会定义所有未使用的空间为0,所以未赋值(Value)的键(Key)初始值都是各个type的默认值,如uint的默认值是0。

变量的初始值

solidity
contract InitialVal { /* 值类型的初始值 */ bool public _bool; // false string public _string; // "" int public _int; // 0 uint public _uint; // 0 address public _address; // 0x0000000000000000000000000000000000000000 (或 address(0)) enum ActionSet { Buy, Hold, Sell} ActionSet public _enum; // 枚举中的第一个元素 function fi() internal{} // internal空白方程 function fe() external{} // external空白方程 /* 引用类型的初始值 */ uint[8] public _staticArray; // 所有成员设为其默认值的静态数组[0,0,0,0,0,0,0,0] uint[] public _dynamicArray; // `[]` mapping(uint => address) public _mapping; // 所有元素都为其默认值的mapping // 所有成员设为其默认值的结构体 0, 0 struct Student{ uint256 id; uint256 score; } Student public student; }

delete操作符 会让变量变为初始值

常数

  • constant变量必须在声明的时候初始化,之后再也不能改变。否则编译不通过
  • immutable 变量可以在声明时或构造函数中初始化,因此更加灵活
solidity
// 报错 string immutable x7 = "hello world";

控制流

solidity的控制流和其它语言比较类似

  • if else
  • for循环 continue break
  • while循环
  • do while循环
  • 三元运算符

写一个冒泡排序

solidity
function bubbleSort(uint[] memory a) public pure returns(uint[] memory) { for(uint i = a.length-1; i>=0;i--) { bool hasChange = false; for(uint j = 0; j<i; j++) { if(a[j] > a[j+1]) { uint temp = a[j+1]; a[j+1] = a[j]; a[j] = temp; hasChange = true; } } // 冒泡排序剪枝优化 if(!hasChange) { break; } } return (a); }

构造函数和装饰器

构造函数

  • 构造函数(constructor)是一种特殊的函数,每个合约可以定义一个,并在部署合约的时候自动运行一次
  • 它可以用来初始化合约的一些参数,例如初始化合约的owner地址
  • 在Solidity 0.4.22之前使用与合约名同名的函数作为构造函数而使用

装饰器

  • 修饰器(modifier)是solidity特有的语法,类似于面向对象编程中的decorator
  • 作用 运行函数前的检查声明函数拥有的特性并减少代码冗余
solidity
// SPDX-License-Identifier: MIT pragma solidity ^0.8.4; contract Owner { address public owner; // 定义Owner变量 constructor() { owner = msg.sender; // 在部署合约的时候将owner设置为部署者的地址 } modifier onlyOwner { require(msg.sender == owner); // 检查调用者是否为owner地址 _; // 如果是,继续运行函数主体,否则报错并revert交易 } function changeOwner(address _newOwner) external onlyOwner { owner = _newOwner; } }

事件

Solidity中的事件是EVM上日志的抽象,它具有两个特点

  • 响应:应用程序ethers.js可以通过RPC接口订阅和监听这些事件,并在前端做响应
  • 经济:事件是EVM 上比较经济的存储数据的方式,每个大概消耗2000gas;相比之下链上存储一个新变量至少需要20000gas

声明事件

event Transfer(address indexed from, address indexed to, uint256 value);

触发事件

emit Transfer(from, to, amount)

EVM日志: 每条日志都包含TopicsData两部分

事件可以在etherscan上查询

继承

可以把合约看成对象,solidity也是面向对象的编程,也支持继承。

规则

  • 合约之间通过is继承
  • virtual 父合约中的函数,如何希望子合约重写,需要加上virtual关键字。
  • override 子合约重写父合约中的函数,需要加上override关键字。
    • 注意: 用override修饰public变量,会重写与变量同名的getter函数
  • 支持多重继承
    • 继承时要按辈分从高到低排
    • 如果一个函数在多个继承的合约里都存在,则在子合约里必须重写,不然报错。
    • 重写父合约中的函数时,override关键字要加上所有父合约的名字
  • 支持修饰器的继承
  • 构造函数的继承
    • 子合约在继承时声明父合约构造函数参数
    • 子合约构造函数声明父合约构造函数参数
  • 调用父合约中的函数
    • 直接调用父合约名.函数名()
    • super关键字 super.函数名()
      • 多继承按照从右到左的顺序调用
  • 砖石继承
solidity
// SPDX-License-Identifier: MIT pragma solidity ^0.8.4; contract Yeye { event Log(string msg); function hip() public virtual { emit Log("Yeye"); } function pop() public virtual{ emit Log("Yeye"); } function yeye() public virtual { emit Log("Yeye"); } } // 合约通过 is继承 contract Baba is Yeye { function hip() public virtual override { emit Log("Baba"); } function pop() public virtual override { emit Log("Baba"); } function baba() public virtual { emit Log("Baba"); } } // 多继承的顺序是按照辈分从高到低 contract Erzi is Yeye, Baba { function hip() public virtual override(Yeye, Baba) { emit Log("Erzi"); } function pop() public virtual override(Yeye, Baba) { emit Log("Erzi"); } } /* 验证多重继承 父函数的方法必须加 virtual 子合约函数必须继承所有父类同一重名的方法 */ contract P1 { function test() public virtual {} } contract P2 { function test() public virtual{} } contract C is P1, P2 { function test() public override(P1, P2){} } // 修饰器的继承 contract Base1 { modifier exactDividedBy2And3(uint _a) virtual { require(_a % 2 == 0 && _a % 3 == 0); _; } } contract Identifier is Base1 { modifier exactDividedBy2And3(uint _a) virtual override { _; require(_a % 2 == 0 && _a % 3 == 0); } function test(uint a) public exactDividedBy2And3(a) {} } // 构造函数的继承 abstract contract A { constructor(uint a) {} } // 1. 在继承时声明父构造函数的参数 contract B is A(1) {} // 2. 在子合约的构造函数中声明构造函数的参数 contract C1 is A { constructor(uint a) A(a * a){} } // 砖石继承 /* 继承树: God / \ Adam Eve \ / people */ contract God { event Log(string message); function bar() public virtual { emit Log("God.bar called"); } } contract Adam is God { function bar() public virtual override { emit Log("Adam.bar called"); super.bar(); } } contract Eve is God { function bar() public virtual override { emit Log("Eve.bar called"); super.bar(); } } contract people is Adam, Eve { function bar() public override(Adam, Eve) { super.bar(); } } // 调用合约people中的super.bar()会依次调用Eve、Adam,最后是God合约

抽象合约和接口

抽象合约

  • TypeScript类似,Solidity语言使用abstract关键字定义抽象合约
  • 如果一个智能合约里至少有一个未实现的函数,则必须将该合约标记为abstract

接口

接口类似于抽象合约,但是它不能实现任何功能,接口的规则如下

  1. 不能包含状态变量
  2. 不能包含构造函数
  3. 不能继承除接口外的其它合约
  4. 所有函数都必须是enternal且不能有函数体
  5. 继承接口的合约必须实现接口定义的所有功能

虽然接口不实现任何功能,但它非常重要。它是智能合约的骨架,定义了合约的功能以及如何触发它们:如果智能合约实现了某种接口,其它Dapps和智能合约就知道如何与它交互。因为接口提供了两个重要的信息:

  • 合约里每个函数的bytes4选择器,以及函数签名函数名(每个参数类型)
  • 接口id

另外,接口与合约ABI登记,可以相互转换: 编译接口可与得到合约的ABI, 利用 abi-to-sol工具也可以将ABIjson 文件转换为接口sol文件

异常

智能合约经常会出bug solidity中的异常命令帮助我们debug

  • Error ^0.8.4
    • error必须搭配revert使用
    • 支持携带参数
  • require ^0.8.0 require(检查条件, "异常的描述")
    • 缺点是gas随着描述一场的字符串长度增加, 比error要高
  • assert
    • 缺点是不能解释异常的原因

gas费用消耗 error < assert < require

本文作者:郭郭同学

本文链接:

版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!