0%

solidity基础16-18

函数重载

重载

solidity中允许函数进行重载(overloading),即名字相同但输入参数类型不同的函数可以同时存在,他们被视为不同的函数。注意,solidity不允许修饰器(modifier)重载。

函数重载

举个例子,我们可以定义两个都叫saySomething()的函数,一个没有任何参数,输出"Nothing";另一个接收一个string参数,输出这个string

1
2
3
4
5
6
7
function saySomething() public pure returns(string memory){
return("Nothing");
}

function saySomething(string memory something) public pure returns(string memory){
return(something);
}

==最终重载函数在经过编译器编译后,由于不同的参数类型,都变成了不同的函数选择器(selector)。关于函数选择器的具体内容可参考Solidity极简入门: 29. 函数选择器Selector。==

Overloading.sol 合约为例,在 Remix 上编译部署后,分别调用重载函数 saySomething()saySomething(string memory something),可以看到他们返回了不同的结果,被区分为不同的函数。

img

实参匹配(Argument Matching)

在调用重载函数时,会把输入的实际参数和函数参数的变量类型做匹配。 如果出现多个匹配的重载函数,则会报错。下面这个例子有两个叫f()的函数,一个参数为uint8,另一个为uint256

1
2
3
4
5
6
7
function f(uint8 _in) public pure returns (uint8 out) {
out = _in;
}

function f(uint256 _in) public pure returns (uint256 out) {
out = _in;
}

==我们调用f(50),因为50既可以被转换为uint8,也可以被转换为uint256,因此会报错。==

总结

这一讲,我们介绍了solidity中函数重载的基本用法:名字相同但输入参数类型不同的函数可以同时存在,他们被视为不同的函数。

==solidity不允许修饰器(modifier)重载。==

最终重载函数在经过编译器编译后,由于不同的参数类型,都变成了不同的函数选择器(selector)

函数重载有什么用

函数重载(Function Overloading)是一种编程技术,允许在同一个作用域内创建多个同名函数,但这些函数的参数列表(参数类型或参数数量)不同。在Solidity智能合约编程中,函数重载用于提供多个具有相同名称但接受不同参数的函数版本,这使得合约的功能更加灵活和强大。函数重载的主要用途包括:

提高代码的可读性和易用性

通过使用相同的函数名来执行类似的操作,但针对不同的参数类型或数量,可以使代码更容易理解和使用。调用者可以根据他们提供的参数类型和数量,选择最合适的函数版本执行。

实现多态

函数重载是多态的一种形式,允许相同的函数调用根据调用时提供的参数类型或数量,映射到不同的函数实现上。这增加了合约的灵活性,允许开发者根据不同的使用场景优雅地处理不同类型的数据。

简化函数命名

开发者不需要为执行相似操作的不同函数版本想出不同的名字,这样可以避免在合约接口中出现大量几乎相同功能的函数,只是因为它们的参数类型或数量不同。例如,一个处理支付的函数,无论是接收一个地址和金额,还是接收一个地址数组和金额数组,都可以命名为pay,只是参数不同。

代码组织

在某些情况下,函数重载可以帮助组织和模块化代码。例如,你可能有一个基础操作的简单版本和一个支持更多自定义选项的高级版本。通过重载,可以保持这些函数在逻辑上紧密相关,而不是分散在合约中。

示例

假设你有一个资产交易的智能合约,你可能需要一个transfer函数来处理不同类型的资产转移,如下所示:

  • transfer(address to, uint256 amount):转移代币到指定地址。
  • transfer(address to, uint256 tokenId):转移特定ID的NFT到指定地址。

在这个例子中,transfer函数被重载来处理不同类型的资产,使得同一合约能够以统一的方式处理多种转账操作。

总的来说,函数重载增加了Solidity合约的表达能力,使开发者能够以更直观和灵活的方式设计合约接口。

