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;
//科学计数法
比如:1018 次方,也就是 1 后面跟着 180,可以简写为 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_ba_sub_ba_mul_ba_div_ba_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);
}
}

浮点型

浮点型 共有两种:fixedufixed,分别代表 有符号定长浮点数无符号定长浮点数

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

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

fixedufixed 分别是 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 种可见性修饰符,分别是 publicprivateinternal,用于限制状态变量的访问权限。

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;

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

constantimmutable区别:

  • 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 种,分别是 privatepublicinternalexternal,用于限制函数的使用范围。

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 种:pureviewpayable、未标记状态。

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
  • 调用发送以太币
  • 调用任何不是 viewpure 的函数
  • 使用了底层调用
  • 使用包含特定操作码的内联汇编

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 {
// ......
}
}

设置状态可变性的作用:

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

调用 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 的函数体内添加处理语句的话,最好不要添加太多的业务逻辑

因为外部调用 sendtransfer 方法进行转账的时候,为了防止重入攻击,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 不为空时,就意味着:外部在调用合约中的函数。

在左边的分支可以看到,receivefallback 函数都能够用于接收以太币 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函数。

第二种情况

智能合约中同时定义了 receivefallback 函数,而且两者的状态可变性都为 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 支持两种断言语句:requireassert

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 ,才会执行除法操作。

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

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

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 种小类,使用 bytes1bytes2bytes3 直到 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 可以是任何基本数据类型,比如:整型、地址型、布尔型、枚举型,以及 bytesstring,但是部分复杂对象不允许使用,比如:动态数组、结构体、映射。

**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 种:memorystoragecalldata

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 的值,dataRefdata 实际上指向了同一块数据,也可以说,dataRefdata 的别名,所以,修改了 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 的数据是只读的,不能修改。与 storagememory 相比,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 中就自动拥有了状态变量 nameage 和方法 getSalary

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

virtual 和 override

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

父合约首先需要使用 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;
}
}

子合约 EmployeegetSalary 方法覆盖了父合约 Person 的 同名方法,调用子合约 EmployeegetSalary 方法,输出结果是 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();
}
}

按照线性继承原则,合约的继承顺序是 ContractAContractBChildContract

也就是说,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