变量类型
- 数值类型(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() 接收函数。
|