面向对象编程(Object-Oriented Programming, OOP)的三大特点是:

  1. 封装(Encapsulation)

    • 封装是指将数据(属性)和操作数据的方法(方法或函数)捆绑在一起形成类(Class)的过程。
    • 类通过暴露有限的接口来与外部交互,隐藏了内部的实现细节,从而提高了安全性和可维护性。
    • 封装有助于实现信息隐藏,使得对象的状态只能通过定义的接口进行访问和修改。
  2. 继承(Inheritance)

    • 继承是指一个类(子类或派生类)可以继承另一个类(父类或基类)的属性和方法。
    • 子类可以通过继承从父类中获得已有的功能,同时可以添加新的功能或重写已有的方法。
    • 继承提高了代码的重用性和扩展性,减少了重复编码的工作量。
  3. 多态(Polymorphism)

    • 多态是指同一个方法名可以在不同的对象上有不同的实现。
    • 多态性允许以统一的方式处理不同类型的对象,通过向上转型和动态绑定实现。
    • 多态性提高了代码的灵活性和可扩展性,使得程序更加易于理解和维护。

这三个特点共同构成了面向对象编程的核心思想,使得程序设计更加模块化、可重用和易于理解。

库合约

这一讲,我们用ERC721的引用的库合约String为例介绍solidity中的库合约(library),并总结了常用的库函数。

库函数

库函数是一种特殊的合约,为了提升solidity代码的复用性和减少gas而存在。库合约一般都是一些好用的函数合集(库函数),由大神或者项目方创作,咱们站在巨人的肩膀上,会用就行了。

库合约:站在巨人的肩膀上

==

他和普通合约主要有以下几点不同:

  1. 不能存在状态变量
  2. 不能够继承或被继承
  3. 不能接收以太币
  4. 不可以被销毁

==

Strings库合约

==注意Strings库合约跟string类型不一样==

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
59
library 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转为stringtoHexString()uint256转换为16进制,再转换为stringtoHexString是重载函数,有两个函数

如何使用库合约

我们用String库函数的toHexString()来演示两种使用库合约中函数的办法。

1. 利用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();
}

2. 通过库合约名称调用库函数

1
2
3
4
// 直接通过库合约名调用
function getString2(uint256 _number) public pure returns(string memory){
return Strings.toHexString(_number);
}

我们部署合约并输入170测试一下,两种方法均能返回正确的16进制string “0xaa”。证明我们调用库函数成功!

成功调用库函数

总结

这一讲,我们用ERC721的引用的库函数String为例介绍solidity中的库函数(Library)。99%的开发者都不需要自己去写库合约,会用大神写的就可以了。我们只需要知道什么情况该用什么库合约。常用的有:

  1. Strings:将uint256转换为String
  2. Address:判断某个地址是否为合约地址
  3. Create2:更安全的使用Create2 EVM opcode
  4. Arrays:跟数组相关的库函数

是的,使用库(Libraries)函数在某些情况下可以帮助减少智能合约的Gas消耗。库在Solidity中是一种特殊类型的合约,旨在被部署一次,然后由其他合约通过委托调用(delegatecall)来复用其函数。这种设计可以在多个合约之间共享代码逻辑,从而减少重复代码的部署,进一步减少Gas消耗。以下是库函数减少Gas消耗的几种方式:

1. 代码复用

通过将常用的逻辑和函数放在库中,多个合约可以共享这些代码,而不是在每个合约中重复相同的代码。这减少了链上存储的总量,因为库代码只需要部署一次,而不是与每个合约一起部署。

2. 优化存储访问

库可以用来优化数据的存储和访问模式。例如,Solidity的SafeMath库提供了安全的数学运算,防止溢出,而不增加额外的存储开销。通过优化数据处理,可以减少执行操作所需的Gas。

3. 减少部署成本

库的部署是独立于使用它的合约进行的。一旦库被部署,任何合约都可以通过委托调用来访问其函数,而不需要再次部署库代码。这意味着合约的部署成本降低了,因为合约体积变小。

4. 简化合约代码

