Solidity入门学习(一)
2025-02-03 20:59:02 # Web3

Hello Web3

Solidity简介

Solidity是一种用于编写以太坊虚拟机(EVM)智能合约的编程语言。

Hello Web3

简单的程序HelloWeb3.sol如下:

1
2
3
4
5
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.21;
contract HelloWeb3{
string public _string = "Hello Web3!";
}

使用到https://remix.ethereum.org/

进行编译和部署,点击_string输出。

image-20250122224953662

代码中的

1
pragma solidity ^0.8.21;

表示源文件将不允许小于 0.8.21 版本或大于等于 0.9.0 的编译器编译(第二个条件由 ^ 提供)。

其中

1
string public _string = "Hello Web3!";

public 修饰符表示 _string 变量是公开的,任何人都可以读取该变量的值。

Solidity 会自动为 public 修饰的状态变量生成一个 getter 函数,允许外部代码(如其他合约或外部用户)通过合约的 ABI(应用二进制接口)访问这个变量的值。

值类型

Solidity中的变量类型

  1. 值类型(Value Type):包括布尔型、整数型等等,这类变量赋值时候直接传递数值。
  2. 引用类型(Reference Type):包括数组和结构体,这类变量占空间大,赋值时候直接传递地址(类似指针)。
  3. 映射类型(Mapping Type):Solidity中存储键值对的数据结构,可以理解为哈希表。

编写一个简单的程序ValueTypes.sol,介绍一下各种变量类型,程序如下:

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
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;
contract ValueTypes{
// 布尔值
bool public _bool = true;
// 布尔运算
bool public _bool1 = !_bool; //取非
bool public _bool2 = _bool && _bool1; //与
bool public _bool3 = _bool || _bool2; //或
bool public _bool4 = _bool == _bool; //相等
bool public _bool5 = _bool != _bool1; //不想等
// 整数
int public _int = -1;
uint public _uint = 1;
uint256 public _number = 20250122;
// 整数运算
uint256 public _number1 = _number + 1; // +,-,*,/
uint256 public _number2 = 2**2; // 指数
uint256 public _number3 = 7 % 2; // 取余数
bool public _numberbool = _number2 > _number3; // 比大小
// 地址
address public _address = 0xAb8483F64d9C6d1EcF9b849Ae677dD3315835cb2;
address payable public _address1 = payable(_address); // payable address,可以转账、查余额
// 地址类型的成员
uint256 public balance = _address1.balance; // balance of address
// 固定长度的字节数组
bytes32 public _byte32 = "MiniSolidity";
bytes1 public _byte = _byte32[0];

// 用enum将uint 0, 1, 2表示为Buy, Hold, Sell
enum ActionSet { Buy, Hold, Sell }
// 创建enum变量 action
//ActionSet action = ActionSet.Buy;
ActionSet action = ActionSet.Buy;
// enum可以和uint显式的转换
function enumToUint() external view returns(uint){
return uint(action);
}

}

地址类型

地址类型(address)有两类:

  • 普通地址(address): 存储一个 20 字节的值(以太坊地址的大小)。
  • payable address: 比普通地址多了 transfersend 两个成员方法,用于接收转账。

定长字节数组

字节数组分为定长和不定长两种:

  • 定长字节数组: 属于值类型,数组长度在声明之后不能改变。根据字节数组的长度分为 bytes1, bytes8, bytes32 等类型。定长字节数组最多存储 32 bytes 数据,即bytes32
  • 不定长字节数组: 属于引用类型,数组长度在声明之后可以改变,包括 bytes 等。
1
2
3
// 固定长度的字节数组
bytes32 public _byte32 = "MiniSolidity";
bytes1 public _byte = _byte32[0];

在上述代码中,MiniSolidity 变量以字节的方式存储进变量 _byte32。如果把它转换成 16 进制,就是:0x4d696e69536f6c69646974790000000000000000000000000000000000000000

_byte 变量的值为 _byte32 的第一个字节,即 0x4d

image-20250122234241902

枚举enum

枚举(enum)是 Solidity 中用户定义的数据类型。它主要用于为 uint 分配名称,使程序易于阅读和维护。它与 C 语言 中的 enum 类似,使用名称来代替从 0 开始的 uint

