2022/07/06 Solidity_Day7

# 2022/06/29 Solidity_Day_No5

### 库

**库的特点:**

1. 只需要在特定的地址部署一次,代码可以通过`EVM`的`DELEGATECALL`特性进行重用
2. 库函数被调用,它的代码在调用合约的上下文中执行,即`this`指向调用合约.每个库都是一段独立的代码,所以它仅能访问调用合约明确提供的状态变量
3. 任何库不可能被销毁
4. 库当中保存在内存类型都是引用传递而不是值传递
5. 没有状态变量
6. 不能够被继承或者继承
7. 不能接收以太币
8. 可以对库函数

**库函数的使用示例:**

`
pragma solidity ^0.8.0;

// 定义一个外部结构体,在调用合约中保存的数据
struct Data {
mapping(uint => bool) flags;
}

// 定义一个库,里面定义插入、移除、合并三个函数
library Set {
// 库函数的一个特性。如果该函数可以被视为对象的方法,则习惯称第一个参数为 `self`
// 该参数是“storage reference”类型,因此在调用中参数传递的只是它的存储地址而不是内容
function insert(Data storage self, uint value) public returns (bool) {
if (self.flags[value]) {
return false; // 已经存在
}
self.flags[value] = true;
return true;
}

function remove(Data storage self, uint value) public returns (bool) {
if (!self.flags[value]) {
return false; // 不存在
}
self.flags[value] = false;
return true;
}

function contains(Data storage self, uint value) public view returns (bool) {
require(self.flags[value], "Not Exist!");
return self.flags[value];
}
}

// 调用 Set.contains,Set.insert 和 Set.remove 都被编译为外部调用
`

**库的使用:**
`
contract Use {
Data knownValues;

function register(uint value) public {
// 不需要库的特定实例就可以调用库函数,
// 因为当前合约就是“instance”。
require(Set.insert(knownValues, value));
}
// 也可以在这个合约中直接访问 knownValues.flags。
}
`

**库中使用内存类型和内部函数来实现自定义类型:**
`
pragma solidity ^0.8.0;

// 定义一个结构体,里面有一个数组,类似hashmap当中的位桶数组
struct bigint {
uint[] lamda;
}

library InnerLibrary {
// 定义结构体可以使用的方法
function fromUint(uint x) internal pure returns (bigint r) {
// 可以直接使用函数声明的返回值(可以理解为go当中的在函数声明时已经给返回值声明了.所以可以直接使用)
r.lamda = new uint[](1);
r.lamda[0] = x;
}

// 定义该库可以具备的两个功能函数
// 根据索引从数组当中取值
function limb(bigint memory a, uint index) internal pure returns (uint) {
return index < a.lamda.length ? a.lamda[index] : 0;
}

// 比较大小,返回两数当中的较大的数
function max(uint a, uint b) private pure returns (uint) {
return a > b ? a : b;
}

function add(bigint memory a, bigint memory b) internal pure returns (bigint memory r) {
// 将两个数组的对应的元素求和并生成一个新的数组
// 创建两个数组当中长度最长的数组
r.lamda = new uint[](max(a.lamda.length, b.lamda.length));
uint carry = 0;
for (uint i = 0; i < r.lamda.length; ++i) {
uint lamdaA = limb(a, i);
uint lamdaB = limb(b, i);
unchecked {
// 定义新数组的每一位索引
r.lamda[i] = lamdaA + lamdaB + carry;

if (lamdaA + lamdaB < lamdaA || (lamdaA + lamdaB == type(uint).max && carry > 0)) {
carry = 1;
}else {
carry = 0;
}
}
}

if (carry > 0) {
// 增长数组,将新数组的最后一位设置为carry
uint[] memory newTempLamda = new uint[](r.lamda.length + 1);
uint i;
for (i = 0; i < r.lamda.length; ++i) {
newTempLamda[i] = r.lamda[i];
}
// 这样做的好处是当执行到最后一轮的时候i的值会 = r.lamda.length,这个长度刚好是newTempLamda数组的最后一位的索引
newTempLamda[i] = carry;
// 将临时数组设置为结构体数组
r.lamda = newTempLamda;
}
}
}
`

**库函数的使用:**
`
contract UseTest {
// 引用库
using InnerLibrary for bigint;

function f() public pure {
bigint memory x = InnerLibrary.fromUint(7);
bigint memory y = InnerLibrary.fromUint(type(uint).max);
bigint memory z = x.add(y);
assert(z.limb(1) > 0);
}

// 这样做的好处是在InnerLibrary当中定义了修改bigint的函数,所有的对bigint结构体的修改都通过library的库函数进行.这些函数都是内部的不会暴露在abi当中
}
`
**可以直接将库类型更改为`address`类型.编译器无法知道库的部署位置,编译器会生成 `__$30bbc0abd4d6364515865950d3e0d10953$__`形式的占位符,该占位符是完整的库名称的keccak256哈希的十六进制编码的34个字符的前缀**

