变量类型
- 数值类型(Value Type):包括布尔型,整数型等等,这类变量赋值时候直接传递数值。
- 引用类型(Reference Type):包括数组和结构体,这类变量占空间大,赋值时候直接传递地址(类似指针)。
- 映射类型(Mapping Type):?
Solidity里的哈希表。 - 函数类型(Function Type):
Solidity文档里把函数归到数值类型,但我觉得他跟其他类型差别很大,所以单独分一类。
一、数值类型
1.布尔型(bool):true 或者 false
2.整型(uint):uint、uint8、uint16、uint32、uint256
????????整型数值没有负数
3.地址类型(address):地址类型(address)存储一个 20 字节的值(以太坊地址的大小)
4.定长字节数组:字节数组bytes分两种,一种定长(byte,?bytes8,?bytes32),另一种不定长
5.枚举型(enum):枚举(enum)是solidity中用户定义的数据类型
二、引用类型
1.数组(Array):数组(Array)是solidity常用的一种变量类型,用来存储一组数据(整数,字节,地址等等)。数组分为固定长度数组和可变长度数组两种
数组成员:
length: 数组有一个包含元素数量的length成员,memory数组的长度在创建后是固定的。push():?动态数组和bytes拥有push()成员,可以在数组最后添加一个0元素。push(x):?动态数组和bytes拥有push(x)成员,可以在数组最后添加一个x元素。pop():?动态数组和bytes拥有pop()成员,可以移除数组最后一个元素。
2.结构体(struct):Solidity支持通过构造结构体的形式定义新的类型
// 结构体用法
struct Student{
uint256 id;
uint256 score;
}
三、映射类型
Mapping:声明映射的格式为mapping(_KeyType => _ValueType)
映射的规则:
- 规则1:映射的
_KeyType只能选择solidity默认的类型,比如uint,address等,不能用自定义的结构体。 -
规则2:映射的存储位置必须是storage,因此可以用于合约的状态变量,函数中的storage变量。不能用于public函数的参数或返回结果中,因为mapping记录的是一种关系 (key - value pair)。 -
规则3:如果映射声明为public,那么solidity会自动给你创建一个getter函数,可以通过Key来查询对应的Value。 -
规则4:给映射新增的键值对的语法为_Var[_Key] = _Value,其中_Var是映射变量名,_Key和_Value对应新增的键值对
四、函数类型
1.function:声明函数时的固定用法,想写函数,就要以function关键字开头。
2.(<parameter types>):圆括号里写函数的参数,也就是要输入到函数的变量类型和名字。
3.{internal|external|public|private}:函数可见性说明符,一共4种。没标明函数类型的,默认internal。
public: 内部外部均可见。(也可用于修饰状态变量,public变量会自动生成?getter函数,用于查询数值).private: 只能从本合约内部访问,继承的合约也不能用(也可用于修饰状态变量)。external: 只能从合约外部访问(但是可以用this.f()来调用,f是函数名)internal: 只能从合约内部访问,继承的合约可以用(也可用于修饰状态变量)。
4.[pure|view|payable]:决定函数权限/功能的关键字。
pure和view都不需要付gas
- pure:不能读取也不能写入
- view:只能读取
- payable:可支付
5.[returns ()]:函数返回的变量类型和名称。
常量
一、constant
constant变量必须在声明的时候初始化,之后再也不能改变。尝试改变的话,编译不通过。
二、immutable
immutable变量可以在声明时或构造函数中初始化,因此更加灵活。
常用方法
1.修饰器(modifier)
修饰器(modifier)是solidity特有的语法,类似于面向对象编程中的decorator,声明函数拥有的特性,并减少代码冗余。
// 定义modifier
modifier onlyOwner {
require(msg.sender == owner); // 检查调用者是否为owner地址
_; // 如果是的话,继续运行函数主体;否则报错并revert交易
}
2.构造函数(constructor)
构造函数(constructor)是一种特殊的函数,每个合约可以定义一个,并在部署合约的时候自动运行一次。它可以用来初始化合约的一些参数,例如初始化合约的owner地址:
address owner; // 定义owner变量
// 构造函数
constructor() public {
owner = msg.sender; // 在部署合约的时候,将owner设置为部署者的地址
}
3.事件(events)
Solidity中的事件(event)是EVM上日志的抽象,它具有两个特点:
- 响应:应用程序(
ether.js)可以通过RPC接口订阅和监听这些事件,并在前端做响应。 - 经济:事件是
EVM上比较经济的存储数据的方式,每个大概消耗2,000-5,000?gas不等。相比之下,存储一个新的变量至少需要20,000?gas。
事件的声明由event关键字开头,然后跟事件名称,括号里面写好事件需要记录的变量类型和变量名。以ERC20代币合约的Transfer事件为例:
event Transfer(address indexed from, address indexed to, uint256 value);
4.继承(inheritance)
规则
virtual: 父合约中的函数,如果希望子合约重写,需要加上virtual关键字。
override:子合约重写了父合约中的函数,需要加上override关键。
简单继承
contract Baba is Yeye{}
多重继承
contract Erzi is Yeye, Baba{}
修饰器的继承
用法同函数继承
构造函数的继承
- 在继承时声明父构造函数的参数,例如:
contract B is A(1) - 在子合约的构造函数中声明构造函数的参数
5.异常(errors)
error:方便高效省gas
error TransferNotOwner(); // 自定义error,在执行当中,error必须搭配revert(回退)命令使用。
function transferOwner1(uint256 tokenId, address newOwner) public {
if(_owners[tokenId] != msg.sender){
revert TransferNotOwner();
}
_owners[tokenId] = newOwner;
}
require:gas随着描述异常的字符串长度增加
使用方法:require(检查条件,”异常的描述”),当检查条件不成立的时候,就会抛出异常。
我们用require命令重写一下上面的transferOwner函数:
function transferOwner2(uint256 tokenId, address newOwner) public {
require(_owners[tokenId] == msg.sender, "Transfer Not Owner");
_owners[tokenId] = newOwner;
}
assert命令一般用于程序员写程序debug,因为他不能解释抛出异常的原因(比require少个字符串)。他的用法很简单,assert(检查条件),当检查条件不成立的时候,就会抛出异常。
function transferOwner3(uint256 tokenId, address newOwner) public {
assert(_owners[tokenId] == msg.sender);
_owners[tokenId] = newOwner;
}
6.安全数学(SafeMath)
SafeMath用来防止溢出,有四个方法 —?add,?sub,?mul, 以及?div。
using SafeMath for uint256;
uint256 a = 5;
uint256 b = a.add(3); // 5 + 3 = 8
uint256 c = a.mul(2); // 5 * 2 = 10
7.import
solidity支持利用import关键字导入其他源代码中的合约,让开发更加模块化。
// 通过文件相对位置import
import './Yeye.sol';
8.接收ETH
Solidity支持两种特殊的回调函数,receive()和fallback(),他们主要在两种情况下被使用:
- 接收
ETH - 处理合约中不存在的函数调用(代理合约
proxy contract)
接收ETH函数 receive
receive()只用于处理接收ETH。一个合约最多有一个receive()函数,声明方式与一般函数不一样,不需要function关键字:receive() external payable { ... }
receive()函数不能有任何的参数,不能返回任何值,必须包含external和payable。
当合约接收ETH的时候,receive()会被触发。
回退函数 fallback
fallback()函数会在调用合约不存在的函数时被触发。可用于接收ETH,也可以用于代理合约proxy contract。fallback()声明时不需要function关键字,必须由external修饰,一般也会用payable修饰,用于接收ETH:fallback() external payable { ... }。
receive和fallback都能够用于接收ETH,他们触发的规则如下:
触发fallback() 还是 receive()?
接收ETH
|
msg.data是空?
/ \
是 否
/ \
receive()存在? fallback()
/ \
是 否
/ \
receive() fallback()
9.发送ETH
transfer
- 用法是
transfer(发送ETH数额)。 transfer()的gas限制是2300,足够用于转账,但对方合约的fallback()或receive()函数不能实现太复杂的逻辑。transfer()如果转账失败,会自动revert(回滚交易)。
send
- 用法是
send(发送ETH数额)。 send()的gas限制是2300,足够用于转账,但对方合约的fallback()或receive()函数不能实现太复杂的逻辑。send()如果转账失败,不会revert。send()的返回值是bool,代表着转账成功或失败,需要额外代码处理一下。
call
- 用法是
call{value: 发送ETH数额}("")。 call()没有gas限制,可以支持对方合约fallback()或receive()函数实现复杂逻辑。call()如果转账失败,不会revert。call()的返回值是(bool, data),其中bool代表着转账成功或失败,需要额外代码处理一下。
10.call
call?是address类型的低级成员函数,它用来与其他合约交互。它的返回值为(bool, data),分别对应call是否成功以及目标函数的返回值。
call的使用规则
call的使用规则如下:
目标合约地址.call(二进制编码);
其中二进制编码利用结构化编码函数abi.encodeWithSignature获得:
abi.encodeWithSignature("函数签名", 逗号分隔的具体参数)
函数签名为"函数名(逗号分隔的参数类型)"。例如abi.encodeWithSignature("f(uint256,address)", _x, _addr)。
另外call在调用合约时可以指定交易发送的ETH数额和gas:
目标合约地址.call{value:发送ETH数额, gas:gas数额}(二进制编码);
11.ABI编码解码
abi.encode
将给定参数利用ABI规则编码。ABI被设计出来跟智能合约交互,他将每个参数转填充为32字节的数据,并拼接在一起。
abi.decode
abi.decode用于解码abi.encode生成的二进制编码,将它还原成原本的参数。
12.函数选择器(Selector)
?selector定义为函数签名的哈希的前4个字节
函数签名,为"函数名(逗号分隔的参数类型)"。举个例子,mint的函数签名为"mint(address)"。在智能合约中,不同的函数有不同的函数签名,因此我们可以通过函数签名来确定要调用哪个函数。
13.try-catch
在solidity中,try-catch只能被用于external函数或创建合约时constructor(被视为external函数)的调用。基本语法如下:
try externalContract.f() {
// call成功的情况下 运行一些代码
} catch {
// call失败的情况下 运行一些代码
}
其中externalContract.f()时某个外部合约的函数调用,try模块在调用成功的情况下运行,而catch模块则在调用失败时运行。
应用
1.ERC20
IERC20是ERC20代币标准的接口合约,规定了ERC20代币需要实现的函数和事件。
事件
IERC20定义了2个事件:Transfer事件和Approval事件,分别在转账和授权时被释放
/**
* @dev 释放条件:当 `value` 单位的货币从账户 (`from`) 转账到另一账户 (`to`)时.
*/
event Transfer(address indexed from, address indexed to, uint256 value);
/**
* @dev 释放条件:当 `value` 单位的货币从账户 (`owner`) 授权给另一账户 (`spender`)时.
*/
event Approval(address indexed owner, address indexed spender, uint256 value);
函数
IERC20定义了6个函数,提供了转移代币的基本功能,并允许代币获得批准,以便其他链上第三方使用。
totalSupply()返回代币总供给balanceOf()返回账户余额transfer()转账allowance()返回授权额度approve()授权transferFrom()授权转账
2.erc721
IERC721事件
IERC721包含3个事件,其中Transfer和Approval事件在ERC20中也有。
Transfer事件:在转账时被释放,记录代币的发出地址from,接收地址to和tokenid。Approval事件:在授权时释放,记录授权地址owner,被授权地址approved和tokenid`。ApprovalForAll事件:在批量授权时释放,记录批量授权的发出地址owner,被授权地址operator和授权与否的approved。
IERC721函数
balanceOf:返回某地址的NFT持有量balance。ownerOf:返回某tokenId的主人owner。transferFrom:普通转账,参数为转出地址from,接收地址to和tokenId。safeTransferFrom:安全转账(如果接收方是合约地址,会要求实现ERC721Receiver接口)。参数为转出地址from,接收地址to和tokenId。approve:授权另一个地址使用你的NFT。参数为被授权地址approve和tokenId。getApproved:查询tokenId被批准给了哪个地址。setApprovalForAll:将自己持有的该系列NFT批量授权给某个地址operator。isApprovedForAll:查询某地址的NFT是否批量授权给了另一个operator地址。safeTransferFrom:安全转账的重载函数,参数里面包含了data。
3.erc1155
面是ERC1155的元数据接口合约IERC1155MetadataURI:
/**
* @dev ERC1155的可选接口,加入了uri()函数查询元数据
*/
interface IERC1155MetadataURI is IERC1155 {
/**
* @dev 返回第`id`种类代币的URI
*/
function uri(uint256 id) external view returns (string memory);
IERC1155事件
TransferSingle事件:单类代币转账事件,在单币种转账时释放。TransferBatch事件:批量代币转账事件,在多币种转账时释放。ApprovalForAll事件:批量授权事件,在批量授权时释放。URI事件:元数据地址变更事件,在uri变化时释放。
IERC1155函数
balanceOf():单币种余额查询,返回account拥有的id种类的代币的持仓量。balanceOfBatch():多币种余额查询,查询的地址accounts数组和代币种类ids数组的长度要相等。setApprovalForAll():批量授权,将调用者的代币授权给operator地址。。isApprovedForAll():查询批量授权信息,如果授权地址operator被account授权,则返回true。safeTransferFrom():安全单币转账,将amount单位id种类的代币从from地址转账给to地址。如果to地址是合约,则会验证是否实现了onERC1155Received()接收函数。safeBatchTransferFrom():安全多币转账,与单币转账类似,只不过转账数量amounts和代币种类ids变为数组,且长度相等。如果to地址是合约,则会验证是否实现了onERC1155BatchReceived()接收函数。
|