1
2
3
4
// 用enum将uint 0, 1, 2表示为Buy, Hold, Sell
enum ActionSet { Buy, Hold, Sell }
// 创建enum变量 action
ActionSet action = ActionSet.Buy;

枚举可以显式地和 uint 相互转换,并会检查转换的正整数是否在枚举的长度内,否则会报错:

1
2
3
4
// enum可以和uint显式的转换
function enumToUint() external view returns(uint){
return uint(action);
}

enum 是一个比较冷门的变量,几乎没什么人用。

enumuint相互转换,如下:

image-20250122234959766

函数和函数输出

函数

函数的代码形式

Solidity 中函数的形式(方括号中的是可写可不 写的关键字):

1
function <function name>(<parameter types>) {internal|external|public|private} [pure|view|payable] [returns (<return types>)]
  1. function:声明函数时的固定用法。要编写函数,就需要以 function 关键字开头。

  2. <function name>:函数名。

  3. (<parameter types>):圆括号内写入函数的参数,即输入到函数的变量类型和名称。

  4. {internal|external|public|private}:函数可见性说明符,共有4种。

    • public:内部和外部均可见。

    • private:只能从本合约内部访问,继承的合约也不能使用。

    • external:只能从合约外部访问(但内部可以通过 this.f() 来调用,f是函数名)。

    • internal: 只能从合约内部访问,继承的合约可以用。

    注意1⚠️:合约中定义的函数需要明确指定可见性,它们没有默认值。

    注意2⚠️:public|private|internal 也可用于修饰状态变量。public变量会自动生成同名的getter函数,用于查询数值。未标明可见性类型的状态变量,默认为internal

  5. [pure|view|payable]:决定函数权限/功能的关键字。payable(可支付的)很好理解,带着它的函数,运行的时候可以给合约转入 ETH。

  6. [returns (<return types>)]:函数返回的变量类型和名称。

到底什么是PureView

solidity 引入这两个关键字主要是因为 以太坊交易需要支付gas fee。合约的状态变量存储在链上,gas fee 很贵,如果计算不改变链上状态,就可以不用付 gas。包含 pureview 关键字的函数是不改写链上状态的,因此用户直接调用它们是不需要付 gas 的(注意⚠️,合约中非 pure/view 函数调用 pure/view 函数时需要付gas)。

在以太坊中,以下语句被视为修改链上状态:

  1. 写入状态变量。
  2. 释放事件。
  3. 创建其他合约。
  4. 使用 selfdestruct.
  5. 通过调用发送以太币。
  6. 调用任何未标记 viewpure 的函数。
  7. 使用低级调用(low-level calls)。
  8. 使用包含某些操作码的内联汇编。
  • pure函数既不能读取也不能写入链上的状态变量。
  • view函数能读取但不能写入状态变量。
  • pureview的函数既可以读取也可以写入状态变量。

代码测试

测试代码FunctionTypes.sol如下:

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
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.21;
contract FunctionTypes{
uint256 public number = 5;
// 默认function
function add() external{
number = number + 1;
}
// 验证pure和view
// function addtest() external view{
// number = number + 1;
// }

// pure
function addPure(uint256 _number) external pure returns(uint256 new_number){
new_number = _number + 1;
}
// view
function addView() external view returns(uint256 new_number) {
new_number = number + 1;
}
// internal: 内部函数
function minus() internal {
number = number - 1;
}
// 合约内的函数可以调用内部函数
function minusCall() external {
minus();
}
// payable: 递钱,能给合约支付eth的函数
function minusPayable() external payable returns(uint256 balance) {
minus();
balance = address(this).balance;
}
}
pure和view

在代码中定义了一个add()函数,每次调用会使得number加1。

1
2
3
4
// 默认function
function add() external{
number = number + 1;
}

如果 add() 函数被标记为 pure,比如 function add() external pure,就会报错。因为 pure 是不能读取合约里的状态变量的,更不能改写。

使用到了pure出现的报错,如下:

image-20250123030906176

如果使用view,则代码会报错,证实了view是只能读取而不能写入状态变量。

image-20250123031819323

pure 函数能做些什么?举个例子,你可以给函数传递一个参数 _number,然后让他返回 _number + 1,这个操作不会读取或写入状态变量。

如代码中的addPure

