2024-02-13
Web3
00
请注意,本文编写于 145 天前,最后修改于 145 天前,其中某些信息可能已经过时。

目录

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 许可协议。转载请注明出处!