blockchain solidity基础

blockchain solidity基础

henry Lv4

在线智能合约环境:https://remix.ethereum.org/

Reference

https://binschool.app/

基本结构

使用solidity编写的简单的智能合约

1
2
3
4
5
6
7
8
9
10
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract HelloWorld {
string hello = "Hello World";

function greet() public view returns(string memory) {
return hello;
}
}

一个典型的智能合约,通常由 4 个部分组成,分别是:声明部分、合约定义部分、状态变量部分 和 函数部分。其中,状态变量部分 和 函数部分 是智能合约的主体。

nipaste_2024-07-29_17-15-1

solidity 基本数据类型

在 Solidity 中,数据可分为两种类型:基础类型 和 复合类型。

基础类型 包括:整型、布尔型、地址型、字节型、浮点型、枚举型等。

复合类型 包括:数组、映射、结构体等。复合类型是由基础类型组合而成,它比基础类型略微复杂。

注意:数据类型还有其它的分类方法,比如按照数据在传递和使用时的特征不同,又可以分为 值类型(value types) 和 引用类型(reference types) 两大类。

整型

1
2
3
4
5
6
7
8
9
// 无符号整型: uint 的内部存储长度是 256 位
uint ucount = 16;
// 有符号整型: int 的内部存储长度是256位
int count = -1;
//如果要表示的数字位数过长,或者需要按照特定位数进行分组
uint num1 = 1000_000_000;
uint num2 = 10_0000_0000;
//科学计数法
比如:10 的 18 次方,也就是 1 后面跟着 18 个 0,可以简写为 10e18。

算数运算符

1
2
3
4
5
6
7
8
9
10
11
12
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract IntegerCompare {
int public a = 2;
int public b = 3;
int public a_add_b = a + b; // a_add_b = 5
int public a_sub_b = a - b; // a_sub_b = -1
int public a_mul_b = a * b; // a_mul_b = 6
int public a_div_b = a / b; // a_div_b = 0
int public a_mod_b = a % b; // a_mod_b = 2
}

点击部署的合约中变量 a_add_b、a_sub_b、a_mul_b、a_div_b、a_mod_b,就会显示运算后的变量值。

nipaste_2024-07-29_17-27-3

除了 uint 和 int 外,Solidity 中按照存储长度,还定义了一系列特定长度的 整型。它们的长度从 8 位一直到 256 位,按照 8 的倍数递增。

无符号整型 uint8、uint16、uint24、uint32……uint256,

有符号整型 int8、int16、int24、int32……int256

获取某种整型的最大值和最小值,可以使用 type 函数。这在有些 ERC20 代币合约中会用到

nipaste_2024-07-29_17-30-3

注意事项:如果不对变量初始化,默认值为 0

布尔型

1
2
3
4
5
6
7
8
9
10
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract BoolOps {
bool public a = true;
bool public b = false;
bool public not_a = !a; // not_a = false
bool public a_and_b = (a && b); // a_and_b = false
bool public a_or_b = (a || b); // a_or_b = true
}

运算符 || 和 && 遵循短路( short-circuiting )规则。

布尔型变量默认值为 false

地址型

地址型 是 Solidity 中常用的数据类型之一,用来存储以太坊中的账户地址,它使用关键字 address 来声明。

地址型 变量的内部存储长度为160位,也就是20个字节,通常使用一个十六进制字符串来表示,并以前缀 0x 开头

账户地址是通过公钥按照一定算法计算得到的。

计算过程如下:

  • 首先,将未压缩格式的公钥作为输入值,使用 keccak-256 哈希算法,生成了一个256位的哈希值。
  • 然后,截取256位哈希值右边的160位作为账户地址。
  • 最后,为了便于显示,账户地址使用十六进制字符串来表示,这个字符串的长度为40( 1个字节由两个16进制字符来表示),并以前缀 0x 开头。

地址之间可以进行比较运算,使用的操作符有:==、!=、 <=、 <、>=、>

1
2
3
4
5
6
7
8
9
10
11
// SPDX-License-Identifier: MIT 
pragma solidity ^0.8.0;

contract AddressCompare {
address address1 = 0xB2D02Ac73b98DA8baF7B8FD5ACA31430Ec7D4429;
address address2 = 0x4B20993Bc481177ec7E8f571ceCaE8A9e22C02db;

function compareAddresses() public view returns (bool) {
return (address1 == address2);
}
}

上面这个程序比较两个账户地址是否相同,可以看到在上面的例子中,两个值并不相同,最终返回值为 false

地址属性和方法

地址 具有 .balance 属性,用于返回该账户中以太坊的余额,这也是 地址 最常用的方法

1
2
3
4
5
6
7
8
9
10
// SPDX-License-Identifier: MIT 
pragma solidity ^0.8.0;

contract AddressBalance {
address account = 0x4B20993Bc481177ec7E8f571ceCaE8A9e22C02db;

function getBalance() public view returns(uint) {
return account.balance;
}
}

nipaste_2024-07-29_17-41-1

地址 也经常用于转账,我们可以使用地址的 .transfer() 和 .send() 方法进行转账。

1
2
3
4
5
6
7
8
9
10
// SPDX-License-Identifier: MIT 
pragma solidity ^0.8.0;

contract AddressTransfer {
address account = 0x4B20993Bc481177ec7E8f571ceCaE8A9e22C02db;

function transferETH() public {
payable(account).transfer(1 ether);
}
}

浮点型

浮点型 共有两种:fixed 和 ufixed,分别代表 有符号定长浮点数 和 无符号定长浮点数。

还定义了一系列特定长度的浮点数,分别使用关键字 fixedMxN 和 ufixedMxN 表示,其中 M 表示该类型占用的总位数,N 表示可用的小数位数。

M 可以取值 8 到 256 位,但必须能够被 8 整除;N 可以是从 0 到 80 之间的任意数。

fixed 和 ufixed 分别是 fixed128x18 和 ufixed128x18 的别名。