1
2
3
4
// pure
function addPure(uint256 _number) external pure returns(uint256 new_number){
new_number = _number + 1;
}

image-20250123031506521

view读取状态变量,然后对读取的结果进行加1。

1
2
3
4
// view
function addView() external view returns(uint256 new_number) {
new_number = number + 1;
}

image-20250123032230889

internal和external的对比

代码如下:

1
2
3
4
5
6
7
8
9
// internal: 内部函数
function minus() internal {
number = number - 1;
}

// 合约内的函数可以调用内部函数
function minusCall() external {
minus();
}

定义了一个 internalminus() 函数,每次调用使得 number 变量减少 1。由于 internal 函数只能由合约内部调用,我们必须再定义一个 externalminusCall() 函数,外部通过它间接调用内部的 minus() 函数。

image-20250123033818196

payable的使用
1
2
3
4
5
// payable: 递钱,能给合约支付eth的函数
function minusPayable() external payable returns(uint256 balance) {
minus();
balance = address(this).balance;
}

定义一个 external payableminusPayable() 函数,间接的调用 minus(),并且返回合约里的 ETH 余额(this 关键字可以让我们引用合约地址)。我们可以在调用 minusPayable() 时往合约里转入1个 ETH。

image-20250123034641393

函数输出

测试代码Return.sol如下:

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
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;
contract Return{
// 返回多个变量
function returnMultiple() public pure returns(uint256, bool, uint256[3] memory){
return(1, true, [uint256(1),2,5]);
}
// 命名式返回
function returnNamed() public pure returns(uint256 _number, bool _bool, uint256[3] memory _array){
_number = 2;
_bool = false;
_array = [uint256(3),2,1];
}
// 命名式返回,依然支持return
function returnNamed2() public pure returns(uint256 _number, bool _bool, uint256[3] memory _array){
return(1, true, [uint256(1),2,5]);
}
//读取返回值,解构式赋值
function readReturn() public pure{
//读取全部返回值
uint256 _number;
bool _bool;
bool _bool2;
uint256[3] memory _array;
(_number, _bool, _array) = returnNamed();

//读取部分返回值
(,_bool2,) = returnNamed();

}
}

返回值:return和returns

  • returns: 跟在函数名后面,用于声明返回的变量类型及变量名。
  • return: 用于函数主体中,返回指定的变量。
1
2
3
4
// 返回多个变量
function returnMultiple() public pure returns(uint256, bool, uint256[3] memory){
return(1, true, [uint256(1),2,5]);
}

在上述代码中,我们利用 returns 关键字声明了有多个返回值的 returnMultiple() 函数,然后我们在函数主体中使用 return(1, true, [uint256(1),2,5]) 确定了返回值。

这里uint256[3]声明了一个长度为3且类型为uint256的数组作为返回值。因为[1,2,3]会默认为uint8(3),因此[uint256(1),2,5]中首个元素必须强转uint256来声明该数组内的元素皆为此类型

image-20250123234741062

命名式返回

可以在 returns 中标明返回变量的名称。Solidity 会初始化这些变量,并且自动返回这些函数的值,无需使用 return

1
2
3
4
5
6
// 命名式返回
function returnNamed() public pure returns(uint256 _number, bool _bool, uint256[3] memory _array){
_number = 2;
_bool = false;
_array = [uint256(3),2,1];
}

在上述代码中,我们用 returns(uint256 _number, bool _bool, uint256[3] memory _array) 声明了返回变量类型以及变量名。这样,在主体中只需为变量 _number_bool_array 赋值,即可自动返回

也可以在命名式返回中用 return 来返回变量:

1
2
3
4
// 命名式返回,依然支持return
function returnNamed2() public pure returns(uint256 _number, bool _bool, uint256[3] memory _array){
return(1, true, [uint256(1),2,5]);
}

解构式复制

Solidity 支持使用解构式赋值规则来读取函数的全部或部分返回值。

  • 读取所有返回值:声明变量,然后将要赋值的变量用,隔开,按顺序排列。
1
2
3
4
uint256 _number;
bool _bool;
uint256[3] memory _array;
(_number, _bool, _array) = returnNamed();
  • 读取部分返回值:声明要读取的返回值对应的变量,不读取的留空。在下面的代码中,我们只读取_bool,而不读取返回的_number_array
1
(, _bool2, ) = returnNamed();

