1 Hello Web3
开发工具:Remix
我们将使用 Remix
运行 Solidity
合约。Remix
是以太坊官方推荐的智能合约集成开发环境(IDE),适合新手,可以在浏览器中快速开发和部署合约,无需在本地安装任何程序。
在 Remix
中,左侧菜单有按钮,对应文件(编写代码)、编译(运行代码)和部署(将合约部署到链上)。
点击“创建新文件”(Create New File
)按钮,即可创建一个空白的 Solidity
合约。
将文件命名为HelloWeb3.sol
第一个 Solidity 程序
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.21;
contract HelloWeb3{
string public _string = "Hello Web3!";
}
第 1 行是注释,说明代码所使用的软件许可(license),这里使用的是 MIT 许可。如果不写许可,编译时会出现警告(warning),但程序仍可运行。Solidity 注释以“//”开头,后面跟注释内容,注释不会被程序执行。
第 2 行声明源文件所使用的 Solidity 版本,因为不同版本的语法有差异。这行代码表示源文件将不允许小于 0.8.21 版本或大于等于 0.9.0 的编译器编译(第二个条件由 ^
提供)。Solidity 语句以分号(;)结尾。
第 3-4 行是合约部分。第 3 行创建合约(contract),并声明合约名为 HelloWeb3
。第 4 行是合约内容,声明了一个 string(字符串)变量 _string
,并赋值为 "Hello Web3!"。
编译并部署代码