示例

1
2
3
4
// 浮点型常量可以直接赋值
fixed constant PI = 3.14159265;
// 不能给浮点型变量直接赋值
fixed a = 1.2;

nipaste_2024-07-29_17-48-3

重要

在 Solidity 中,wei是以太币的基本计量单位,也是默认的计量单位,而不是 ETH。

1 个以太币 ETH 等于 10**18(10的18次方)wei。

在 Solidity 中,对某地址 address 进行一笔转账,可以使用如下的方式

1
payable(address).transfer(100);

表明向 address 转了 100 wei,而不是100个比特币。

枚举型

1
2
3
4
// 定义枚举类型
enum gender {male, female}
// 使用枚举型
gender a = gender.male;

注意事项:

  • 枚举列表不能为空,至少要包含一个成员,否则编译器将会报错
  • 定义枚举型的语句,后面不需要跟着分号。一般大括号的后面都不需要跟着分号,如果跟着分号,编译器会报错
  • 枚举型只能在全局空间内声明,不能在函数内声明。通常放在状态变量前面的位置声明

枚举型在编译后就会转换为无符号整数 uint8,在 Solidity 中,枚举值可以转换为整数,它的第一个成员的值默认是 0,后面的值依次递增。

1
2
3
enum status {normal, deleted}
uint8 a = uint8(status.normal) // a = 0
uint8 b = uint8(status.deleted) // b = 1

下面是一个类型转换的例子

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
// SPDX-License-Identifier: MIT 
pragma solidity ^0.8.0;

contract EnumOps {
// 定义枚举型
enum gender {male, female}

// 使用枚举型声明状态变量
gender private myGender = gender.female;

// 函数内使用枚举类型
function useEnum() public returns(gender) {
gender t = myGender;
myGender = gender.male;
return t;
}
// 枚举型用作返回值
function returnEnum() public pure returns(gender) {
return gender.female;
}
// 枚举值转换为整型
function convertInt() public pure returns(uint) {
return uint(gender.female);
}
// 整型转换为枚举型
function convertEnum() public pure returns(gender) {
return gender(1);
}
}

结果如下

nipaste_2024-07-29_17-59-5

solidity 变量

Solidity 提供了 3 种类型的变量:状态变量、局部变量、全局变量。

状态变量

状态变量是指在智能合约中声明的持久化存储的变量。它存在于合约的整个生命周期,直到合约被销毁。

状态变量的变动是要记录在区块链上的,永久存储,也就是通常所说的“数据上链”。

状态变量在合约的不同函数之间共享,可以通过调用函数来读取或修改它的数据值。

状态变量类似于其它编程语言中“类”的成员变量。

1
2
3
4
5
6
7
8
9
10
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract StatusVar {
uint256 myStatus = 1; // 声明状态变量

function getStatus() public view returns(uint256) {
return myStatus; // 使用状态变量
}
}

局部变量

局部变量如上代码中所示,不难理解其作用。

1
2
3
4
5
6
7
8
9
10
11
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract LocalVar {
function sum() public pure returns(uint256){
uint256 a = 1; // 声明局部变量 a
uint256 b = 2; // 声明局部变量 b
uint256 result = a + b; // 声明局部变量 result,并使用局部变量a, b
return result; // 使用局部变量 result 作为返回值
}
}

全局变量

全局变量是指在合约的顶层预先定义的特殊变量,用于提供有关区块链和交易属性的信息。

全局变量是由 Solidity 语言本身提供,用户无权定义或者修改,但可以直接在任何位置使用。

1
2
3
4
5
6
7
8
9
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract GlobalVar {
// 返回当前函数调用所在的区块号、时间戳、调用者地址
function getGlobalVars() public view returns(uint,uint,address){
return (block.number, block.timestamp, msg.sender);
}
}

可见性

Solidity 为状态变量提供了 3 种可见性修饰符,分别是 public、private 和 internal,用于限制状态变量的访问权限。

1
2
uint256 public delta = 8; // 可见性声明为 public 合约内部和外部都可以访问这个状态变量
uint256 private delta = 8; // 可见性声明为 private 只能在合约内部访问这个状态变量

internal

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract InternalVisibility {
uint256 internal delta = 8; // 可见性声明为 internal

function addDelta(uint256 num) external view returns(uint256) {
uint256 sum = num + delta; // 函数内可以使用状态变量 delta
return sum;
}
}

// InheritedVisibility 继承合约 InternalVisibility
contract InheritedVisibility is InternalVisibility {
function getDelta() external view returns(uint256) {
return delta; // 继承合约中可以使用状态变量 delta
}
}

注意事项:如果状态变量在声明的时候,没有指定可见性,那么它的可见性就为默认值 internal。

默认值

Solidity 中有一个 delete 操作符,它可以对变量重新赋值,从字面意思来看,似乎是要删除一个变量,其实不是,delete 操作符只是对变量重新初始化,使其值变为默认值。

1
2
uint a = 100;
delete a; // 执行delete后,a = 0

常量

在智能合约中,如果一个状态变量的值恒定不变,就可以使用关键字 constant 或者 immutable 进行修饰,把它定义为常量。

1
2
string constant SYMBOL = "WETH";
uint256 immutable TOTAL_SUPPLY = 1000;

状态变量一旦声明为 constant 和 immutable 后,就不能更改它的值了。

constant 和 immutable区别:

  • constant 关键字修饰的状态变量,必须在声明时就立即显式赋值,然后就不再允许修改了

  • immutable 关键字修饰的状态变量,既可以在声明时显式赋值,还可以在合约部署时赋值,也就是在合约的构造函数 constructor 中赋值

  • 适用的数据类型

    constant 可以修饰任何数据类型。

    immutable 只能修饰值类型,比如:int、uint、bool、address 等,不能修饰 string、bytes 等引用类型。

注意事项:

使用常量比变量更节省 gas 成本,这也是非常重要的一点。常量的值在编译时就已知,且不可改变,编译器会将其值直接嵌入到合约代码中,避免了在运行时进行存储和访问的开销。