变量数据存储和作用域

测试代码DataStorage.sol,如下所示:

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

contract DataStorage {
// The data location of x is storage.
// This is the only place where the
// data location can be omitted.
uint[] public x = [1,2,3];

function fStorage() public{
//声明一个storage的变量xStorage,指向x。修改xStorage也会影响x
uint[] storage xStorage = x;
xStorage[0] = 100;
}

function fMemory() public view{
//声明一个Memory的变量xMemory,复制x。修改xMemory不会影响x
uint[] memory xMemory = x;
xMemory[0] = 100;
xMemory[1] = 200;
uint[] memory xMemory2 = x;
xMemory2[0] = 300;
}

function fCalldata(uint[] calldata _x) public pure returns(uint[] calldata){
//参数为calldata数组,不能被修改
// _x[0] = 0 //这样修改会报错
return(_x);
}
}

测试代码Variables.sol,如下图所示:

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
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.21;

contract Variables {
uint public x = 1;
uint public y;
string public z;

function foo() external{
// 可以在函数里更改状态变量的值
x = 5;
y = 2;
z = "0xAA";
}

function bar() external pure returns(uint){
uint xx = 1;
uint yy = 3;
uint zz = xx + yy;
return(zz);
}

function global() external view returns(address, uint, bytes memory){
address sender = msg.sender;
uint blockNum = block.number;
bytes memory data = msg.data;
return(sender, blockNum, data);
}

function weiUnit() external pure returns(uint) {
assert(1 wei == 1e0);
assert(1 wei == 1);
return 1 wei;
}

function gweiUnit() external pure returns(uint) {
assert(1 gwei == 1e9);
assert(1 gwei == 1000000000);
return 1 gwei;
}

function etherUnit() external pure returns(uint) {
assert(1 ether == 1e18);
assert(1 ether == 1000000000000000000);
return 1 ether;
}

function secondsUnit() external pure returns(uint) {
assert(1 seconds == 1);
return 1 seconds;
}

function minutesUnit() external pure returns(uint) {
assert(1 minutes == 60);
assert(1 minutes == 60 seconds);
return 1 minutes;
}

function hoursUnit() external pure returns(uint) {
assert(1 hours == 3600);
assert(1 hours == 60 minutes);
return 1 hours;
}

function daysUnit() external pure returns(uint) {
assert(1 days == 86400);
assert(1 days == 24 hours);
return 1 days;
}

function weeksUnit() external pure returns(uint) {
assert(1 weeks == 604800);
assert(1 weeks == 7 days);
return 1 weeks;
}
}

Solidity中的引用类型

引用类型(Reference Type):包括数组(array)和结构体(struct),由于这类变量比较复杂,占用存储空间大,我们在使用时必须要声明数据存储的位置。

数据位置

Solidity数据存储位置有三类:storagememorycalldata。不同存储位置的gas成本不同。

  • storage类型的数据存在链上,类似计算机的硬盘,消耗gas多;

  • memorycalldata类型的临时存在内存里,消耗gas少。

大致用法:

  1. storage:合约里的状态变量默认都是storage,存储在链上。
  2. memory:函数里的参数和临时变量一般用memory,存储在内存中,不上链。尤其是如果返回数据类型是变长的情况下,必须加memory修饰,例如:string, bytes, array和自定义结构。
  3. calldata:和memory类似,存储在内存中,不上链。与memory的不同点在于calldata变量不能修改(immutable),一般用于函数的参数。

calldata修饰的变量不能被修改,例子如下:

1
2
3
4
5
function fCalldata(uint[] calldata _x) public pure returns(uint[] calldata){
//参数为calldata数组,不能被修改
_x[0] = 0; //这样修改会报错
return(_x);
}

image-20250124163637660

数据位置和赋值规则

在不同存储类型相互赋值时候,有时会产生独立的副本(修改新变量不会影响原变量),有时会产生引用(修改新变量会影响原变量)。规则如下:

  1. 赋值本质上是创建引用指向本体,因此修改本体或者是引用,变化可以被同步。
  • storage(合约的状态变量)赋值给本地storage(函数里的)时候,会创建引用,改变新变量会影响原变量。样例如下:
1
2
3
4
5
6
7
uint[] x = [1,2,3]; // 状态变量:数组 x