### 库的函数签名和选择器

**以下标识符可以作为函数签名中的类型:**

- 值类型: 非存储的(non-storage)`string`及非存储的`bytes`使用和合约`ABI`中同样的标识符
- `<type>[]`为动态数组,`<type>[M]`中`M`为数组长度
- 非存储的结构体使用完整的命名引用,`C.S`用于`contract C { struct S { ... } }`
- 存储的映射指针使用`mapping(<keyType> => <valueType>) storage`当`<keyType>`和`<valueType>`是映射的键和值类型
- 其他的存储的指针类型使用其对应的非存储类型的类型标识符,但在其后面附加一个空格及`storage`

**选择器:**

- 选择器由签名的Keccak256哈希的前四个字节组成.可以使用`.selector`成员从`Solidity`中获取其值

**选择器示例代码:**
`
pragma solidity ^0.8.0;

library LibraryFunctionSelector {
function f(uint256) external {}
}

contract C {
function g() public pure returns (bytes4) {
return LibraryFunctionSelector.f.selector;
}
}
`

### 库的调用保护

**库调用的特点:**

- 如果库的代码是通过`CALL`来执行,而不是`DELEGATECALL`或者`CALLCODE`那么执行的结果会被回退,,除非是对`view`或者`pure`函数的调用
- `EVM`没有为合约提供检测是否使用`CALL`的直接方式,合约可以使用`ADDRESS`操作码找出正在运行的“位置”.`生成的代码通过比较这个地址和构造时的地址来确定调用模式.`

**由上诉特点可知:库在链上存储的实际代码与编译器输出的`deployedBytecode`的编码是不同的.所以每一次编译以后实际上合约的`bytecode`都会改变**

**`Using For`:**

**`Using For`的特点:**

- 在当前的合约上下里,指令`using A for B`;可用于附加库函数(从库`A`)到任何类型(`B`)作为成员函数.这些函数将接收到调用它们的对象作为它们的第一个参数

**`Using For`的作用域:**

1. 文件或合约内部
2. 合约级

`A`可以是:

- 库或文件级的函数列表(`using {f,g,h,L,t} for uint;`}, 函数被附加到类型
- 库名称(`using L for uint;`),库里所有的函数(`public`和`internal`函数)被附加到类型上

`B`必须是一个显示类型:

- 可以使用`using L for *;`,表示库`L`中的函数被附加在所有类型上
- 指定一个库,库内所有函数都会被加载,即使它们的第一个参数类型与对象的类型不匹配.类型检查会在函数调用和重载解析时执行
- 使用函数列表(`using {f, g, h, L.t} for uint;`),那么类型(`uint`)会隐式的转换为这些函数的第一个参数.即便这些函数中没有一个被调用,检查也会进行

`Using A for B`仅在当前作用域有效.包括在作用域内的所有函数.在合约或模块之外无效

当`using for`指令在文件级别使用,并应用于一个用户定义类型(在用一个文件定义的文件级别的用户类型),`global`关键字可以添加到末尾.函数被附加到使用该类型的任何地方(包括其他文件),而不仅仅是声明处所在的作用域 ---> 作用域得到了提升

**扩展内置类型的示例代码:**

`
pragma solidity ^0.8.0;

library LibraryIn {
function indexOf(uint[] storage self, uint value) public view returns (uint) {
// 判断值是否在数组中的函数
for (uint i = 0; i < self.length; i++) {
if (self[i] == value) {
return i;
}
}
return type(uint).max;
}
}

// 在合约外部调用库函数
using LibraryIn for uint[];

contract C {
using LibraryIn for uint[];
uint[] data;

// 定义功能函数,直接在直接添加在数组尾部
function append(uint value) public {
data.push(value);
}

// 修改指定索引的值
function replace(uint from, uint to) public {
// 调用库函数查找索引
uint index = data.indexOf(from);
if (index == type(uint).max) {
data.push(to);
}else {
data[index] = to;
}
}
}
`

**所有`external`库调用都是实际的`EVM`函数调用.如果传递内存或值类型,都将产生一个副本,即使是`self`变量.引用存储变量或者`internal`库调用是唯一不会发生拷贝的情况.**

posted @ 2022-08-10 18:08  俊king  阅读(39)  评论(0)    收藏  举报