solidity 函数

在 Solidity 中,函数是合约中的可执行代码块,用于定义合约的行为和操作,函数和状态变量是 Solidity 智能合约中最重要的组成部分。

函数定义如下:

1
2
3
function function_name(<parameter list>) <visibility> <state mutability> [returns(<return type>)] {
......
}

举例如下(一个加法函数):

1
2
3
function add(uint a, uint b) public pure returns(uint) {
return a + b;
}

函数可见性共有 4 种,分别是 private、public、internal、external,用于限制函数的使用范围。

在 Solidity 的新版本中,函数定义中必须显式地指定可见性,不能省略,否则无法通过编译。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract FunctionVisibility {
// 可见性为 private,只能在合约内部调用
function add(uint a, uint b) private pure returns(uint){
return a + b;
}
// 可见性为 public,合约内部和外部均可调用
function sub(uint a, uint b) public pure returns(uint){
return a - b;
}
// 可见性为 internal,合约内部和继承合约中可以调用
function mul(uint a, uint b) internal pure returns(uint){
return a * b;
}
// 可见性为 external,只能在合约外部调用
function div(uint a, uint b) external pure returns(uint){
return a / b;
}
}

如图可以看到只有 div 和 sub 可以从外部调用

nipaste_2024-07-29_20-00-2

返回值

在 Solidity 中,函数可以没有返回值,也可以返回一个或多个值,返回值可以是任何有效的数据类型,在函数声明中,需要使用 returns 关键字指定返回值的类型。

在函数体中,可以有两种方式来返回结果值:

  1. 使用 return 关键字指定返回值;
  2. 使用返回值的参数名称指定返回值。
1
2
3
4
5
6
7
8
9
10
11
12
13
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract FunctionReturn {
// 使用 `return` 关键字指定返回值
function add1(uint a, uint b) public pure returns (uint){
return a + b;
}
// 使用返回值的参数名称指定返回值
function add2(uint a, uint b) public pure returns (uint result){
result = a + b;
}
}

多返回值

1
2
3
4
5
6
7
8
9
10
11
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract FunctionMultiReturn {
// 多返回值
function addAndMul(uint a, uint b) public pure returns (uint, uint){
uint sum = a + b;
uint product = a * b;
return (sum, product);
}
}

solidity 支持函数重载

状态可变性 visibility

函数的状态可变性有 4 种:pure、view、payable、未标记状态。

1. pure

状态可变性为 pure 的函数,也称为纯函数,是指函数不会读取和修改合约的状态,换言之,pure 函数不会读取和修改链上的数据,例如:

1
2
3
4
5
6
7
8
9
10
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract MutabilityPure {
function sum() public pure returns(uint){
uint a = 2; // 局部变量 a
uint b = 3; // 局部变量 b
return a + b; // 只使用了局部变量 a、b
}
}

由于上面的代码只使用了局部变量,并不涉及到全局变量和状态变量,因此可以将可变性设置为 pure。

在下面的情况下就不能使用 pure,不然不能通过编译

  • 读取状态变量
  • 访问 <address>.balance
  • 访问任何区块、交易、msg等全局变量
  • 调用了任何不是纯函数的函数
  • 使用包含特定操作码的内联汇编

2. view

状态可变性为 view 的函数,也称为视图函数,是指函数会读取合约的状态,但不会进行修改。

1
2
3
4
5
6
7
8
9
10
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract MutabilityView {
uint factor = 2; // 状态变量

function times(uint num) public view returns(uint){
return num * factor; // 使用了状态变量 factor
}
}

上面的代码只是对状态变量进行了读取,但并没有修改其值,因此可以将可变性设置为 view。

对于下列情况就不适用于 view:

  • 修改状态变量
  • 触发事件
  • 创建其它合约
  • 使用了自毁函数 selfdestruct
  • 调用发送以太币
  • 调用任何不是 view 或 pure 的函数
  • 使用了底层调用
  • 使用包含特定操作码的内联汇编

3. 未标记状态可变性

如果一个函数定义中没有标记任何状态可变性,也就是说,函数既没标记为 view 也没标记为 pure ,那么就意味着这个函数是要改变状态的

1
2
3
4
5
6
7
8
9
10
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract MutabilityNone {
uint factor = 2; // 状态变量

function setFactor(uint _factor) public {
factor = _factor; // 重设了状态变量 factor 的值
}
}

4. payable 状态可变性

如果一个函数的状态可变性标记为 payable,那么就表示它可以接收以太币,这些以太币是由调用者在调用函数时支付的。

1
2
3
4
5
6
7
8
9
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract MutabilityPayable {
// 投注函数标记为 payable,表示它可以接收以太币
function stake(uint teamID) public payable {
// ......
}
}

设置状态可变性的作用:

  • 安全性
  • 可靠性: view、pure 修饰的函数在调用时不会产生副作用,因此可以安全地被其它函数调用
  • 互操作性:通过标记函数的状态可变性,可以提供给其它合约和工具有关函数的重要信息

调用 view 、pure 函数,无需支付 gas,而调用非 view 、pure 函数就需要支付一定的 gas

构造函数