function fStorage() public{
//声明一个storage的变量 xStorage,指向x。修改xStorage也会影响x
uint[] storage xStorage = x;
xStorage[0] = 100;
}

部署调用一下fStorage函数,然后利用交易的hash对代码进行调试,发现数值被更改。

image-20250124165024783

  • memory赋值给memory,会创建引用,改变新变量会影响原变量。
  1. 其他情况下,赋值创建的是本体的副本,即对二者之一的修改,并不会同步到另一方。

变量的作用域

Solidity中变量按作用域划分有三种,分别是状态变量(state variable)局部变量(local variable)全局变量(global variable)

状态变量(state variable)

状态变量是数据存储在链上的变量,所有合约内函数都可以访问,gas消耗高。状态变量在合约内、函数外声明:

1
2
3
4
5
contract Variables {
uint public x = 1;
uint public y;
string public z;
}

可以在函数里更改状态变量的值:

1
2
3
4
5
6
function foo() external{
// 可以在函数里更改状态变量的值
x = 5;
y = 2;
z = "0xAA";
}

局部变量(local variable)

局部变量是仅在函数执行过程中有效的变量,函数退出后,变量无效。局部变量的数据存储在内存里,不上链,gas低。

局部变量在函数内声明:

1
2
3
4
5
6
function bar() external pure returns(uint){
uint xx = 1;
uint yy = 3;
uint zz = xx + yy;
return(zz);
}

全局变量(global variable)

全局变量是全局范围工作的变量,都是solidity预留关键字。他们可以在函数内不声明直接使用:

1
2
3
4
5
6
function global() external view returns(address, uint, bytes memory){
address sender = msg.sender;
uint blockNum = block.number;
bytes memory data = msg.data;
return(sender, blockNum, data);
}

在上面例子里,我们使用了3个常用的全局变量:msg.senderblock.numbermsg.data,他们分别代表请求发起地址,当前区块高度,和请求数据。下面是一些常用的全局变量,更完整的列表请看这个链接

  • blockhash(uint blockNumber): (bytes32) 给定区块的哈希值 – 只适用于256最近区块, 不包含当前区块。
  • block.coinbase: (address payable) 当前区块矿工的地址
  • block.gaslimit: (uint) 当前区块的gaslimit
  • block.number: (uint) 当前区块的number
  • block.timestamp: (uint) 当前区块的时间戳,为unix纪元以来的秒
  • gasleft(): (uint256) 剩余 gas
  • msg.data: (bytes calldata) 完整call data
  • msg.sender: (address payable) 消息发送者 (当前 caller)
  • msg.sig: (bytes4) calldata的前四个字节 (function identifier)
  • msg.value: (uint) 当前交易发送的 wei
  • block.blobbasefee: (uint) 当前区块的blob基础费用。这是Cancun升级新增的全局变量。
  • blobhash(uint index): (bytes32) 返回跟当前交易关联的第 index 个blob的版本化哈希(第一个字节为版本号,当前为0x01,后面接KZG承诺的SHA256哈希的最后31个字节)。若当前交易不包含blob,则返回空字节。这是Cancun升级新增的全局变量。

image-20250124230136670

全局变量-以太单位与时间单位

以太单位

Solidity中不存在小数点,以0代替为小数点,来确保交易的精确度,并且防止精度的损失,利用以太单位可以避免误算的问题,方便程序员在合约中处理货币交易。

  • wei: 1
  • gwei: 1e9 = 1000000000
  • ether: 1e18 = 1000000000000000000

image-20250124232933314

时间单位

可以在合约中规定一个操作必须在一周内完成,或者某个事件在一个月后发生。这样就能让合约的执行可以更加精确,不会因为技术上的误差而影响合约的结果。因此,时间单位在Solidity中是一个重要的概念,有助于提高合约的可读性和可维护性。

  • seconds: 1
  • minutes: 60 seconds = 60
  • hours: 60 minutes = 3600
  • days: 24 hours = 86400
  • weeks: 7 days = 604800

image-20250124233108775

引用类型和映射类型

引用类型

这一讲,我们将介绍Solidity中的两个重要变量类型:数组(array)和结构体(struct)。

数组array