通过将复杂逻辑移至库中,合约代码本身变得更简单、更清晰。这不仅有助于减少直接的Gas消耗,而且还使得合约更容易审核,减少了因错误或低效率代码导致的潜在Gas浪费。

==然而,值得注意的是,使用库并不总是导致Gas成本的减少。委托调用(delegatecall)本身是有成本的,并且如果库函数相对简单,那么使用库可能不会带来显著的Gas节省。因此,是否使用库以及如何使用库,应该根据具体情况和需求来决定,以确保能有效优化Gas消耗。==

Import

solidity支持利用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';

OpenZeppelin 是一个在区块链开发领域非常知名的库,专门为智能合约开发提供安全的基础设施。它主要用于构建在以太坊和其他兼容EVM(以太坊虚拟机)的区块链上的智能合约。OpenZeppelin 的库和工具广泛应用于实现代币发行(如 ERC-20 和 ERC-721 代币标准)、访问控制、安全合约升级以及各种金融工具。

OpenZeppelin 主要特点包括:

  1. 安全性:提供经过严格审核和社区验证的智能合约实现,帮助开发人员避免常见的安全问题,如重入攻击、整数溢出等。

  2. 可复用性:通过提供标准化的合约模板(如代币合约、所有权管理合约等),简化了开发流程并提高了代码的可复用性。

  3. 易于集成:OpenZeppelin 的合约可以轻松集成到任何基于Solidity的项目中,支持通过 npm 安装,方便与其他项目或库结合使用。

  4. 社区支持:拥有活跃的开发社区和广泛的用户基础,为开发者提供问题解答、最新的安全实践和持续的库更新。

  5. 升级插件:提供了升级插件支持智能合约的透明升级,允许开发者修复、更新和改进已部署的合约。

使用场景示例:

  • 创建代币:OpenZeppelin 提供了多种代币合约实现,如 ERC-20(标准化代币)、ERC-721(非同质化代币,即NFT)、ERC-1155(多代币标准)等。

  • 访问控制:提供了多种访问控制机制,如 OwnableRoles,允许合约的创建者控制谁可以调用合约的特定功能。

  • 金融工具:例如创建可在特定条件下执行的智能合约,如基于时间的锁定或投票机制。

  • 通过全局符号导入特定的合约,例子:

1
import {Yeye} from './Yeye.sol';
  • 引用(import)在代码中的位置为:在声明版本号之后,在其余代码之前。

测试导入结果

我们可以用下面这段代码测试是否成功导入了外部源代码:

1
2
3
4
5
6
7
8
9
10
11
contract Import {
// 成功导入Address库
using Address for address;
// 声明yeye变量
Yeye yeye = new Yeye();

// 测试是否能调用yeye的函数
function test() external{
yeye.hip();
}
}

result

总结

这一讲,我们介绍了利用import关键字导入外部源代码的方法。通过import关键字,可以引用我们写的其他文件中的合约或者函数,也可以直接导入别人写好的代码,非常方便。

在Solidity中,import语句用于导入其他源文件中定义的全局符号,这些符号包括合约、库、接口、其他类型的定义以及函数等。使用import可以组织和重用代码,允许开发者将合约逻辑分散在多个文件中,便于管理和维护。import不仅限于接口,它可以导入任何在其他Solidity文件中定义的公共或外部可见的元素,但不能导入私有变量或内部变量,因为这些元素是封装在其原始合约中的,不对外部可见。

当你想要在Solidity中单独导入被导入文件中的全局符号(如合约、接口、库、函数、结构体等)时,这些符号应该与合约并列在文件结构中,而不是被包含在另一个合约的内部。这样,每个全局符号都被视为独立的定义,可以通过import语句单独导入到其他合约或文件中。

这种组织方式有助于提高代码的模块化和可重用性,允许开发者仅导入他们需要的部分,而不是整个文件。例如,如果你有一个定义了多个合约、库或其他全局符号的Solidity文件,你可以选择性地只导入需要的那部分符号到你的合约中,这样做可以减少编译后代码的大小,提高合约的效率。