1
2
3
4
5
6
7
8
9
10
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract FuncConstructor {
uint256 totalSupply; // 状态变量

// 构造函数
constructor(uint256 _totalSupply) payable {
totalSupply = _totalSupply;
}

对于构造函数不需要设置为 pure 或者 view,因为构造函数常用来初始化合约,会改变合约的状态,如果在部署一个合约的时候,同时需要向合约内存入一些以太币,那么就需要将构造函数的可见性设置为 payable。

1
2
3
4
5
6
7
8
9
10
11
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract FuncConstructor {
uint256 totalSupply; // 状态变量

// 构造函数
constructor(uint256 _totalSupply) payable {
totalSupply = _totalSupply;
}
}

经典示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract FuncConstructor {
address _owner; // 合约部署者

// 构造函数
constructor() {
_owner = msg.sender; // 将合约部署者保存到状态变量 _owner
}

// 只允许合约部署者操作
function operate() public view {
require(msg.sender == _owner, "caller is not the owner");
// ......
}

// 获取合约部署者
function owner() public view returns (address) {
return _owner;
}
}

这段合约代码在部署的时候,将调用构造函数 constructor,把合约部署者的地址 msg.sender 记录在了状态变量 _owner 中, _owner 类似于合约的管理员,比普通用户拥有更大的权限。

比如,下面的函数 operate,就只能由合约部署者使用,而其它用户不能使用。

1
require(msg.sender == _owner, "caller is not the owner");

这里判断调用者是不是合约部署者 _owner。 如果是合约部署者的话,就继续向后执行;如果不是的话,就会输出错误信息。

nipaste_2024-07-29_20-24-2

点击函数 owner,可以获取合约部署者地址,

接受函数 receive

在以太坊区块链中存在两种类型的账户:外部账户 和 合约账户,它们在以太坊上有着不同的特性和用途。

1. 外部账户

外部账户,英文为 Externally Owned Account,缩写为 EOA,外部账户也就是平常使用的用户账户,用于存储以太币 ETH 。这些账户可以向其它账户发送以太币,或者从其它账户接收以太币。

在钱包里管理的账户,通常就是外部账户。比如,在小狐狸钱包 Metamask 里添加或者生成的 Account 就是外部账户。外部账户会有一个与之相关的以太坊地址,这个地址是一个以 “0x” 开头,长度为20字节的十六进制数,比如:0x7CA35…9C6F。外部账户都有一个对应的私钥,只有持有私钥的人才能对交易进行签名,所以,外部账户非常适用于资金管理。

常说的以太坊账户,在不特别指明的情况下,一般是指外部账户。

nipaste_2024-07-29_20-29-3

上图就是外部账户,同时拥有一个对应的私钥。

2. 合约账户

合约账户,英文为 Contract Account,缩写为 CA。在以太坊区块链上部署一个智能合约后,都会产生一个对应的合约地址,这个地址称为合约账户。合约账户主要用于托管智能合约,它里面包含着智能合约的二进制代码和状态信息。合约账户地址的格式与外部账相同:以 “0x” 开头,长度为20字节的十六进制数。合约账户没有私钥,只能由智能合约中的代码逻辑进行控制。

它在一定条件下,也可以用来存储以太币 ETH。

3. receive 函数

在以太坊区块链上部署智能合约时产生的合约账户,并不都是可以存入以太币ETH 的。一个智能合约如果允许存入以太币,就必须实现 receive 或者 fallback 函数。如果一个智能合约中这两个函数都没有定义,那么它就不能接收以太币。

如果只是为了让合约账户能够存入以太币,按照 solidity 语言规范,推荐使用 receive 函数。因为 receive 函数简单明了,目的明确,而 fallback 函数的用途相对复杂一些。

格式如下:

1
2
3
receive() external payable {
// 这里可以添加自定义的处理逻辑,但也可以为空
}

receive 函数有如下几个特点:

  • 无需使用 function 声明。
  • 参数为空。
  • 可见性必须设置为 external。
  • 状态可变性必须设置为 payable。

当外部地址向智能合约地址发送以太币时,将触发执行 receive 函数,可以在函数体内不写任何自定义的处理逻辑,它依然能够接收以太币,这也是最常见的使用方式。

如果必须在 receive 的函数体内添加处理语句的话,最好不要添加太多的业务逻辑。

因为外部调用 send 和 transfer 方法进行转账的时候,为了防止重入攻击,gas 会限制在 2300。如果 receive 的函数太复杂,就很容易会耗尽 gas,从而触发交易回滚。

receive 函数里通常会执行一些简单记录日志的动作,比如触发 event。

1
2
3
4
5
6
7
8
9
10
11
12
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract FuncReceive {
// 定义接收事件
event Received(address sender, uint amount);

// 接收 ETH 时,触发 Received 事件
receive() external payable {
emit Received(msg.sender, msg.value);
}
}

nipaste_2024-07-29_20-37-0

回退函数 fallback

在 Solidity 语言中,fallback 是一个预定义的特殊函数,用于在处理未知函数和接收以太币 ETH 时调用。

定义 fallback 函数的格式如下:

1
2
3
fallback () external [payable] {
// 这里可以添加自定义的处理逻辑,但也可以为空
}

fallback 函数有如下几个特点:

  • 无需使用 function声明。
  • 参数为空。
  • 可见性必须设置为 external。
  • 状态可变性可以为空,或者设置为 payable。

调用条件

fallback 会在两种情况下,被外部事件触发而执行:

  • 外部调用了智能合约中不存在的函数

    在这种情况下,函数声明中无需设置状态可变性,函数形式如下:

    1
    2
    fallback () external {
    }
  • 外部向智能合约中存入以太币,并且当前合约中不存在 receive 函数

    在这种情况下,函数声明中必须设置状态可变性为 payable,函数形式如下:

    1
    2
    fallback () external payable {
    }

如果合约中已经定义了 receive函数,那么向这个合约中存入以太币,将会优先调用 receive 函数,而不会执行 fallback 函数。

所以,如果一个智能合约允许存入以太币,那么它就必须实现 receive 或者 fallback 函数,而且函数的状态可变性设置为 payable。如果一个智能合约没有定义这两个函数中的任何一个,那么它就不能接收以太币

receive 和 fallback 工作流程

nipaste_2024-07-29_20-43-0

当参数 msg.data 为空时,就意味着:外部向合约进行转账,存入以太币。

当参数 msg.data 不为空时,就意味着:外部在调用合约中的函数。

在左边的分支可以看到,receive 和 fallback 函数都能够用于接收以太币 ETH。

一个智能合约在接收 ETH 时:

  • 如果存在着 receive 函数,就会触发 receive;
  • 当不存在 receive 函数,但存在 fallback 函数时,就会触发 fallback;
  • 而当两者都不存在时,交易就会 revert,存入 ETH 失败

测试和验证

第一种情况

智能合约中只定义 fallback 函数,而且状态可变性为 payable。

1
2
3
4
5
6
7
8
9
10
11
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract FuncFallback {
// 定义回退事件
event Fallback();

fallback() external payable {
emit Fallback();
}
}

nipaste_2024-07-29_20-47-2

转了 100wei,由于没有receive函数,所以触发fallback函数。

第二种情况

智能合约中同时定义了 receive 和 fallback 函数,而且两者的状态可变性都为 payable。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract FuncFallback {
// 定义接收事件
event Receive();
// 定义回退事件
event Fallback();

receive() external payable {
emit Receive();
}
fallback() external payable {
emit Fallback();
}
}

nipaste_2024-07-29_20-49-2

果不其然,两者同时存在的情况下,会优先调用 receive 函数。

第三种情况

依然使用上面的合约,当调用一个不存在的函数,将会触发 Fallback 事件。

这里依然使用第二种情况中的例子,在 “CALLDATA” 栏中填入随意编写的 msg.data 数据,使之不为空,再点击 **”Transact”**,可以看到交易成功,并且触发了 Fallback 事件。

nipaste_2024-07-29_20-52-4

可以看到在 msg.data 不为空的情况下,这里触发了 fallback 函数。

solidity 流程控制语句

运算符

与 C 语言大体一致

条件语句

与 C 语言大体一致

循环语句

与 C 语言大体一致

断言语句

Solidity 提供了断言语句,用于在合约执行过程中进行条件检查和错误处理,Solidity 支持两种断言语句:require 和 assert。

1. require

require 语句用于检查函数执行的先决条件,也就是说,确保满足某些特定条件后才能继续执行函数,如果条件不满足,则会中止当前函数的执行,并回滚(revert)所有的状态改变

1
2
3
4
5
6
7
8
9
10
11
12
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract Require {

// 转账函数
function transfer(address to, uint256 amount) public pure {
require(to != address(0), "address `to` is zero");
require(amount > 0, "`amount` is zero");
// 执行转账操作
}
}

调用 transfer 函数,然后将 to 设置为 0 然后调用,可以发现报错。

nipaste_2024-07-29_21-02-2

同时将 amount 设置为 0,也会触发报错

nipaste_2024-07-29_21-04-5

require 完全可以使用 revert 语句来代替。比如:

1
require(amount > 0, "`amount` is zero");

就可以使用 revert 语句改写为:

1
2
3
if(amount <= 0) { 
revert("`amount` is zero");
}

以上两种写法完全等价。关于 revert 实现原理,将在后面的章节中详细讲解。

2. assert

assert 语句的行为 require 非常类似,通常用于捕捉合约内部编程错误和异常情况。

如果捕捉到了异常,则会中止当前函数的执行,并回滚(revert)所有的状态改变。

1
2
3
4
5
6
7
8
9
10
11
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract Assert {

// 除法函数
function divide(uint256 dividend, uint256 divisor) public pure returns(uint256) {
assert(divisor != 0); // 确保除数不为零
return dividend / divisor;
}
}

合约中有一个除法函数 divide,在调用的时候,需要首先检测除数 divisor 是否为 0,如果为 0 ,触发异常,函数终止运行。只有除数 divisor 不为 0 ,才会执行除法操作。

require 和 assert 都能终止函数的执行,并回滚交易,但两者有一些区别。

  1. require 和 assert 参数不同,require 可以带有一个说明原因的参数,assert 没有这个参数。
  2. require 通常用于检查外部输入是否满足要求,而 assert 用于捕捉内部编程错误和异常情况。
  3. require 通常位于函数首部来检查参数,assert 则通常位于函数内部,当出现严重错误时触发。
  4. assert 是 Solidity 早期版本遗留下来的函数,不再建议使用,最好使用 require 和 revert 代替。

require 在实际运行的合约中使用广泛,通常用来检查输入参数的正确性,我们要熟练掌握它的使用方法。

solidity 复合数据类型

数组

Solidity 支持 固定长度数组 和 动态长度数组 两种类型

固定长度数组

格式如下

1
2
3
//type_name arrayName [length];
//举例:
uint balance[10];

初始化一个 固定长度数组,可以使用下面的语句:

1
2
uint balance[3] = [uint(1), 2, 3]; // 初始化固定长度数组
balance[2] = 5; // 设置第 3 个元素的值为 5

动态长度数组

动态长度数组 与 固定长度数组 相比,就是 动态长度数组 的长度是可以改变的。

声明一个 动态长度数组,只需要指定元素类型,而不需要指定元素的数量,语法如下:

1
2
type_name arrayName[];
uint[] balance = [uint(1), 2, 3];

对于动态长度数组,可以使用 push 和 pop 方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract DynamicArray {
// 初始化动态长度数组为[1,2,3]
uint[] balance = [uint(1), 2, 3];

function dynamicArray() external returns (uint length, uint[] memory array) {
// 追加两个新元素4、5
balance.push(4);
balance.push(5);

// 删除最后一个元素 5
balance.pop();

// 返回数组的长度和数组所有元素
return (balance.length, balance);
}
}

new、delete 操作

可以使用 new 关键字来动态创建一个数组。

1
2
3
4
5
6
7
8
9
10
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract Array {
function newArray() external pure returns(uint[] memory) {
uint[] memory arr = new uint[](3);
arr[0] = 1;
return arr; // 返回结果 arr = [1,0,0]
}
}

通过 new 关键字创建的数组是一个动态长度数组。它是无法一次性赋予初值的,只能对每一个元素的值分别进行设置。

在前面已经讲过,**delete 操作符只是对变量值重新初始化,使其值变为默认值,而不是删除这个变量**。在数组上使用 delete 操作,效果也是一样的。

delete arr[i],并不是要删除一个数组中的元素,而是将该元素恢为默认值,例如:

1
2
uint[] balance = [uint(1), 2, 3];
delete balance[0]; // 执行 delete 后,balance = [0,2,3]

除了动态长度数组的 pop 函数外,Solidity 并没有提供删除某个特定元素的函数。

字节和字符串

字节型 也是 Solidity 语言中重要的数据类型,字节型 用于表示特定长度的字节序列,分为 固定长度字节型 和 动态长度字节型 两种类型,字节型 本质上是一个字节数组。

比如,在智能合约中经常出现的哈希值、数据签名等数据,都会使用 字节型 来定义。

固定长度字节型

固定长度字节型 按照长度分为 32 种小类,使用 bytes1、bytes2、bytes3 直到 bytes32 表示,每种类型代表不同长度的字节序列,其中的 bytes32 使用最为普遍。

固定长度字节型 的变量声明如下:

1
2
3
bytes1 myBytes1 = 0x12;  // 单个字节
bytes2 myBytes2 = 0x1234; // 两个字节
bytes32 myBytes32 = 0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef; // 32个字节

动态长度字节型

动态长度字节型 可以存储任意长度的字节序列,它使用关键字 bytes 来声明,在声明变量时,需要指定其长度或初始化值。

1
2
3
bytes myBytes = new bytes(10);  // 声明一个长度为 10 的动态长度字节变量
bytes myBytes = "Hello"; // 声明一个动态长度字节变量,并初始化为 "Hello"
bytes myBytes = hex"1234"; // 声明一个动态长度字节变量,并初始化为十六进制 0x1234

示例:

1
2
3
4
5
6
7
8
9
10
11
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract DynamicBytes {
function dynamicBytes() external pure returns (uint256, bytes memory) {
bytes memory myBytes = new bytes(2); // 创建动态长度字节型变量
myBytes[0] = 0x12; // 设置单个字节的值
myBytes[1] = 0x34;
return (myBytes.length, myBytes);// .length 取字符串的长度
}
}

字符串

在 Solidity 中,字符串 是用来存储文本数据的类型。字符串的值使用双引号 (“) 或单引号 (‘) 包裹,类型用 string 表示。

1
string public myString = "Hello World";

字符串与固定长度的字节数组非常类似,它的值在声明之后就不可变了,如果想要对字符串中包含的字符进行操作,通常会将它转换为 bytes 类型。

Solidity 提供了字节数组 bytes 与字符串 string 之间的内置转换。

1
2
3
4
5
6
//bytes 转换 string
string(myBytes);
//string 转换 bytes
bytes(myString);
//获得字符串长度
bytes(myString).length;

结构体

定义一个结构体类型使用 struct 关键字,它的语法如下:

1
2
3
4
5
6
struct struct_name { 
type1 type_name_1;
type2 type_name_2;
type3 type_name_3;
...
}

其中 struct_name 是结构体的名称,typeN 是结构体成员的数据类型,type_name_N 是结构体成员的名字。

这里定义一个结构体类型 Book,用来表示一本书:

1
2
3
4
5
struct Book { 
string title;
string author;
uint ID;
}

使用方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract StructAccess {
struct Book {
string title;
string author;
uint ID;
} // 定义结构体 Book

Book book; // 使用结构体 Book 声明变量

function setBook() public {
book.title = "Learn Solidity"; // 设置结构体的成员 title
book.author = "BinSchool";
book.ID = 1;
}

function getBookAuthor() public view returns(string memory) {
return book.author; // 读取结构体的成员 author
}
}

初始化方式

结构体变量共有 3 种初始化数据的方式:按字段顺序初始化、按字段名称初始化、按默认值初始化。

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
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract StructInit {
struct Book {
string title;
string author;
uint ID;
} // 定义结构体 Book


function getBooks() public pure returns(Book memory,Book memory,Book memory) {
// 按字段顺序初始化
Book memory book1 = Book('Learn Java', 'BinSchool', 1);

// 按字段名称初始化
Book memory book2 = Book({title:"Learn JS", author:"BinSchool", ID:2});

// 按默认值初始化
Book memory book3;
book3.ID = 3;
book3.title = 'Learn Solidity';
book3.author = 'BinSchool';
return (book1, book2, book3);
}
}

映射

Solidity 中的映射类型 mapping,用来以键值对的形式存储数据。它的主要作用是提供高效的查找功能,类似于其它编程语言中的哈希表或者字典。

例如,在使用 白名单 的场景中,可以通过 映射类型 将用户地址映射到一个布尔值,以标识哪些地址是被允许的,哪些地址是不被允许的,从而高效地控制访问权限。

映射类型是智能合约中最常用的数据类型之一

语法如下:

1
mapping(key_type => value_type)

mapping 类型是将一个键 (key) 映射到一个值 (value)。

其中:key_type 可以是任何基本数据类型,比如:整型、地址型、布尔型、枚举型,以及 bytes 和 string,但是部分复杂对象不允许使用,比如:动态数组、结构体、映射。

**value_type 可以是任何数据类型 **

1
2
3
4
5
6
7
8
9
10
struct MyStruct { uint256 value; } // 定义一个结构体

mapping(address => uint256) a; // 正确
mapping(string => bool[]) b; // 正确
mapping(int => MyStruct) c; // 正确
mapping(address => mapping(address => uint)) d; // 正确

// mapping(uint[] => uint) public e; // 错误
// mapping(MyStruct => addrss) public f; // 错误
// mapping(mapping(string=>int)) => uint) g; // 错误

在智能合约中,mapping 类型的使用非常普遍的。比如,在 ERC20 代币合约中,经常会使用 mapping 类型的变量作为一个内部账本,用来记录每一个钱包地址拥有的代币余额。

下面例子模拟了一个稳定币 USDT 的合约:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract USDT {
mapping(address => uint256) balances; // 保存所有持有 USDT 账户的余额

// 构造函数,合约部署时自动调用
constructor() {
balances[msg.sender] = 100; // 初始设定合约部署者的账户余额为 100 USDT
}

// 查询某一个账户的USDT余额
function balanceOf(address account) public view returns(uint256) {
return balances[account];
}
}

复制上面的合约地址,然后传给下面的 balanceof 函数

nipaste_2024-07-30_10-43-1

返回值为 100

nipaste_2024-07-30_10-44-1

注意:在 mapping 类型的变量中获取一个不存在的键的值,并不会报错,而是会返回值类型的默认值。比如,整型会返回 0,布尔型会返回 false 等。

优缺点

优点:

mapping 可以用来存储数据集,并且提供了高效的查找功能。它可以根据“键”快速定位到特定元素。

缺点:

mapping 最大的问题是无法直接遍历。

值类型和引用类型

当调用一个函数时,如果参数为值类型,那么在函数内部对参数数据进行修改,是不会影响到原始数据的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract ValueOps {
// 更新值类型的值
function update() external pure returns(bytes2){
// 原始数据 data 的值为 0x1234
bytes2 data = 0x1234;
// 调用函数修改它的值
updateValue(data);
return data; //返回值依然是 0x1234
}

// 修改第一个字符为 0x4567
function updateValue(bytes2 value) internal pure {
value = 0x4567;
}
}

引用类型

在 Solidity 中引用类型共有三种:数组、结构体 struct 和映射 mapping。(类似于C中的指针)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract ReferenceOps {
// 更新引用类型的值
function update() external pure returns(string memory){
// 原始数据 data 的值为 "1234"
bytes memory data = "1234";
// 调用函数修改它的第一个字符为 '5'
updateValue(data);
// 将其转为字符串,输出最终结果 0x5234
return string(data);
}

// 修改第一个字符为 '5'
function updateValue(bytes memory value) internal pure {
value[0] = '5';
}
}

数据位置

Solidity 中的数据的存储位置有 3 种:memory、storage、calldata。

1. storage

storage 是指永久保存在区块链上的存储,通常用于存储合约的状态变量。

storage 中的变量在合约部署后会一直存在,直到合约被销毁。

由于它保存在区块链上,需要同步到所有区块链节点,而且永久保存,所以它的使用成本高,gas 消耗多。

1
2
3
contract StorageVar {
string name = "BinSchool.app"; // 声明状态变量
}

name 是一个状态变量,存储在 storage 中,它的数据会一直保存在区块链上。

在函数中,对于“引用类型”的状态变量,可以通过关键字 storage 来引用它。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract StorageVar {
// 引用类型的状态变量
uint[] data = [1,2,3];

// 修改状态变量
function update() external {
// 变量 dataRef 引用了状态变量 data
uint[] storage dataRef = data;
// 修改 dataRef
dataRef[0] = 100;
}

// 打印状态变量
function print() external view returns(uint, uint, uint) {
return (data[0],data[1],data[2]);
}
}

为状态变量 data 创建了一个引用 dataRef,然后修改了 dataRef 的值,dataRef 和 data 实际上指向了同一块数据,也可以说,dataRef 是 data 的别名,所以,修改了 dataRef 指向的数据,也就是修改了 data 指向的数据。

2. memory

memory 是函数调用期间分配的临时内存,通常用于存储引用类型的局部变量。memory 中的变量在函数调用结束后会被销毁。它对应于其它编程语言中的 “堆”。

memory 的使用成本非常低,消耗的 gas 少。

1
2
3
4
5
6
contract MemoryVar {
function name() public pure returns(string memory){
string memory s = "BinSchool.app"; // 声明局部变量 s
return s;
}
}

函数 name 中的变量 s,存储在 memory 中。当函数调用结束后,就会从内存中清除。

在函数中,对于“引用类型”的状态变量,可以通过关键字 memory 来创建它的副本。

拿上面 storage 中例子,进行分析,修改了 dataRef 指向的数据,不会修改 data 指向的数据,因为memory会创建一个对应引用类型的副本。

3. calldata

calldata 是外部程序在调用合约函数时,用来保存传入参数的存储位置。

calldata 变量的行为类似于 memory 变量,它在函数调用结束后就会被销毁。两者不同之处在于,calldata 的数据是只读的,不能修改。与 storage 和 memory 相比,calldata 存储的成本最低,gas 消耗最少。

calldata 只能用于函数参数,无法在函数内部声明。

例如:

1
function setName(string calldata name) external;

calldata 的使用场景并不多,在函数内部,通常会转为 memory 再去操作。但在某些场景下,出于节省 gas 的目的,也会使用 calldata 变量。

注意:在函数中,值类型的变量通常分配在栈上,不在上面的三种存储中。

solidity 面向对象编程

合约继承

语法如下

1
2
3
contract child_contract is parent_contract {
// ......
}

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract Person{
string public name;
uint public age;
function getSalary() public pure returns(uint){
return 1000;
}
}

contract Employee is Person {
}

nipaste_2024-07-30_11-00-3

虽然合约 Employee 中没有定义任何状态变量和方法,但是由于它继承了合约 Person ,所以在 Employee 中就自动拥有了状态变量 name、age 和方法 getSalary。

注意:子合约只能继承父合约中可见性为 public 、internal 和 external 的状态变量和方法,而可见性为 private 的状态变量和方法,是不能被子合约继承的。

virtual 和 override

Solidity 在合约继承的语法中,引入了 virtual 和 override 两个关键字,用于重写父合约中的函数。

父合约首先需要使用 virtual 关键字声明一个虚函数,然后,在子合约中使用 override 关键字来覆盖父合约的方法。

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract Person{
string public name;
uint public age;

// 使用 virtual 声明此函数可以被子合约覆盖
function getSalary() public pure virtual returns(uint){
return 1000;
}
}

contract Employee is Person {
// 使用 override 表示此函数覆盖了父合约的同名函数
function getSalary() public pure override returns(uint){
return 3000;
}
}

子合约 Employee 的 getSalary 方法覆盖了父合约 Person 的 同名方法,调用子合约 Employee 的 getSalary 方法,输出结果是 3000,而不是父合约中的 1000。

注意 :这里如果不使用 virtual 将会报错

构造函数的继承

如果父合约的构造函数带有参数,那么子合约在继承父合约时,就需要在自己的构造函数中显示地调用父合约的构造函数。子合约继承合约时,调用父合约的构造函数有两种方式:直接传值 和 传入输入值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract Parent {
uint public data;
constructor(uint _data){
data = _data;
}
}
//直接传值
contract Child is Parent(1) {
}
//传入输入值
contract Child is Parent {
constructor(uint _data) Parent(_data) {
}
}

调用父合约函数

子合约调用父合约的函数有两种方法:使用关键字 super 和 使用合约名称。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 子合约 Employee
contract Employee is Person {
function getSalary() public pure override returns(uint){
return 3000;
}

function getPersonSalaryBySuper() public pure returns(uint){
return super.getSalary(); // 使用 super 调用父合约函数
}

function getPersonSalaryByName() public pure returns(uint){
return Person.getSalary(); // 使用合约名称调用父合约函数
}
}

抽象合约和接口

抽象合约

Solidity 允许在一个合约中只声明函数的原型,而没有具体实现,然后在继承的子合约中再去实现,这样的合约称为抽象合约。

抽象合约使用 abstract 关键字进行声明。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

abstract contract Person{
string public name;
uint public age;

// 使用 virtual,表示函数可以被子合约覆盖
function getSalary() public pure virtual returns(uint);
}

contract Employee is Person {
// 使用 override,表示覆盖了子合约的同名函数
function getSalary() public pure override returns(uint){
return 3000;
}
}

使用 abstract 声明的抽象合约,通常包含至少一个未实现的函数。由于抽象合约并不完整,所以,抽象合约不能单独部署。

接口

定义了合约应该实现的函数名和事件,但没有实现任何函数的具体逻辑。

编写接口有以下限制规则:

  1. 不能包含状态变量
  2. 不能定义结构体
  3. 不能包含构造函数
  4. 不能继承除接口外的其它合约
  5. 所有函数的可见性都必须是 external,而且不能有函数体
  6. 继承接口的合约必须实现接口定义的所有功能

语法如下:

1
2
3
interface 接口名{
函数声明;
}

下面的代码即是一个接口的实现

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
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

interface IERC20 {
function name() external view returns (string memory);
function symbol() external view returns (string memory);
function decimals() external view returns (uint8);
}

contract ERC20 is IERC20 {
string _name = "MyCoin"; // 代币名称
string _symbol = "MYC"; // 代币符号
uint8 _decimals = 18; // 小数精度

function name() external view returns (string memory) {
return _name;
}

function symbol() external view returns (string memory) {
return _symbol;
}

function decimals() external view returns (uint8) {
return _decimals;
}
}

使用接口

接口的主要作用是提供了一种约定和规范,用于与其它合约进行交互和通信。

在上面的例子中,编写了一个代币合约 ERC20,它实现了接口 IERC20,如果另外有一个合约,要调用已经部署在区块链上的 ERC20 合约,实际上非常简单,只需要知道这个合约的地址,并且知道它实现了 IERC20 接口,就可以在其它合约中使用它。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
 // SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

interface IERC20 {
function name() external view returns (string memory);
function symbol() external view returns (string memory);
function decimals() external view returns (uint8);
}

contract UseERC20 {
function getToken() external view returns (string memory,string memory,uint8) {
// 通过传入合约地址,构造调用接口
IERC20 token = IERC20(0xd9145CCE52D386f254917e481eB44e9943F39138);
return (token.name(),token.symbol(),token.decimals());
}
}

其中 0xd9145CCE52D386f254917e481eB44e9943F39138 就是上面部署的实现接口的合约地址。

多重继承

语法如下

1
2
3
contract child_contract is parent_contract1, parent_contract2... {
// ......
}

线性继承

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract ContractA {
function foo() public pure virtual returns(string memory){
return "ContractA";
}
}

contract ContractB {
function foo() public pure virtual returns(string memory){
return "ContractB";
}
}

contract ChildContract is ContractA, ContractB {
function foo() public pure override(ContractA, ContractB) returns(string memory){
return super.foo();
}
}

按照线性继承原则,合约的继承顺序是 ContractA 、ContractB、ChildContract。

也就是说,ChildContract 先继承 ContractA 的属性和方法,再继承 ContractB 的属性和方法,所以上面例子中的 super 是 ContractB ,调用 super.foo() 的返回结果为 “ContractB”。

多重继承分析

在定义合约多重继承的时候,必须正确地设置父合约的顺序,使之符合线性继承原则。如果顺序设置不正确,那么合约将无法编译。

第一种情况

合约 Y 继承了合约 X,合约 Z 同时继承了 X,Y。如图所示:

1
2
3
4
5
  X 
/ \
Y |
\ /
Z

这种情况下,我们按照线性继承原则,最基本的合约放在最前面,合约的继承顺序应该为:X,Y,Z。

1
2
contract Z is X,Y {
}

如果写成:

1
2
contract Z is Y,X {
}

那么编译器就会报错。

第二种情况

合约 Y 继承了合约 X;合约 A 也继承了合约 X,合约 B 又继承了 A;合约 Z 同时继承了 Y,B。如图所示:

1
2
3
4
5
6
7
   X 
/ \
Y A
| |
| B
\ /
Z

这种情况下,按照线性继承原则,最终理清的合约继承顺序为:X,Y,A,B,Z。

1
2
contract Z is Y,B {
}
  • Title: blockchain solidity基础
  • Author: henry
  • Created at : 2024-07-30 11:38:55
  • Updated at : 2024-07-30 11:40:32
  • Link: https://henrymartin262.github.io/2024/07/30/solidity_study/
  • License: This work is licensed under CC BY-NC-SA 4.0.
 Comments