Hello Web3
Solidity简介
Solidity是一种用于编写以太坊虚拟机(EVM)智能合约的编程语言。
Hello Web3
简单的程序HelloWeb3.sol如下:
1 | // SPDX-License-Identifier: MIT |
使用到https://remix.ethereum.org/
进行编译和部署,点击_string输出。

代码中的
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中的变量类型
- 值类型(Value Type):包括布尔型、整数型等等,这类变量赋值时候直接传递数值。
- 引用类型(Reference Type):包括数组和结构体,这类变量占空间大,赋值时候直接传递地址(类似指针)。
- 映射类型(Mapping Type):Solidity中存储键值对的数据结构,可以理解为哈希表。
编写一个简单的程序ValueTypes.sol,介绍一下各种变量类型,程序如下:
1 | // SPDX-License-Identifier: MIT |
地址类型
地址类型(address)有两类:
- 普通地址(address): 存储一个 20 字节的值(以太坊地址的大小)。
- payable address: 比普通地址多了
transfer和send两个成员方法,用于接收转账。
定长字节数组
字节数组分为定长和不定长两种:
- 定长字节数组: 属于值类型,数组长度在声明之后不能改变。根据字节数组的长度分为
bytes1,bytes8,bytes32等类型。定长字节数组最多存储 32 bytes 数据,即bytes32。 - 不定长字节数组: 属于引用类型,数组长度在声明之后可以改变,包括
bytes等。
1 | // 固定长度的字节数组 |
在上述代码中,MiniSolidity 变量以字节的方式存储进变量 _byte32。如果把它转换成 16 进制,就是:0x4d696e69536f6c69646974790000000000000000000000000000000000000000
_byte 变量的值为 _byte32 的第一个字节,即 0x4d。

枚举enum
枚举(enum)是 Solidity 中用户定义的数据类型。它主要用于为 uint 分配名称,使程序易于阅读和维护。它与 C 语言 中的 enum 类似,使用名称来代替从 0 开始的 uint:
1 | // 用enum将uint 0, 1, 2表示为Buy, Hold, Sell |
枚举可以显式地和 uint 相互转换,并会检查转换的正整数是否在枚举的长度内,否则会报错:
1 | // enum可以和uint显式的转换 |
enum 是一个比较冷门的变量,几乎没什么人用。
enum和uint相互转换,如下:

函数和函数输出
函数
函数的代码形式
Solidity 中函数的形式(方括号中的是可写可不 写的关键字):
1 | function <function name>(<parameter types>) {internal|external|public|private} [pure|view|payable] [returns (<return types>)] |
-
function:声明函数时的固定用法。要编写函数,就需要以function关键字开头。 -
<function name>:函数名。 -
(<parameter types>):圆括号内写入函数的参数,即输入到函数的变量类型和名称。 -
{internal|external|public|private}:函数可见性说明符,共有4种。-
public:内部和外部均可见。 -
private:只能从本合约内部访问,继承的合约也不能使用。 -
external:只能从合约外部访问(但内部可以通过this.f()来调用,f是函数名)。 -
internal: 只能从合约内部访问,继承的合约可以用。
注意1⚠️:合约中定义的函数需要明确指定可见性,它们没有默认值。
注意2⚠️:
public|private|internal也可用于修饰状态变量。public变量会自动生成同名的getter函数,用于查询数值。未标明可见性类型的状态变量,默认为internal。 -
-
[pure|view|payable]:决定函数权限/功能的关键字。payable(可支付的)很好理解,带着它的函数,运行的时候可以给合约转入 ETH。 -
[returns (<return types>)]:函数返回的变量类型和名称。
到底什么是Pure和View?
solidity 引入这两个关键字主要是因为 以太坊交易需要支付gas fee。合约的状态变量存储在链上,gas fee 很贵,如果计算不改变链上状态,就可以不用付 gas。包含 pure 和 view 关键字的函数是不改写链上状态的,因此用户直接调用它们是不需要付 gas 的(注意⚠️,合约中非 pure/view 函数调用 pure/view 函数时需要付gas)。
在以太坊中,以下语句被视为修改链上状态:
- 写入状态变量。
- 释放事件。
- 创建其他合约。
- 使用
selfdestruct. - 通过调用发送以太币。
- 调用任何未标记
view或pure的函数。 - 使用低级调用(low-level calls)。
- 使用包含某些操作码的内联汇编。
pure函数既不能读取也不能写入链上的状态变量。view函数能读取但不能写入状态变量。- 非
pure或view的函数既可以读取也可以写入状态变量。
代码测试
测试代码FunctionTypes.sol如下:
1 | // SPDX-License-Identifier: MIT |
pure和view
在代码中定义了一个add()函数,每次调用会使得number加1。
1 | // 默认function |
如果 add() 函数被标记为 pure,比如 function add() external pure,就会报错。因为 pure 是不能读取合约里的状态变量的,更不能改写。
使用到了pure出现的报错,如下:

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

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

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

