第一个 Solidity 程序
Solidity 是一种用于编写以太坊虚拟机(EVM)智能合约的编程语言。
掌握 Solidity 是参与链上项目的必备技能
在 Remix 中,左侧菜单有三个按钮,分别对应文件(编写代码)、编译(运行代码)和部署(将合约部署到链上)。点击“创建新文件”(Create New File)按钮,即可创建一个空白的 Solidity 合约。
编写代码
// 第 1 行是注释,说明代码所使用的软件许可(license),这里使用的是 MIT 许可。如果不写许可,编译时会出现警告(warning),但程序仍可运行
// SPDX-License-Identifier: MIT
// 第 2 行声明源文件所使用的 Solidity 版本,因为不同版本的语法有差异。这行代码表示源文件将不允许小于 0.8.21 版本或大于等于 0.9.0 的编译器编译(第二个条件由 ^ 提供)。
pragma solidity ^0.8.21;
// 第 3-4 行是合约部分。第 3 行创建合约(contract),并声明合约名为 HelloWeb3。第 4 行是合约内容,声明了一个 string(字符串)变量 _string,并赋值为 "Hello Web3!"。
contract HelloWeb3{
string public _string = "Hello Web3!";
}
编译并部署代码
在 Remix 编辑代码的页面,按 Ctrl + S 即可编译代码。
编译完成后,点击左侧菜单的“部署”按钮,进入部署页面。
Remix 会使用 Remix 虚拟机(以前称为 JavaScript 虚拟机)来模拟以太坊链,运行智能合约,类似在浏览器里运行一条测试链。
Remix 还会为你分配一些测试账户,每个账户里有 100 ETH(测试代币),随意使用。
点击 Deploy(黄色按钮),即可部署我们编写的合约。
部署成功后,在下方会看到名为 HelloWeb3 的合约。点击 _string,即可看到 “Hello Web3!”。
变量
-
值类型(Value Type):包括布尔型,整数型等等,这类变量赋值时候直接传递数值。
-
引用类型(Reference Type):包括数组和结构体,这类变量占空间大,赋值时候直接传递地址(类似指针)。
-
映射类型(Mapping Type): Solidity中存储键值对的数据结构,可以理解为哈希表
-
public、private、internal 用于修饰状态变量。
- public 变量会自动生成同名的 getter 函数,用于查询数值。
- 未标明可见性类型的状态变量,默认为 internal。
状态变量和非状态变量
- 状态变量通常位于合约的顶部,并且是合约的一部分。这些变量保存在存储(storage)中,并且在整个合约生命周期内都是可用的。
- 非状态变量指的是在函数内部声明的变量,它们仅在函数执行期间存在,并且不在合约的存储空间中保存。
值类型
- 布尔型
- 整型
- 地址类型
- 访问修饰符修饰的 address: 存储一个 20 字节的值(以太坊地址的大小)。
- payable 修饰的 address: 比普通地址多了 transfer 和 send 两个成员方法,用于接收转账。
// 地址
address public _address = 0x7A58c0Be72BE218B41C608b7Fe7C5bB630736C71;
address payable public _address1 = payable(_address); // payable address,可以转账、查余额
// 地址类型的成员
uint256 public balance = _address1.balance; // balance of address
- 定长字节数组
- 定长字节数组: 属于值类型,数组长度在声明之后不能改变。根据字节数组的长度分为 bytes1, bytes8, bytes32 等类型。定长字节数组最多存储 32 bytes 数据,即bytes32。
- 不定长字节数组: 属于引用类型(之后的章节介绍),数组长度在声明之后可以改变,包括 bytes 等。
// 固定长度的字节数组
bytes32 public _byte32 = "MiniSolidity";
bytes1 public _byte = _byte32[0];
在上述代码中,MiniSolidity 变量以字节的方式存储进变量 _byte32。如果把它转换成 16 进制,就是:0x4d696e69536f6c69646974790000000000000000000000000000000000000000
_byte 变量的值为 _byte32 的第一个字节,即 0x4d。
- 枚举 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);
}
引用类型
数组
函数
funtion <function name> <access modifier> [pure|view|payable] (<parameter types>) [returns (<return types>)] {
// 函数体
}
- function:``声明函数时的固定开头
- :``函数名
- ():``圆括号内写入函数的参数,即输入到函数的变量类型和名称。
- :
必须指明可见性
。函数可见性说明符,共有4种。- public:内部和外部均可见。
- private:只能从本合约内部访问,继承的合约也不能使用。
- external:只能从合约外部访问(但内部可以通过 this.f() 来调用,f是函数名)。
- internal: 只能从合约内部访问,继承的合约可以用。
- [pure|view|payable]:
非必须,不写时默认为 nonpayable
。决定函数权限/功能的关键字。- payable:以这个关键字修饰的函数,运行的时候可以给合约转入 ETH。
- pure 和 view :声明该函数不改变链上状态。
- [returns ()]:
非必须
。返回值的类型和名称。
pure 和 view
ETH 中任意会改变链上状态的操作都会被收取燃料费(gas fee),例如:
- 写入状态变量。
- 释放事件。
- 创建其他合约。
- 使用 selfdestruct.
- 通过调用发送以太币。
- 调用任何未标记 view 或 pure 的函数。
- 使用低级调用(low-level calls)。
- 使用包含某些操作码的内联汇编。
solidity 引入这两个关键字,核心原因是以太坊交易需要支付燃料费(gas fee)。gas fee 很贵,但如果计算不改变链上状态,就可以不用付 gas。
合约的状态变量存储在链上,如果任何操作都改变状态变量的链上状态,合约的代价是很高的。
此时,可以在函数增加 pure / view 关键字,此时函数内的任何操作不会改变链上状态。因此用户直接调用它们是不需要付 gas 的(注意,合约中非 pure/view 函数调用 pure/view 函数时需要付gas)。
- pure 函数既不能读取也不能写入链上的状态变量。
- view 函数能读取但也不能写入状态变量。
- pure 或 view 的函数既可以读取也可以写入状态变量。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.21;
contract FunctionTypes{
// 在合约里定义一个状态变量 number,初始化为 5。
uint256 public number = 5;
}
// 定义一个 add() 函数,每次调用会让 number 增加 1。
function add() external{
number = number + 1;
}
如果 add() 函数被标记为 pure,比如 function add() external pure,就会报错。因为 pure 是不能读取合约里的状态变量的,更不配改写。
// 报错
function add() external pure{
number = number + 1;
}
纯函数 Pure Function
用 pure 修饰的函数即为纯函数。
纯函数指的是那些只依赖于其输入参数,并且除了返回值之外没有任何副作用的函数。这意味着纯函数不修改外部状态,也不依赖于外部状态。这样的函数对于确保代码的可预测性和简化调试非常有用。
例如,给函数传入的参数是一个非状态变量 number,返回的依然是非状态变量 new_number ,这个操作不会读取或写入状态变量。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.21;
contract FunctionTypes{
// 没有状态变量,都是暂时性的非状态变量
function addPure(uint256 number) external pure returns(uint256 new_number){
new_number = number + 1;
}
}
部署后,在右侧填传入的参数(number),下侧即可看到返回的参数(new_number)
internal & external
- external:只能从合约外部访问(但内部可以通过 this.f() 来调用,f是函数名)。
- internal: 只能从合约内部访问,继承的合约可以用。
出于安全考虑,尽量将函数设置为 internal。
在不得不从外部访问的时候,可以通过合约内的 external 间接调用。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.21;
contract MathContract {
uint256 public number; // 声明一个公共变量 number
// 定义一个 internal 的 minus() 函数,每次调用使得 number 变量减少 1。
function minus() internal {
number = number - 1;
}
// 由于 internal 函数只能由合约内部调用,我们必须再定义一个 external 的 minusCall() 函数,
// 通过它间接调用内部的 minus() 函数。
function minusCall() external {
minus(); // 调用内部函数来减少 number
}
// 用于设置初始值
function setNumber(uint256 _number) public {
number = _number;
}
}
按照这样的顺序,就能在外部调用内部函数,改变状态变量。
payable
如果你希望智能合约能够接收Ether作为资金来源,例如作为运营资金,那么接收这些转账的函数需要标记为 payable。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.21;
contract MathContract {
uint256 public number; // 声明一个公共变量 number
// internal 函数,减少 number 的值
function minus() internal {
number = number - 1;
}
// external 函数,允许外部调用 minus()
function minusCall() external {
minus(); // 调用内部函数来减少 number
}
// 示例函数,用于设置初始值
function setNumber(uint256 _number) public {
number = _number;
}
// external payable 的 minusPayable() 函数,间接调用 minus(),并且返回合约里的 ETH 余额(this 关键字可以让我们引用合约地址)。
function minusPayable() external payable returns(uint256 balance) {
minus();
balance = address(this).balance;
}
}
在调用 minusPayable() 时往合约里转入1个 ETH。
通过这个操作,最终每执行一次 minusPayable,都会往账户里存入 1ETH。