数组(Array)是Solidity常用的一种变量类型,用来存储一组数据(整数,字节,地址等等)。数组分为固定长度数组和可变长度数组两种:

  • 固定长度数组:在声明时指定数组的长度。用T[k]的格式声明,其中T是元素的类型,k是长度,例如:
1
2
3
4
// 固定长度 Array
uint[8] array1;
bytes1[5] array2;
address[100] array3;
  • 可变长度数组(动态数组):在声明时不指定数组的长度。用T[]的格式声明,其中T是元素的类型,例如:
1
2
3
4
5
// 可变长度 Array
uint[] array4;
bytes1[] array5;
address[] array6;
bytes array7;

注意⚠️bytes比较特殊,是数组,但是不用加[]。另外,不能用byte[]声明单字节数组,可以使用bytesbytes1[]bytesbytes1[] 省gas。

创建数组的规则

在Solidity里,创建数组有一些规则:

  • 对于memory修饰的动态数组,可以用new操作符来创建,但是必须声明长度,并且声明后长度不能改变。例子:
1
2
3
// memory动态数组
uint[] memory array8 = new uint[](5);
bytes memory array9 = new bytes(9);
  • 数组字面常数(Array Literals)是写作表达式形式的数组,用方括号包着来初始化array的一种方式,并且里面每一个元素的type是以第一个元素为准的,例如[1,2,3]里面所有的元素都是uint8类型,因为在Solidity中,如果一个值没有指定type的话,会根据上下文推断出元素的类型,默认就是最小单位的type,这里默认最小单位类型是uint8。而[uint(1),2,3]里面的元素都是uint类型,因为第一个元素指定了是uint类型了,里面每一个元素的type都以第一个元素为准。

    下面的例子中,如果没有对传入 g() 函数的数组进行 uint 转换,是会报错的。

1
2
3
4
5
6
7
8
9
10
11
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.16 <0.9.0;

contract C {
function f() public pure {
g([uint(1), 2, 3]);
}
function g(uint[3] memory _data) public pure {
// ...
}
}
  • 如果创建的是动态数组,你需要一个一个元素的赋值。
1
2
3
4
uint[] memory x = new uint[](3);
x[0] = 1;
x[1] = 3;
x[2] = 4;
数组成员
  • length: 数组有一个包含元素数量的length成员,memory数组的长度在创建后是固定的。
  • push(): 动态数组拥有push()成员,可以在数组最后添加一个0元素,并返回该元素的引用
  • push(x): 动态数组拥有push(x)成员,可以在数组最后添加一个x元素
  • pop(): 动态数组拥有pop()成员,可以移除数组最后一个元素。

image-20250124234418473

结构体struct

此时代码StructTypes.sol,代码如下:

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
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.21;
contract StructTypes {
// 结构体 Struct
struct Student{
uint256 id;
uint256 score;
}
Student student; // 初始一个student结构体
// 给结构体赋值
// 方法1:在函数中创建一个storage的struct引用
function initStudent1() external{
Student storage _student = student; // assign a copy of student
_student.id = 11;
_student.score = 100;
}

// 方法2:直接引用状态变量的struct
function initStudent2() external{
student.id = 1;
student.score = 80;
}

// 方法3:构造函数式
function initStudent3() external {
student = Student(3, 90);
}

// 方法4:key value
function initStudent4() external {
student = Student({id: 4, score: 60});
}
}

Solidity支持通过构造结构体的形式定义新的类型。结构体中的元素可以是原始类型,也可以是引用类型;结构体可以作为数组或映射的元素。创建结构体的方法:

1
2
3
4
5
6
7
// 结构体
struct Student{
uint256 id;
uint256 score;
}

Student student; // 初始一个student结构体

给结构体赋值的四种方法:

1
2
3
4
5
6
7
//  给结构体赋值
// 方法1:在函数中创建一个storage的struct引用
function initStudent1() external{
Student storage _student = student; // assign a copy of student
_student.id = 11;
_student.score = 100;
}
1
2
3
4
5
// 方法2:直接引用状态变量的struct
function initStudent2() external{
student.id = 1;
student.score = 80;
}
1
2
3
4
// 方法3:构造函数式
function initStudent3() external {
student = Student(3, 90);
}
1
2
3
4
// 方法4:key value
function initStudent4() external {
student = Student({id: 4, score: 60});
}

映射类型

