本文是基于WTF学院的Solidity学习笔记, 推荐还是去官网学习, 教程写的很友好,章节划分很有节奏,学完一节还可以做题检测,我这里只是摘抄一部分内容,仅为了个人学习与掌握,不推荐阅读。
Solidity
是以太坊虚拟机(EVM
)智能合约的语言。Solidity
语言的开发工具是remix
,remix
是以太坊官方推荐的智能合约开发IDE,你也可以使用在线IDEremix
IDE后有三个教程 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中的变量类型有四大分类:数值类型、函数类型、引用类型、映射类型
Value Type
包括布尔型、整数型、无符号整型、地址类型Reference Type
包含数组和结构体,这类变量占空间大,赋值的时候直接传递地址(类似指针)MappingType
Solidity
里的哈希表Function Type
Solidity
文档里把函数归到数值类型,但实际区别比较大运算符与JavaScript一致,区别是Solidity是强类型语言,所有没有 ===
、!==
运算符
包括布尔型、整数型、无符号整型、地址类型
address
地址类型存储一个20字节的值。地址类型也有成员变量,并作为所有合约的基础。有普通的地址和可转账ETH的地址payable
。其中payable修饰的地址相对普通地址多了transfer
和send
两个成员。
在payable
修饰的地址中,send执行失败不会影响当前合约的执行,但是返回false值需要开发人员检查send的返回值
字节数组bytes分两种,一种定长(byte、bytes8、bytes32),另一种不定长。定长的属于数值类型,不定长的是引用类型。定长字节数组消耗的gas比较少
soliditybytes32 public _byte32 = "MiniSolidity"; // 0x4d....0000 bytes1 public _byte = _byte32[0]; // 0x4d
枚举 是solidity中用户定义的数据类型,它主要用于为unit
分配名称,使程序易于阅读和维护。它与TypeScript
中的enum
类似
solidityenum 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. 默认值pure
和view
与汽油费有关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); }
解构式赋值
solidityfunction getVal() public payable { uint256 _number; bool _bool; uint256[3] memory _array; (_number, _bool, _array) = returnNamed(); }
引用类型,包括数组、结构体和映射,这三类变量占空间大,赋值的时候直接传递地址(类似指针)。由于这类变量比较复杂,占用存储空间大,我们在使用时必须声明数据存储的位置。
solidity数据存储的位置有三类: storage
、memory
、calldata
。不同的存储位置的gas
成本不同。
storage
数据存储在链上,类似计算机硬盘,消耗gas
多; 合约中的状态变量默认都是storage
。memory
函数里的参数和临时变量一般用memory
,存储在内存中,消耗gas
少calldata
和memory
类似,存储在内存中,区别是calldata
变量不能修改 immutable
,一般用于函数的参数。在不同存储类型相互赋值时,有时产生独立的副本,有时产生引用。规则如下
storage
(合约的状态变量) 赋值给本地storage
(函数里)的时候,会创建引用,改变新变量会影响原变量。solidityuint[] x = [1,2,3]; // 状态变量:数组 x function fStorage() public{ //声明一个storage的变量 xStorage,指向x。修改xStorage也会影响x uint[] storage xStorage = x; xStorage[0] = 100; }
storage
赋值给memory
,会创建独立的副本,修改其中一个不会影响另一个,反之亦然。memory
赋值给memory
,会创建引用,改变新变量会影响原变量storage
, 会创建独立的副本,求改其中一个不会影响另一个总结, storage
与memory
相互赋值会创建副本,其它情况时引用类型赋值
gas
消耗高gas
低。局部变量在函数内声明solidity
预留的关键字。solidityfunction 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)
当前区块的gaslimitblock.number: (uint)
当前区块的numberblock.timestamp: (uint)
当前区块的时间戳,为unix纪元以来的秒gasleft(): (uint256)
剩余 gasmsg.data: (bytes calldata)
完整call datamsg.sender: (address payable)
消息发送者 (当前 caller)msg.sig: (bytes4)
calldata的前四个字节 (function identifier)msg.value: (uint)
当前交易发送的wei值数组是Solidity常用的一种变量类型,用来存储一组数据(整数、字节、地址等等)。数组分为固定长度数组和可变长度数组两种
用T[K]
的格式声明,其中T
是元素的类型,k
是长度。
solidity// 固定长度 Array uint[8] array1; bytes1[5] array2; address[100] array3;
在声明时不指定数组长度。用T[]
的格式声明
solidity// 可变长度 Array uint[] array4; bytes1[] array5; address[] array6; bytes array7; // 特殊情况
注意:bytes
比较特殊,是数组,但不用加[]
。另外,不能用byte[]
声明单字节数组,可以使用bytes
或bytes1[]
。在gas上,bytes
比bytes1[]
便宜。因为bytes1[]
在memory
中要增加31个字节进行填充,会产生额外的gas
。但是在storage
中,由于内存紧密打包,不存在字节填充。
创建数组的规则
[1,2,3]
里面所有的元素都是uint8
类型,因为在solidity
中如果一个值没有指定type
的话,默认就是最小单位的该type
,这里int
的默认最小单位类型就是uint8
[uint(1),2,3]
里面的元素都是uint
类型,因为第一个元素指定了是uint
类型了,我们都以第一个元素为准。solidityuint[] memory x = new uint[](2); x[0] = 1; x[1] = 3;
数组成员属性和方法
length
memory
数组的长度在创建后是固定的push()
push(x)
pop()
solidity// 结构体 struct Student{ uint256 id; uint256 score; } Student student; // 初始一个student结构体
给结构体赋值有两种方式
solidityfunction 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)
soliditymapping(uint => address) public idToAddress; // id映射到地址 mapping(address => address) public swapPair; // 币对的映射,地址到地址
映射的规则:
_KeyType
只能是solidity
默认的类型,_ValueType
类型可以是任意类型,比如结构体storage
,因此可以用于合约的状态变量,函数中的storage
变量,和library
函数的参数。不能用于public函数的参数或返回结果中,因为mapping
记录到的是一种关系(key - value pair)public
,那么solidity
会自动给你创建一个getter
函数,可以通过Key
来查询对应的Value
_Var[_Key] = _Value
映射的原理
soliditycontract 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
循环写一个冒泡排序
solidityfunction 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); }
构造函数
装饰器
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接口订阅和监听这些事件,并在前端做响应声明事件
event Transfer(address indexed from, address indexed to, uint256 value);
触发事件
emit Transfer(from, to, amount)
EVM日志: 每条日志都包含Topics
和Data
两部分
事件可以在etherscan
上查询
可以把合约看成对象,solidity也是面向对象的编程,也支持继承。
规则
is
继承virtual
父合约中的函数,如何希望子合约重写,需要加上virtual
关键字。override
子合约重写父合约中的函数,需要加上override关键字。
override
修饰public
变量,会重写与变量同名的getter
函数父合约名.函数名()
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
接口
接口类似于抽象合约,但是它不能实现任何功能,接口的规则如下
enternal
且不能有函数体虽然接口不实现任何功能,但它非常重要。它是智能合约的骨架,定义了合约的功能以及如何触发它们:如果智能合约实现了某种接口,其它Dapps和智能合约就知道如何与它交互。因为接口提供了两个重要的信息:
bytes4
选择器,以及函数签名函数名(每个参数类型)
。另外,接口与合约ABI
登记,可以相互转换: 编译接口可与得到合约的ABI, 利用 abi-to-sol工具也可以将ABIjson
文件转换为接口sol
文件
智能合约经常会出bug
solidity
中的异常命令帮助我们debug
Error
^0.8.4
error
必须搭配revert
使用require
^0.8.0 require(检查条件, "异常的描述")
assert
gas费用消耗 error < assert < require
本文作者:郭郭同学
本文链接:
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!