Solidity 学习笔记
其中solidity是一门专门用于编写智能合约的高级语言,在以太坊虚拟机上运行。它是一种静态类型语言,受 JavaScript、Python 和 C++ 的影响,具有合约、状态变量、函数修饰符和事件等概念,适用于去中心化应用(DApp)和区块链开发。
智能合约的概念
智能合约(Smart Contract)是一种运行在区块链上的自执行合约,其条款以代码形式直接写入区块链。一旦满足设定的条件,合约便会自动执行,且不可篡改。智能合约通常用于去中心化金融(DeFi)、NFT、供应链管理等场景,以实现信任最小化和自动化交易。
1.变量类型
常用三种类型:
1.值类型: 布尔型(bool),整数型(int),正整数(uint)等
2.引用类型: 数组(arry[]),结构体(struct)等
3.映射类型:存储键值对的数据结构(mapping)
详细介绍
1布尔型
布尔型是二值变量,取值为 true
或 false
。
1 | bool san = ture; |
布尔值的运算符包括:
!
(逻辑非)&&
(逻辑与,”and”)||
(逻辑或,”or”)==
(等于)!=
(不等于)
2 整型
solidity中的整数
1 | int public _int = -1; // 整数,包括负数 |
常用的整型运算符包括:
- 比较运算符(返回布尔值):
<=
,<
,==
,!=
,>=
,>
- 算数运算符:
+
,-
,*
,/
,%
(取余),**
(幂)
1 | //整数运算 |
3.地址类型
其中分为两种类型:
普通地址(address): 存储一个 20 字节的值(以太坊地址的大小)。
payable address : 比普通地址多了
transfer
和send
两个成员方法,用于接收转账。
4.字长字节数组
字节数组分为定长和不定长两种:
- 定长字节数组: 属于值类型,数组长度在声明之后不能改变。根据字节数组的长度分为
bytes1
,bytes8
,bytes32
等类型。定长字节数组最多存储 32 bytes 数据,即bytes32
。 - 不定长字节数组: 属于引用类型数组长度在声明之后可以改变,包括
bytes
等(动态数组)
2.函数
四种函数
公共函数
内部函数
外部函数
私有函数
详细介绍
1.公共函数
特点: 可以从合约外部和内部调用,默认可见修饰符。
调用方式: 可以直接在合约内调用。可以从合约外部通过合约地址调用。
限制: 无限制
继承:可以被继承并在子合约中重写
1 | contract Example { |
2.内部函数
特点:只能在当前合约和从当前合约继承的合约中调用,比public函数更加高效,因为他们不会涉及外部调用的开销。
**调用方式:**只能在合约内或继承合约中调用
**限制:**不能从合约外部调用
**继承:**可以被继承并且子合约中调用或被重写
1 | contract Parent { |
3.外部函数
**特点:**只能从合约外部调用。可以通过this关键子从合约外部调用,但是效率低。
**调用方式:**从合约外部通过合约地址调用,使用this关键字调用
**限制:**不能直接在合约内部调用
**继承:**可以被继承,并在子合约中重写
1 | contract Example { |
4.私有函数
**特点:**只能在当前合约内部调用,访问权限最严格。
**调用方式:**只能在合约内部调用
**限制:**不能从合约外部或继承合约中调用。
**继承:**不能被继承,子合约无法访问或者重写。
1 | contract Example { |
总结
public
: 可在任何地方调用,包括合约内部和外部,并可继承。
internal
: 只能在当前合约和继承合约中调用,并可继承。
external
: 只能从合约外部调用,可通过 this
关键字在内部调用,并可继承。
private
: 只能在当前合约内部调用,不能继承。
3.控制流
if语句
1语法:
1 | if (条件) { |
2.if-else
语句
1 | if (条件) { |
3. if-else if-else
语句
1 | if (条件1) { |
for循环(以及其他循环)
1.语法
1 | for (初始化; 条件; 迭代) { |
2.while循环
1 | pragma solidity ^0.8.0; |
3.do-while 循环
1 | pragma solidity ^0.8.0; |
do-while
至少会执行一次,即使初始条件不满足。
4继承
继承规则:
virtual
: 父合约中的函数,如果希望子合约重写,需要加上virtual
关键字。override
:子合约重写了父合约中的函数,需要加上override
关键字。
注意:用override
修饰public
变量,会重写与变量同名的getter
函数,例如:
1 | mapping(address => uint256) public override balanceOf; |
简单继承
我们先写一个简单的爷爷合约Yeye
,里面包含1个Log
事件和3个function
: hip()
, pop()
, yeye()
,输出都是”Yeye”。
1 | contract Yeye { |
我们再定义一个爸爸合约Baba
,让他继承Yeye
合约,语法就是contract Baba is Yeye
,非常直观。在Baba
合约里,我们重写一下hip()
和pop()
这两个函数,加上override
关键字,并将他们的输出改为”Baba”
;并且加一个新的函数baba
,输出也是”Baba”
。
1 | contract Baba is Yeye{ |
我们部署合约,可以看到Baba
合约里有4个函数,其中hip()
和pop()
的输出被成功改写成”Baba”
,而继承来的yeye()
的输出仍然是”Yeye”
。
多重继承
Solidity
的合约可以继承多个合约。
规则:
- 继承时要按辈分最高到最低的顺序排。比如我们写一个
Erzi
合约,继承Yeye
合约和Baba
合约,那么就要写成contract Erzi is Yeye, Baba
,而不能写成contract Erzi is Baba, Yeye
,不然就会报错。(即按照合约的复杂程度大小排序) - 如果某一个函数在多个继承的合约里都存在,比如例子中的
hip()
和pop()
,在子合约里必须重写,不然会报错。 - 重写在多个父合约中都重名的函数时,
override
关键字后面要加上所有父合约名字,例如override(Yeye, Baba)
。
1 | contract Erzi is Yeye, Baba{ |
我们可以看到,Erzi
合约里面重写了hip()
和pop()
两个函数,将输出改为”Erzi”
,并且还分别从Yeye
和Baba
合约继承了yeye()
和baba()
两个函数。
修饰器的继承
Solidity
中的修饰器(Modifier
)同样可以继承,用法与函数继承类似,在相应的地方加virtual
和override
关键字即可。
1 | contract Base1 { |
Identifier
合约可以直接在代码中使用父合约中的exactDividedBy2And3
修饰器,也可以利用override
关键字重写修饰器:
1 | modifier exactDividedBy2And3(uint _a) override { |
构造函数的继承
子合约有两种方法继承父合约的构造函数。举个简单的例子,父合约A
里面有一个状态变量a
,并由构造函数的参数来确定:
1 | // 构造函数的继承 |
在继承时声明父构造函数的参数,例如:
contract B is A(1)
在子合约的构造函数中声明构造函数的参数,例如:
1
2
3contract C is A {
constructor(uint _c) A(_c * _c) {}
}
调用父合约的函数
子合约有两种方式调用父合约的函数,直接调用和利用super
关键字。
直接调用:子合约可以直接用
父合约名.函数名()
的方式来调用父合约函数,例如Yeye.pop()
1
2
3function callParent() public{
Yeye.pop();
}super
关键字:子合约可以利用super.函数名()
来调用最近的父合约函数。Solidity
继承关系按声明时从右到左的顺序是:contract Erzi is Yeye, Baba
,那么Baba
是最近的父合约,super.pop()
将调用Baba.pop()
而不是Yeye.pop()
:1
2
3
4function callParentSuper() public{
// 将调用最近的父合约函数,Baba.pop()
super.pop();
}
钻石继承
在面向对象编程中,钻石继承(菱形继承)指一个派生类同时有两个或两个以上的基类。
在多重+菱形继承链条上使用super
关键字时,需要注意的是使用super
会调用继承链条上的每一个合约的相关函数,而不是只调用最近的父合约。
我们先写一个合约God
,再写Adam
和Eve
两个合约继承God
合约,最后让创建合约people
继承自Adam
和Eve
,每个合约都有foo
和bar
两个函数。
1 | // SPDX-License-Identifier: MIT |
在这个例子中,调用合约people
中的super.bar()
会依次调用Eve
、Adam
,最后是God
合约。
5.抽象合约
抽象合约
如果一个智能合约里至少有一个未实现的函数,即某个函数缺少主体{}
中的内容,则必须将该合约标为abstract
,不然编译会报错;另外,未实现的函数需要加virtual
,以便子合约重写。拿我们之前的插入排序合约为例,如果我们还没想好具体怎么实现插入排序函数,那么可以把合约标为abstract
,之后让别人补写上。
1 | abstract contract InsertionSort{ |
6.接口
- 不能包含状态变量
- 不能包含构造函数
- 不能继承除接口外的其他合约
- 所有函数都必须是external且不能有函数体
- 继承接口的非抽象合约必须实现接口定义的所有功能
作用
接口是智能合约的骨架,定义合约的功能以如何触发他们,
如果智能合约实现了某种接口(比如ERC20
或ERC721
),其他Dapps和智能合约就知道如何与它交互。因为接口提供了两个重要的信息:
- 合约里每个函数的
bytes4
选择器,以及函数签名函数名(每个参数类型)
。 - 接口id
另外,接口与合约ABI
(Application Binary Interface)等价,可以相互转换:编译接口可以得到合约的ABI
,利用abi-to-sol工具,也可以将ABI json
文件转换为接口sol
文件。
我们以ERC721
接口合约IERC721
为例,它定义了3个event
和9个function
,所有ERC721
标准的NFT都实现了这些函数。我们可以看到,接口和常规合约的区别在于每个函数都以;
代替函数体{ }
结尾。
1 | interface IERC721 is IERC165 { |
什么时候使用接口?
如果我们知道一个合约实现了IERC721
接口,我们不需要知道它具体代码实现,就可以与它交互。
无聊猿BAYC
属于ERC721
代币,实现了IERC721
接口的功能。我们不需要知道它的源代码,只需知道它的合约地址,用IERC721
接口就可以与它交互,比如用balanceOf()
来查询某个地址的BAYC
余额,用safeTransferFrom()
来转账BAYC
。
1 | contract interactBAYC { |
抽象合约的例子:
1 | // 定义一个抽象合约 Animal |
Animal
是一个抽象合约,使用 abstract
关键字声明。
Animal
中声明了一个未实现的函数 makeSound()
,以及一个有默认实现的函数 move()
。
Dog
和 Cat
合约分别继承了 Animal
抽象合约,并实现了 makeSound()
函数,分别返回了狗和猫的叫声。
接口的例子:
1 | // 定义一个接口 Animal |
在这个示例中,Animal
是一个接口,声明了一个函数 makeSound()
,没有提供具体的实现。Dog
和 Cat
合约分别实现了 Animal
接口,并提供了 makeSound()
函数的具体实现,分别返回狗和猫的叫声。
区别和使用场景:
- 抽象合约通常用于定义具有部分实现的合约,可以包含实现的函数和未实现的函数。
- 接口是完全未实现的合约,用于定义函数的签名和行为,强制要求实现合约必须提供具体的实现。
7 事件
当触发事件的时候有俩种参数类型:
- indexed参数
- 非indexed参数
最多可以有三个indexed参数 ,也被称为topics(主题);indexed只能用于 uint,address类型的变量;
主题 topics
日志的第一部分是主题数组,用于描述事件,长度不能超过4
。它的第一个元素是事件的签名(哈希)。对于上面的Transfer
事件,它的事件哈希就是:
1 | keccak256("Transfer(address,address,uint256)") |
除了事件哈希,主题还可以包含至多3
个indexed
参数,也就是Transfer
事件中的from
和to
。
indexed
标记的参数可以理解为检索事件的索引“键”,方便之后搜索。每个 indexed
参数的大小为固定的256比特,如果参数太大了(比如字符串),就会自动计算哈希存储在主题中。
数据 data
事件中不带 indexed
的参数会被存储在 data
部分中,可以理解为事件的“值”。data
部分的变量不能被直接检索,但可以存储任意大小的数据。因此一般 data
部分可以用来存储复杂的数据结构,例如数组和字符串等等,因为这些数据超过了256比特,即使存储在事件的 topics
部分中,也是以哈希的方式存储。另外,data
部分的变量在存储上消耗的gas相比于 topics
更少。
**简介:**solidity 中事件是EVM上日志的抽象:
**响应:**应用程序(ethers.js
)可以通过RPC
接口订阅和监听这些事件,并在前端做响应。
**经济:**事件相对于一个新变量更加经济,(一个事件大概2000gas,新变量一个要20000gas)
**声明时间:**由event开头,后面是时间的名称,在括号里面是(变量类型和变量名)
例子:
1 | event Transfer(address indexed from, address indexed to, uint256 value); |
释放事件
我们可以在函数里释放事件。在下面的例子中,每次用_transfer()
函数进行转账操作的时候,都会释放Transfer
事件,并记录相应的变量。
1 | // 定义_transfer函数,执行转账逻辑 |
8.日志
日志(Event)是 Solidity 语言提供的一个重要功能,用于在 EVM 内部记录区块链上的事件。Event 主要用于在链下监听智能合约的状态变化,减少对区块链状态的频繁查询,提高效率。日志由 EVM 处理,并存储在交易的 logs
部分,智能合约本身无法直接访问。
1特点
存储在交易日志中,不占用合约存储空间:事件数据不会占用 storage
,因此 gas 费用更低。
无法在智能合约内部读取:Event 仅用于链外监听(off-chain),不能在智能合约内直接调用。
可以携带索引(indexed):允许最多 三个 indexed 参数,方便链外工具(如 Web3.js、ethers.js)高效检索。
用于链上事件通知:DApp 开发中,前端可以监听事件,以响应合约执行状态的变化。
2.日志的声明与使用
在 Solidity 中,日志通过 event
关键字定义,并使用 emit
关键字触发。
2.1 定义日志
1 | solidity复制编辑// 定义一个事件,记录转账信息 |
说明:
Transfer
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
事件包含 3 个参数:
- `from`(`indexed`):发送方地址
- `to`(`indexed`):接收方地址
- `value`(未 `indexed`):转账金额
- `indexed` 修饰符允许事件数据被高效索引,最多 3 个参数可以使用 `indexed`。
------
#### 2.2 **触发事件**
```solidity
solidity复制编辑function transfer(address _to, uint256 _amount) public {
require(balance[msg.sender] >= _amount, "Insufficient balance");
balance[msg.sender] -= _amount;
balance[_to] += _amount;
// 触发 Transfer 事件
emit Transfer(msg.sender, _to, _amount);
}
说明:
- 事件
Transfer
被emit
关键字触发,并记录在交易日志中。 - 监听器(如前端)可以监听该事件,并更新 UI。
9.内置函数
1 数学运算相关
函数 | 作用 |
---|---|
addmod(uint x, uint y, uint m) | 计算 (x + y) % m ,避免溢出 |
mulmod(uint x, uint y, uint m) | 计算 (x * y) % m ,避免溢出 |
2.哈希计算
函数 | 作用 |
---|---|
keccak256(bytes memory data) | 计算 Keccak-256 哈希值 |
sha256(bytes memory data) | 计算 SHA-256 哈希值 |
ripemd160(bytes memory data) | 计算 RIPEMD-160 哈希值 |
3.地址相关
函数 | 作用 |
---|---|
address(uint160 value) | uint160转换为 address |
uint160(address value) | address转换为 uint160 |
balance | 获取账户余额 |
send(uint256 amount) | 发送以太币(返回 bool ) |
transfer(uint256 amount) | 发送以太币(失败时回滚) |
call(bytes memory data) | 低级调用外部合约 |
4. 区块和交易信息
函数 | 作用 |
---|---|
block.number |
当前区块号 |
block.timestamp |
当前区块的时间戳 |
block.coinbase |
当前区块的矿工地址 |
block.difficulty |
当前区块的难度 |
gasleft() |
剩余的 gas 量 |
msg.sender |
交易发送者地址 |
msg.value |
交易发送的以太币数量 |
msg.data |
调用数据 |
tx.origin |
交易发起者的地址 |
5 ABI 编码和解码
函数 | 作用 |
---|---|
abi.encode(...) |
编码参数为 bytes |
abi.encodePacked(...) |
压缩编码(节省空间) |
abi.decode(bytes memory data, (type1, type2, ...)) |
解码数据 |
abi.encodeWithSelector(bytes4 selector, ...) |
编码带 selector 的调用数据 |
abi.encodeWithSignature(string memory signature, ...) |
编码带函数签名的调用数据 |
6. 合约创建
函数 | 作用 |
---|---|
new ContractName{value: X, gas: Y}(params) |
部署新合约 |
selfdestruct(address payable recipient) |
销毁合约,并转移剩余资金 |
10 以太坊的账户(转账方法
其中以太坊账户分为两类:
- 外部账户(EOA,Externally Owned Account)
- 由私钥控制。
- 通过
msg.sender
发送交易。 - 不能存储代码,仅能发送和接收以太币。
- 合约账户(Contract Account)
- 由智能合约代码控制。
- 具有存储和代码逻辑。
- 由外部账户或合约调用执行。
send call transfer 对比
方法 | 失败时 | Gas 限制 | 是否推荐 |
---|---|---|---|
send() |
返回 false |
2300 | 不推荐(可能导致资金丢失) |
transfer() |
回滚交易 | 2300 | 推荐(安全性较高) |
call() |
返回 (success, data) |
无限制 | 推荐(但需注意重入攻击) |
send()
send()
方法返回 bool
,需要手动检查成功与否:
1 | (bool success,) = payableAddr.send(1 ether); |
⚠️ 不推荐使用,因为它不会自动回滚交易。
transfer()
transfer()
方法失败时会自动回滚:
1 | payableAddr.transfer(1 ether); |
✅ 推荐使用,因为它避免了资金丢失。
call()
call()
是最低级的调用方式,可以传递数据,但容易引发重入攻击:
1 | (bool success, bytes memory data) = payableAddr.call{value: 1 ether}(""); |
⚠️ 推荐 用于复杂合约交互,但需加 reentrancy
保护。(防止重入)
11. 映射(mapping)
mapping
是 Solidity 中的一种哈希表,键值对存储数据,但 无法遍历 或 获取长度。
mapping
语法
1 | solidity复制编辑mapping(address => uint256) public balances; |
基本操作
1 | solidity复制编辑balances[msg.sender] = 100; // 赋值 |
12.数组(Array)
Solidity 数组有 固定长度 和 动态数组 两种。
数组定义
1 | uint256[] public dynamicArray; // 动态数组 |
增删查改
1 | dynamicArray.push(10); // 添加元素 |
注意:固定长度数组不能 push()
或 pop()
。
13.delete 关键字
delete
关键字不会真正删除元素,而是 重置为默认值。- 适用于数组、
mapping
和结构体。
1 | uint256[] public arr = [1, 2, 3]; |
对于 mapping
:
1 | mapping(address => uint256) public balances; |
14.receive 和 fallback
接收ETH的receive函数
其中receive函数实在接收ETH转账时被调用的函数。一个合约最多有一个receive()函数,声明方式直接为receive() external payable { … }。不需要function关键字。receive()函数不能有任何参数,不能有任何返回值,必须包含external和payable。
接受ETH时候, receive()会被触发。
receive()最好不要执行太多的逻辑因为如果别人用
send和
transfer方法发送
ETH的话,
gas会限制在
2300,
receive()太复杂可能会触发
Out of Gas报错;如果用
call就可以自定义
gas`执行更复杂的逻辑(这三种发送ETH的方法我们之后会讲到)。
我们可以在receive()
里发送一个event
,例如:
1 | // 定义事件 |
回退函数 fallback
fallback()
函数会在调用合约不存在的函数时被触发。可用于接收ETH,也可以用于代理合约proxy contract
。fallback()
声明时不需要function
关键字,必须由external
修饰,一般也会用payable
修饰,用于接收ETH:fallback() external payable { ... }
。
我们定义一个fallback()
函数,被触发时候会释放fallbackCalled
事件,并输出msg.sender
,msg.value
和msg.data
:
1 | event fallbackCalled(address Sender, uint Value, bytes Data); |
两者区别
触发fallback() 还是 receive()?
接收ETH
|
msg.data是空?
/
是 否
/
receive()存在? fallback()
/
是 否
/
receive() fallback()
简单来说,合约接收ETH
时,msg.data
为空且存在receive()
时,会触发receive()
;msg.data
不为空或不存在receive()
时,会触发fallback()
,此时fallback()
必须为payable
。
receive()
和payable fallback()
均不存在的时候,向合约直接发送ETH
将会报错(你仍可以通过带有payable
的函数向合约发送ETH
)
15 构造函数和修饰器
构造函数
构造函数(constructor
)是一种特殊的函数,每个合约可以定义一个,并在部署合约的时候自动运行一次。它可以用来初始化合约的一些参数,例如初始化合约的owner
地址:
1 | address owner; // 定义owner变量 |
注意:构造函数在不同的Solidity版本中的语法并不一致,在Solidity 0.4.22之前,构造函数不使用 constructor
而是使用与合约名同名的函数作为构造函数而使用,由于这种旧写法容易使开发者在书写时发生疏漏(例如合约名叫 Parents
,构造函数名写成 parents
),使得构造函数变成普通函数,引发漏洞,所以0.4.22版本及之后,采用了全新的 constructor
写法。
构造函数的旧写法代码示例:
1 | pragma solidity =0.4.21; |
修饰器
修饰器(modifier
)是Solidity
特有的语法,类似于面向对象编程中的装饰器(decorator
),声明函数拥有的特性,并减少代码冗余。它就像钢铁侠的智能盔甲,穿上它的函数会带有某些特定的行为。modifier
的主要使用场景是运行函数前的检查,例如地址,变量,余额等。
我们来定义一个叫做onlyOwner的modifier:
1 | // 定义modifier |
带有onlyOwner
修饰符的函数只能被owner
地址调用,比如下面这个例子:
1 | function changeOwner(address _newOwner) external onlyOwner{ |
我们定义了一个changeOwner
函数,运行它可以改变合约的owner
,但是由于onlyOwner
修饰符的存在,只有原先的owner
可以调用,别人调用就会报错。这也是最常用的控制智能合约权限的方法。
16. 函数重载
重载:
Solidity
中允许函数进行重载(overloading
),即名字相同但输入参数类型不同的函数可以同时存在,他们被视为不同的函数。注意,Solidity
不允许修饰器(modifier
)重载。
函数重载
举个例子,我们可以定义两个都叫saySomething()
的函数,一个没有任何参数,输出"Nothing"
;另一个接收一个string
参数,输出这个string
。
1 | function saySomething() public pure returns(string memory){ |
最终重载函数在经过编译器编译后,由于不同的参数类型,都变成了不同的函数选择器(selector)
17. 库合约
特点(库合约是一种特殊的合约,为了提升Solidity
代码的复用性和减少gas
而存在。库合约是一系列的函数合集,由大神或者项目方创作,咱们站在巨人的肩膀上,会用就行了。)
和普通合约的不同点:
不能存在状态变量
不能够继承或被继承
不能接收以太币
不可以被销毁
Strings库合约
Strings库合约
是将uint256
类型转换为相应的string
类型的代码库,样例代码如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59library Strings {
bytes16 private constant _HEX_SYMBOLS = "0123456789abcdef";
/**
* @dev Converts a `uint256` to its ASCII `string` decimal representation.
*/
function toString(uint256 value) public pure returns (string memory) {
// Inspired by OraclizeAPI's implementation - MIT licence
// https://github.com/oraclize/ethereum-api/blob/b42146b063c7d6ee1358846c198246239e9360e8/oraclizeAPI_0.4.25.sol
if (value == 0) {
return "0";
}
uint256 temp = value;
uint256 digits;
while (temp != 0) {
digits++;
temp /= 10;
}
bytes memory buffer = new bytes(digits);
while (value != 0) {
digits -= 1;
buffer[digits] = bytes1(uint8(48 + uint256(value % 10)));
value /= 10;
}
return string(buffer);
}
/**
* @dev Converts a `uint256` to its ASCII `string` hexadecimal representation.
*/
function toHexString(uint256 value) public pure returns (string memory) {
if (value == 0) {
return "0x00";
}
uint256 temp = value;
uint256 length = 0;
while (temp != 0) {
length++;
temp >>= 8;
}
return toHexString(value, length);
}
/**
* @dev Converts a `uint256` to its ASCII `string` hexadecimal representation with fixed length.
*/
function toHexString(uint256 value, uint256 length) public pure returns (string memory) {
bytes memory buffer = new bytes(2 * length + 2);
buffer[0] = "0";
buffer[1] = "x";
for (uint256 i = 2 * length + 1; i > 1; --i) {
buffer[i] = _HEX_SYMBOLS[value & 0xf];
value >>= 4;
}
require(value == 0, "Strings: hex length insufficient");
return string(buffer);
}
}他主要包含两个函数,
toString()
将uint256
转为string
,toHexString()
将uint256
转换为16进制
,在转换为string
。
如何使用库合约
我们用Strings
库合约的toHexString()
来演示两种使用库合约中函数的办法。
利用using for指令
指令
using A for B;
可用于附加库合约(从库 A)到任何类型(B)。添加完指令后,库A
中的函数会自动添加为B
类型变量的成员,可以直接调用。注意:在调用的时候,这个变量会被当作第一个参数传递给函数:1
2
3
4
5
6// 利用using for指令
using Strings for uint256;
function getString1(uint256 _number) public pure returns(string memory){
// 库合约中的函数会自动添加为uint256型变量的成员
return _number.toHexString();
}通过库合约名称调用函数
1
2
3
4// 直接通过库合约名调用
function getString2(uint256 _number) public pure returns(string memory){
return Strings.toHexString(_number);
}总结:
会用大神写的就可以了。我们只需要知道什么情况该用什么库合约。常用的有:
18.import
import
用法
通过源文件相对位置导入,例子:
1
2
3
4
5
6文件结构
├── Import.sol
└── Yeye.sol
// 通过文件相对位置import
import './Yeye.sol';通过源文件网址导入网上的合约的全局符号,例子:
1
2// 通过网址引用
import 'https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/utils/Address.sol';通过
npm
的目录导入,例子:1
import '@openzeppelin/contracts/access/Ownable.sol';
通过指定
全局符号
导入合约特定的全局符号,例子:1
import {Yeye} from './Yeye.sol';
引用(
import
)在代码中的位置为:在声明版本号之后,在其余代码之前。