这一讲,我们将介绍映射(Mapping)类型,Solidity中存储键值对的数据结构,可以理解为哈希表。

测试代码Mapping.sol,代码如下所示:

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.21;
contract Mapping {
mapping(uint => address) public idToAddress; // id映射到地址
mapping(address => address) public swapPair; // 币对的映射,地址到地址

// 规则1. _KeyType不能是自定义的 下面这个例子会报错
// 我们定义一个结构体 Struct
// struct Student{
// uint256 id;
// uint256 score;
//}
// mapping(Struct => uint) public testVar;

function writeMap (uint _Key, address _Value) public{
idToAddress[_Key] = _Value;
}
}

映射Mapping

在映射中,人们可以通过键(Key)来查询对应的值(Value),比如:通过一个人的id来查询他的钱包地址。

声明映射的格式为mapping(_KeyType => _ValueType),其中_KeyType_ValueType分别是KeyValue的变量类型。例子:

1
2
mapping(uint => address) public idToAddress; // id映射到地址
mapping(address => address) public swapPair; // 币对的映射,地址到地址

映射的规则

  • 规则1:**映射的_KeyType只能选择Solidity内置的值类型,比如uintaddress等,不能用自定义的结构体。而_ValueType可以使用自定义的类型。**下面这个例子会报错,因为_KeyType使用了我们自定义的结构体:
1
2
3
4
5
6
// 我们定义一个结构体 Struct
struct Student{
uint256 id;
uint256 score;
}
mapping(Student => uint) public testVar;
  • 规则2:映射的存储位置必须是storage,因此可以用于合约的状态变量,函数中的storage变量和library函数的参数(见例子)。不能用于public函数的参数或返回结果中,因为mapping记录的是一种关系 (key - value pair)。

  • 规则3:如果映射声明为public,那么Solidity会自动给你创建一个getter函数,可以通过Key来查询对应的Value

  • 规则4:给映射新增的键值对的语法为_Var[_Key] = _Value,其中_Var是映射变量名,_Key_Value对应新增的键值对。例子:

1
2
3
function writeMap (uint _Key, address _Value) public{
idToAddress[_Key] = _Value;
}

写入mapping,并读取。

image-20250125004147867

image-20250125004238778

映射的原理

  • 原理1: 映射不储存任何键(Key)的资讯,也没有length的资讯。
  • 原理2: 映射使用keccak256(abi.encodePacked(key, slot))当成offset存取value,其中slot是映射变量定义所在的插槽位置。
  • 原理3: 因为Ethereum会定义所有未使用的空间为0,所以未赋值(Value)的键(Key)初始值都是各个type的默认值,如uint的默认值是0。

变量初始化

Solidity中,声明但没赋值的变量都有它的初始值或默认值。这一讲,我们将介绍常用变量的初始值。

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

contract InitialValue {
// Value Types
bool public _bool; // false
string public _string; // ""
int public _int; // 0
uint public _uint; // 0
address public _address; // 0x0000000000000000000000000000000000000000

enum ActionSet { Buy, Hold, Sell}
ActionSet public _enum; // 第一个元素 0

function fi() internal{} // internal空白方程
function fe() external{} // external空白方程

// Reference Types
uint[8] public _staticArray; // 所有成员设为其默认值的静态数组[0,0,0,0,0,0,0,0]
uint[] public _dynamicArray; // `[]`
mapping(uint => address) public _mapping; // 所有元素都为其默认值的mapping
// 所有成员设为其默认值的结构体 0, 0
struct Student{
uint256 id;
uint256 score;
}
Student public student;

// delete操作符
bool public _bool2 = true;
function d() external {
delete _bool2; // delete 会让_bool2变为默认值,false
}
}

值类型初始值

  • boolean: false

  • string: ""

  • int: 0

  • uint: 0

  • enum: 枚举中的第一个元素

  • address: 0x0000000000000000000000000000000000000000 (或 address(0))

  • function

    • internal: 空白函数

    • external: 空白函数

可以用public变量的getter函数验证上面写的初始值是否正确:

1
2
3
4
5
6
7
8
9
10
11
bool public _bool; // false
string public _string; // ""
int public _int; // 0
uint public _uint; // 0
address public _address; // 0x0000000000000000000000000000000000000000