internal和external的对比
代码如下:
1 | // internal: 内部函数 |
定义了一个 internal 的 minus() 函数,每次调用使得 number 变量减少 1。由于 internal 函数只能由合约内部调用,我们必须再定义一个 external 的 minusCall() 函数,外部通过它间接调用内部的 minus() 函数。

payable的使用
1 | // payable: 递钱,能给合约支付eth的函数 |
定义一个 external payable 的 minusPayable() 函数,间接的调用 minus(),并且返回合约里的 ETH 余额(this 关键字可以让我们引用合约地址)。我们可以在调用 minusPayable() 时往合约里转入1个 ETH。

函数输出
测试代码Return.sol如下:
1 | // SPDX-License-Identifier: MIT |
返回值:return和returns
returns: 跟在函数名后面,用于声明返回的变量类型及变量名。return: 用于函数主体中,返回指定的变量。
1 | // 返回多个变量 |
在上述代码中,我们利用 returns 关键字声明了有多个返回值的 returnMultiple() 函数,然后我们在函数主体中使用 return(1, true, [uint256(1),2,5]) 确定了返回值。
这里uint256[3]声明了一个长度为3且类型为uint256的数组作为返回值。因为[1,2,3]会默认为uint8(3),因此[uint256(1),2,5]中首个元素必须强转uint256来声明该数组内的元素皆为此类型。