在 Remix 编辑代码的页面,按 Ctrl + S 即可编译代码,非常方便。
编译完成后,点击左侧菜单的“部署”按钮,进入部署(Deploy)页面。
默认情况下,Remix
会使用 Remix
虚拟机(以前称为 JavaScript 虚拟机)来模拟以太坊链,运行智能合约,类似在浏览器里运行一条测试链。Remix
还会为你分配一些测试账户,每个账户里有 100 ETH(测试代币),随意使用。点击 Deploy
(黄色按钮),即可部署我们编写的合约。
部署成功后,在下方会看到名为 HelloWeb3
的合约。点击 _string
,即可看到 "Hello Web3!"。
2 值类型
-
值类型(Value Type) :包括布尔型,整数型等等,这类变量赋值时候直接传递数值。
-
引用类型(Reference Type) :包括数组和结构体,这类变量占空间大,赋值时候直接传递地址(类似指针)。
-
映射类型(Mapping Type) : Solidity中存储键值对的数据结构,可以理解为哈希表
1. 布尔型
布尔型是二值变量,取值为 true
或 false
。
// 布尔值
bool public _bool = true;
布尔值的运算符包括:
!
(逻辑非)&&
(逻辑与,"and")||
(逻辑或,"or")==
(等于)!=
(不等于)
值得注意的是: &&
和 ||
运算符遵循短路规则,这意味着,假如存在 f(x) || g(y)
的表达式,如果 f(x)
是 true
,g(y)
不会被计算,即使它和 f(x)
的结果是相反的。假如存在f(x) && g(y)
的表达式,如果 f(x)
是 false
,g(y)
不会被计算。 所谓“短路规则”,一般出现在逻辑与(&&)和逻辑或(||)中。 当逻辑与(&&)的第一个条件为false时,就不会再去判断第二个条件; 当逻辑或(||)的第一个条件为true时,就不会再去判断第二个条件,这就是短路规则。
2. 整型
整型是 Solidity 中的整数,最常用的包括:
// 整型
int public _int = -1; // 整数,包括负数
uint public _uint = 1; // 无符号整数
uint256 public _number = 20220330; // 256位无符号整数
常用的整型运算符包括:
- 比较运算符(返回布尔值):
<=
,<
,==
,!=
,>=
,>
- 算术运算符:
+
,-
,*
,/
,%
(取余),**
(幂)
3. 地址类型
地址类型(address)有两类:
- 普通地址(address): 存储一个 20 字节的值(以太坊地址的大小)。
- payable address: 比普通地址多了
transfer
和send
两个成员方法,用于接收转账。
// 地址
address public _address = 0x7A58c0Be72BE218B41C608b7Fe7C5bB630736C71;
address payable public _address1 = payable(_address); // payable address,可以转账、查余额
4. 定长字节数组
字节数组分为定长和不定长两种:
- 定长字节数组: 属于值类型,数组长度在声明之后不能改变。根据字节数组的长度分为
bytes1
,bytes8
,bytes32
等类型。定长字节数组最多存储 32 bytes 数据,即bytes32
。 - 不定长字节数组: 属于引用类型,数组长度在声明之后可以改变,包括
bytes
等。
// 固定长度的字节数组
bytes32 public _byte32 = "MiniSolidity";
bytes1 public _byte = _byte32[0];
在上述代码中,字符串 MiniSolidity
以字节的方式存储进变量 _byte32
。如果把它转换成 16 进制
,就是:0x4d696e69536f6c69646974790000000000000000000000000000000000000000
_byte
变量的值为 _byte32
的第一个字节,即 0x4d
。
5. 枚举 enum
枚举(enum
)是 Solidity 中用户定义的数据类型。它主要用于为 uint
分配名称,使程序易于阅读和维护。它与 C 语言
中的 enum
类似,使用名称来代替从 0
开始的 uint
:
// 用enum将uint 0, 1, 2表示为Buy, Hold, Sell
enum ActionSet { Buy, Hold, Sell }
// 创建enum变量 action
ActionSet action = ActionSet.Buy;
枚举可以显式地和 uint
相互转换,并会检查转换的无符号整数是否在枚举的长度内,否则会报错:
// enum可以和uint显式的转换
function enumToUint() external view returns(uint){
return uint(action);
}
在 Remix 上运行
- 部署合约后可以查看每个类型的变量的数值:
enum
和 uint
转换的示例:
3 函数
我们先看一下 Solidity 中函数的形式(方括号中的是可写可不写的关键字):
function <function name>([parameter types[, ...]]) {internal|external|public|private} [pure|view|payable] [virtual|override] [<modifiers>]
[returns (<return types>)]{ <function body> }
([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。 -
[virtual|override]
: 方法是否可以被重写,或者是否是重写方法。virtual
用在父合约上,标识的方法可以被子合约重写。override
用在自合约上,表名方法重写了父合约的方法。 -
<modifiers>
: 自定义的修饰器,可以有0个或多个修饰器。 -
[returns ()]
:函数返回的变量类型和名称。
到底什么是 Pure
和View
?
刚开始学习 solidity
时,pure
和 view
关键字可能令人费解,因为其他编程语言中没有类似的关键字。solidity
引入这两个关键字主要是因为 以太坊交易需要支付气费(gas fee)。合约的状态变量存储在链上,gas fee 很贵,如果计算不改变链上状态,就可以不用付 gas
。包含 pure
和 view
关键字的函数是不改写链上状态的,因此用户直接调用它们是不需要付 gas 的(注意,合约中非 pure
/view
函数调用 pure
/view
函数时需要付gas)。
在以太坊中,以下语句被视为修改链上状态:
- 写入状态变量。
- 释放事件。
- 创建其他合约。
- 使用
selfdestruct
. - 通过调用发送以太币。
- 调用任何未标记
view
或pure
的函数。 - 使用低级调用(low-level calls)。
- 使用包含某些操作码的内联汇编。
将合约中的状态变量(存储在链上)比作碧琪公主,三种不同的角色代表不同的关键字。
代码
1. pure 和 view
我们在合约里定义一个状态变量 number
,初始化为 5。
定义一个 add()
函数,每次调用会让 number
增加 1。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.21;
contract FunctionTypes{
uint256 public number = 5;
// 默认function
function add() external{
number = number + 1;
}
}
如果 add()
函数被标记为 pure
,比如 function add() external pure
,就会报错。因为 pure
是不配读取合约里的状态变量的,更不配改写。那 pure
函数能做些什么?举个例子,你可以给函数传递一个参数 _number
,然后让他返回 _number + 1
,这个操作不会读取或写入状态变量。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.21;
contract FunctionTypes{
uint256 public number = 5;
// 默认function
function add() external{
number = number + 1;
}
// pure: 纯纯牛马
function addPure(uint256 _number) external pure returns(uint256 new_number){
new_number = _number + 1;
}
}
如果 add()
函数被标记为 view
,比如 function add() external view
,也会报错。因为 view
能读取,但不能够改写状态变量。我们可以稍微改写下函数,读取但是不改写 number
,返回一个新的变量。
// view: 看客
function addView() external view returns(uint256 new_number) {
new_number = number + 1;
}
2. internal v.s. external
// internal: 内部函数
function minus() internal {
number = number - 1;
}
// 合约内的函数可以调用内部函数
function minusCall() external {
minus();
}
我们定义一个 internal
的 minus()
函数,每次调用使得 number
变量减少 1。由于 internal
函数只能由合约内部调用,我们必须再定义一个 external
的 minusCall()
函数,通过它间接调用内部的 minus()
函数。
4 函数输出
返回值:return 和 returns
Solidity 中与函数输出相关的有两个关键字:return
和returns
。它们的区别在于:
returns
:跟在函数名后面,用于声明返回的变量类型及变量名。return
:用于函数主体中,返回指定的变量。
// 返回多个变量
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
来声明该数组内的元素皆为此类型。数组类型返回值默认必须用memory修饰.
命名式返回
我们可以在 returns
中标明返回变量的名称。Solidity 会初始化这些变量,并且自动返回这些变量的值,无需使用 return
。
// 命名式返回
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
来返回变量:
// 命名式返回,依然支持return
function returnNamed2() public pure returns(uint256 _number, bool _bool, uint256[3] memory _array){
return(1, true, [uint256(1),2,5]);
}
解构式赋值
Solidity 支持使用解构式赋值规则来读取函数的全部或部分返回值。
-
读取所有返回值:声明变量,然后将要赋值的变量用
,
隔开,按顺序排列。 -
uint256 _number; bool _bool; uint256[3] memory _array; (_number, _bool, _array) = returnNamed();
读取部分返回值:声明要读取的返回值对应的变量,不读取的留空。在下面的代码中,我们只读取
_bool
,而不读取返回的_number
和_array
: -
bool _bool2; (, _bool2, ) = returnNamed();
// 读取返回值,结构式赋值
function readReturn() public pure{
uint256 _number;
bool _bool;
bool _bool2;
uint256[3] memory _array;
(_number, _bool, _array) = returnNamed();
// 读取部分返回值,解构式赋值
(, _bool2, ) = returnNamed();
}
在 Remix 上运行
部署合约后查看三种返回方式的结果
5 变量数据存储和作用域
引用类型(Reference Type) :包括数组(array
)和结构体(struct
),由于这类变量比较复杂,占用存储空间大,我们在使用时必须要声明数据存储的位置。
数据位置
Solidity数据存储位置有三类:storage
,memory
和calldata
。不同存储位置的gas
成本不同。storage
类型的数据存在链上,类似计算机的硬盘,消耗gas
多;memory
和calldata
类型的临时存在内存里,消耗gas
少。整体消耗gas
从多到少依次为:storage
> memory
> calldata
。大致用法:
-
storage
:合约里的状态变量默认都是storage
,存储在链上。 -
memory
:函数里的参数和临时变量一般用memory
,存储在内存中,不上链。尤其是如果返回数据类型是变长的情况下,必须加memory修饰,例如:string, bytes, array和自定义结构。 -
calldata
:和memory
类似,存储在内存中,不上链。与memory
的不同点在于calldata
变量不能修改(immutable
),一般用于函数的参数。例子:
function fCalldata(uint[] calldata _x) public pure returns(uint[] calldata){
//参数为calldata数组,不能被修改
// _x[0] = 0 //这样修改会报错
return(_x);
}