enum ActionSet { Buy, Hold, Sell}
ActionSet public _enum; // 第1个内容Buy的索引0

function fi() internal{} // internal空白函数
function fe() external{} // external空白函数

引用类型初始值

  • 映射mapping: 所有元素都为其默认值的mapping
  • 结构体struct: 所有成员设为其默认值的结构体
  • 数组array
    • 动态数组: []
    • 静态数组(定长): 所有成员设为其默认值的静态数组

可以用public变量的getter函数验证上面写的初始值是否正确:

1
2
3
4
5
6
7
8
9
10
// Reference Types
uint[8] public _staticArray; // 所有成员设为其默认值的静态数组[0,0,0,0,0,0,0,0]
uint[] public _dynamicArray; // `[]`
mapping(uint => address) public _mapping; // 所有元素都为其默认值的mapping
// 所有成员设为其默认值的结构体 0, 0
struct Student{
uint256 id;
uint256 score;
}
Student public student;

delete操作符

delete a会让变量a的值变为初始值。

1
2
3
4
5
// delete操作符
bool public _bool2 = true;
function d() external {
delete _bool2; // delete 会让_bool2变为默认值,false
}

值类型、引用类型delete操作后的默认值。

常数

这一讲,我们介绍Solidity中和常量相关的两个关键字,constant(常量)和immutable(不变量)。状态变量声明这两个关键字之后,不能在初始化后更改数值。这样做的好处是提升合约的安全性并节省gas

另外,只有数值变量可以声明constantimmutablestringbytes可以声明为constant,但不能为immutable

测试代码Constant.sol,代码如下:

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
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.21;
contract Constant {
// constant变量必须在声明的时候初始化,之后不能改变
uint256 public constant CONSTANT_NUM = 10;
string public constant CONSTANT_STRING = "0xAA";
bytes public constant CONSTANT_BYTES = "WTF";
address public constant CONSTANT_ADDRESS = 0x0000000000000000000000000000000000000000;

// immutable变量可以在constructor里初始化,之后不能改变
uint256 public immutable IMMUTABLE_NUM = 9999999999;
address public immutable IMMUTABLE_ADDRESS;
uint256 public immutable IMMUTABLE_BLOCK;
uint256 public immutable IMMUTABLE_TEST;

// 利用constructor初始化immutable变量,因此可以利用
constructor(){
IMMUTABLE_ADDRESS = address(this);
IMMUTABLE_NUM = 1118;
IMMUTABLE_TEST = test();
}

function test() public pure returns(uint256){
uint256 what = 9;
return(what);
}
}

constant和immutable

constant

constant变量必须在声明的时候初始化,之后再也不能改变。尝试改变的话,编译不通过。

1
2
3
4
5
// constant变量必须在声明的时候初始化,之后不能改变
uint256 constant CONSTANT_NUM = 10;
string constant CONSTANT_STRING = "0xAA";
bytes constant CONSTANT_BYTES = "WTF";
address constant CONSTANT_ADDRESS = 0x0000000000000000000000000000000000000000;

immutable

immutable变量可以在声明时或构造函数中初始化,因此更加灵活。在Solidity v8.0.21以后,immutable变量不需要显式初始化。反之,则需要显式初始化。 若immutable变量既在声明时初始化,又在constructor中初始化,会使用constructor初始化的值。

1
2
3
4
5
// immutable变量可以在constructor里初始化,之后不能改变
uint256 public immutable IMMUTABLE_NUM = 9999999999;
address public immutable IMMUTABLE_ADDRESS;
uint256 public immutable IMMUTABLE_BLOCK;
uint256 public immutable IMMUTABLE_TEST;

你可以使用全局变量例如address(this)block.number 或者自定义的函数给immutable变量初始化。在下面这个例子,我们利用了test()函数给IMMUTABLE_TEST初始化为9

1
2
3
4
5
6
7
8
9
10
11
// 利用constructor初始化immutable变量,因此可以利用
constructor(){
IMMUTABLE_ADDRESS = address(this);
IMMUTABLE_NUM = 1118;
IMMUTABLE_TEST = test();
}

function test() public pure returns(uint256){
uint256 what = 9;
return(what);
}

image-20250125011902342

参考

https://www.wtf.academy/docs/solidity-101/HelloWeb3

https://github.com/AmazingAng/WTF-Solidity