命名式返回
可以在 returns 中标明返回变量的名称。Solidity 会初始化这些变量,并且自动返回这些函数的值,无需使用 return
1 | // 命名式返回 |
在上述代码中,我们用 returns(uint256 _number, bool _bool, uint256[3] memory _array) 声明了返回变量类型以及变量名。这样,在主体中只需为变量 _number、_bool和_array 赋值,即可自动返回。
也可以在命名式返回中用 return 来返回变量:
1 | // 命名式返回,依然支持return |
解构式复制
Solidity 支持使用解构式赋值规则来读取函数的全部或部分返回值。
- 读取所有返回值:声明变量,然后将要赋值的变量用
,隔开,按顺序排列。
1 | uint256 _number; |
- 读取部分返回值:声明要读取的返回值对应的变量,不读取的留空。在下面的代码中,我们只读取
_bool,而不读取返回的_number和_array:
1 | (, _bool2, ) = returnNamed(); |
变量数据存储和作用域
测试代码DataStorage.sol,如下所示:
1 | // SPDX-License-Identifier: MIT |
测试代码Variables.sol,如下图所示:
1 | // SPDX-License-Identifier: MIT |
Solidity中的引用类型
引用类型(Reference Type):包括数组(array)和结构体(struct),由于这类变量比较复杂,占用存储空间大,我们在使用时必须要声明数据存储的位置。
数据位置
Solidity数据存储位置有三类:storage,memory和calldata。不同存储位置的gas成本不同。
-
storage类型的数据存在链上,类似计算机的硬盘,消耗gas多; -
memory和calldata类型的临时存在内存里,消耗gas少。
大致用法:
storage:合约里的状态变量默认都是storage,存储在链上。memory:函数里的参数和临时变量一般用memory,存储在内存中,不上链。尤其是如果返回数据类型是变长的情况下,必须加memory修饰,例如:string, bytes, array和自定义结构。calldata:和memory类似,存储在内存中,不上链。与memory的不同点在于calldata变量不能修改(immutable),一般用于函数的参数。
calldata修饰的变量不能被修改,例子如下:
1 | function fCalldata(uint[] calldata _x) public pure returns(uint[] calldata){ |

数据位置和赋值规则
在不同存储类型相互赋值时候,有时会产生独立的副本(修改新变量不会影响原变量),有时会产生引用(修改新变量会影响原变量)。规则如下:
- 赋值本质上是创建引用指向本体,因此修改本体或者是引用,变化可以被同步。
storage(合约的状态变量)赋值给本地storage(函数里的)时候,会创建引用,改变新变量会影响原变量。样例如下:
1 | uint[] x = [1,2,3]; // 状态变量:数组 x |
部署调用一下fStorage函数,然后利用交易的hash对代码进行调试,发现数值被更改。

memory赋值给memory,会创建引用,改变新变量会影响原变量。
- 其他情况下,赋值创建的是本体的副本,即对二者之一的修改,并不会同步到另一方。
变量的作用域
Solidity中变量按作用域划分有三种,分别是状态变量(state variable),局部变量(local variable)和全局变量(global variable)
状态变量(state variable)
状态变量是数据存储在链上的变量,所有合约内函数都可以访问,gas消耗高。状态变量在合约内、函数外声明:
1 | contract Variables { |
可以在函数里更改状态变量的值:
1 | function foo() external{ |
局部变量(local variable)
局部变量是仅在函数执行过程中有效的变量,函数退出后,变量无效。局部变量的数据存储在内存里,不上链,gas低。
局部变量在函数内声明:
1 | function bar() external pure returns(uint){ |
全局变量(global variable)
全局变量是全局范围工作的变量,都是solidity预留关键字。他们可以在函数内不声明直接使用:
1 | function global() external view returns(address, uint, bytes memory){ |
在上面例子里,我们使用了3个常用的全局变量:msg.sender,block.number和msg.data,他们分别代表请求发起地址,当前区块高度,和请求数据。下面是一些常用的全局变量,更完整的列表请看这个链接:
blockhash(uint blockNumber): (bytes32) 给定区块的哈希值 – 只适用于256最近区块, 不包含当前区块。block.coinbase: (address payable) 当前区块矿工的地址block.gaslimit: (uint) 当前区块的gaslimitblock.number: (uint) 当前区块的numberblock.timestamp: (uint) 当前区块的时间戳,为unix纪元以来的秒gasleft(): (uint256) 剩余 gasmsg.data: (bytes calldata) 完整call datamsg.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升级新增的全局变量。

全局变量-以太单位与时间单位
以太单位
Solidity中不存在小数点,以0代替为小数点,来确保交易的精确度,并且防止精度的损失,利用以太单位可以避免误算的问题,方便程序员在合约中处理货币交易。
wei: 1gwei: 1e9 = 1000000000ether: 1e18 = 1000000000000000000

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

引用类型和映射类型
引用类型
这一讲,我们将介绍Solidity中的两个重要变量类型:数组(array)和结构体(struct)。
数组array
数组(Array)是Solidity常用的一种变量类型,用来存储一组数据(整数,字节,地址等等)。数组分为固定长度数组和可变长度数组两种:
- 固定长度数组:在声明时指定数组的长度。用
T[k]的格式声明,其中T是元素的类型,k是长度,例如:
1 | // 固定长度 Array |
- 可变长度数组(动态数组):在声明时不指定数组的长度。用
T[]的格式声明,其中T是元素的类型,例如:
1 | // 可变长度 Array |
注意⚠️:bytes比较特殊,是数组,但是不用加[]。另外,不能用byte[]声明单字节数组,可以使用bytes或bytes1[]。bytes 比 bytes1[] 省gas。
创建数组的规则
在Solidity里,创建数组有一些规则:
- 对于
memory修饰的动态数组,可以用new操作符来创建,但是必须声明长度,并且声明后长度不能改变。例子:
1 | // memory动态数组 |
-
数组字面常数(Array Literals)是写作表达式形式的数组,用方括号包着来初始化array的一种方式,并且里面每一个元素的type是以第一个元素为准的,例如
[1,2,3]里面所有的元素都是uint8类型,因为在Solidity中,如果一个值没有指定type的话,会根据上下文推断出元素的类型,默认就是最小单位的type,这里默认最小单位类型是uint8。而[uint(1),2,3]里面的元素都是uint类型,因为第一个元素指定了是uint类型了,里面每一个元素的type都以第一个元素为准。下面的例子中,如果没有对传入
g()函数的数组进行uint转换,是会报错的。
1 | // SPDX-License-Identifier: GPL-3.0 |
- 如果创建的是动态数组,你需要一个一个元素的赋值。
1 | uint[] memory x = new uint[](3); |
数组成员
length: 数组有一个包含元素数量的length成员,memory数组的长度在创建后是固定的。push():动态数组拥有push()成员,可以在数组最后添加一个0元素,并返回该元素的引用。push(x):动态数组拥有push(x)成员,可以在数组最后添加一个x元素。pop():动态数组拥有pop()成员,可以移除数组最后一个元素。

结构体struct
此时代码StructTypes.sol,代码如下:
1 | // SPDX-License-Identifier: MIT |
Solidity支持通过构造结构体的形式定义新的类型。结构体中的元素可以是原始类型,也可以是引用类型;结构体可以作为数组或映射的元素。创建结构体的方法:
1 | // 结构体 |
给结构体赋值的四种方法:
1 | // 给结构体赋值 |
1 | // 方法2:直接引用状态变量的struct |
1 | // 方法3:构造函数式 |
1 | // 方法4:key value |
映射类型
这一讲,我们将介绍映射(Mapping)类型,Solidity中存储键值对的数据结构,可以理解为哈希表。
测试代码Mapping.sol,代码如下所示:
1 | // SPDX-License-Identifier: MIT |
映射Mapping
在映射中,人们可以通过键(Key)来查询对应的值(Value),比如:通过一个人的id来查询他的钱包地址。
声明映射的格式为mapping(_KeyType => _ValueType),其中_KeyType和_ValueType分别是Key和Value的变量类型。例子:
1 | mapping(uint => address) public idToAddress; // id映射到地址 |
映射的规则
- 规则1:**映射的
_KeyType只能选择Solidity内置的值类型,比如uint,address等,不能用自定义的结构体。而_ValueType可以使用自定义的类型。**下面这个例子会报错,因为_KeyType使用了我们自定义的结构体:
1 | // 我们定义一个结构体 Struct |
-
规则2:映射的存储位置必须是
storage,因此可以用于合约的状态变量,函数中的storage变量和library函数的参数(见例子)。不能用于public函数的参数或返回结果中,因为mapping记录的是一种关系 (key - value pair)。 -
规则3:如果映射声明为
public,那么Solidity会自动给你创建一个getter函数,可以通过Key来查询对应的Value。 -
规则4:给映射新增的键值对的语法为
_Var[_Key] = _Value,其中_Var是映射变量名,_Key和_Value对应新增的键值对。例子:
1 | function writeMap (uint _Key, address _Value) public{ |
写入mapping,并读取。


映射的原理
- 原理1: 映射不储存任何键(
Key)的资讯,也没有length的资讯。 - 原理2: 映射使用
keccak256(abi.encodePacked(key, slot))当成offset存取value,其中slot是映射变量定义所在的插槽位置。 - 原理3: 因为Ethereum会定义所有未使用的空间为0,所以未赋值(
Value)的键(Key)初始值都是各个type的默认值,如uint的默认值是0。
变量初始化
在Solidity中,声明但没赋值的变量都有它的初始值或默认值。这一讲,我们将介绍常用变量的初始值。
1 | // SPDX-License-Identifier: MIT |
值类型初始值
-
boolean:false -
string:"" -
int:0 -
uint:0 -
enum: 枚举中的第一个元素 -
address:0x0000000000000000000000000000000000000000(或address(0)) -
function-
internal: 空白函数 -
external: 空白函数
-
可以用public变量的getter函数验证上面写的初始值是否正确:
1 | bool public _bool; // false |
引用类型初始值
- 映射
mapping: 所有元素都为其默认值的mapping - 结构体
struct: 所有成员设为其默认值的结构体 - 数组
array- 动态数组:
[] - 静态数组(定长): 所有成员设为其默认值的静态数组
- 动态数组:
可以用public变量的getter函数验证上面写的初始值是否正确:
1 | // Reference Types |
delete操作符
delete a会让变量a的值变为初始值。
1 | // delete操作符 |
值类型、引用类型delete操作后的默认值。
常数
这一讲,我们介绍Solidity中和常量相关的两个关键字,constant(常量)和immutable(不变量)。状态变量声明这两个关键字之后,不能在初始化后更改数值。这样做的好处是提升合约的安全性并节省gas。
另外,只有数值变量可以声明constant和immutable;string和bytes可以声明为constant,但不能为immutable。
测试代码Constant.sol,代码如下:
1 | // SPDX-License-Identifier: MIT |
constant和immutable
constant
constant变量必须在声明的时候初始化,之后再也不能改变。尝试改变的话,编译不通过。
1 | // constant变量必须在声明的时候初始化,之后不能改变 |
immutable
immutable变量可以在声明时或构造函数中初始化,因此更加灵活。在Solidity v8.0.21以后,immutable变量不需要显式初始化。反之,则需要显式初始化。 若immutable变量既在声明时初始化,又在constructor中初始化,会使用constructor初始化的值。
1 | // immutable变量可以在constructor里初始化,之后不能改变 |
你可以使用全局变量例如address(this),block.number 或者自定义的函数给immutable变量初始化。在下面这个例子,我们利用了test()函数给IMMUTABLE_TEST初始化为9:
1 | // 利用constructor初始化immutable变量,因此可以利用 |
