Solidity 语言是一种面向合约的高级编程语言,用于在以太坊区块链网络上实现智能合约。Solidity 语言深受c++、Python 和 JavaScript 的影响,针对以太坊(Ethereum)虚拟机(EVM)设计。
很久之前学过 Solidity 和 React,但没有留下任何笔记,太久不用忘得差不多了,遂重学一遍。
值数据类型
uint
:无符号整数,值大于等于0int
:有符号整数bool
:布尔值address
:用于储存账户地址bytes
:这表示固定大小的字节数组(1~32)enum
:枚举类型
整形
与其他语言不同的是,solidity 中的整数类型拥有多个子类型,例如 int8、int16 等。
对于 int 和 uint,都有 8、16、24、32、256。例如 uint256、int8。
地址
地址是20字节的数据类型。这是一个专为储存账户地址设计的数据类型。
它可以保存合约账户地址或者其他一般账户的地址,同时还提供了内置的变量和函数。
字节
字节也有许多种类,例如 bytes1、bytes16,但最高是 bytes32。
枚举
枚举比较简单,如下就可以声明一个枚举类:
enum weekdays { monday, tuesday, friday, sunday };
引用类型
引用类型变量的值是一个指针,指向实际储存对象的位置,solidity 中提供了以下类型:
- 数组(array):数组长度可固定也可动态变化。
- 映射(mapping):与其他语言的 Map 或 Dictionary 类似。
- 字符串(string):和其他语言的字符串一样,字节数组。
- 结构(struct):与 C 语言的结构体一样,开发者可以自定义数据类型。
数组
定长数组
定长数组的长度不可改变,声明方式很简单,如下所示:
uint256[8] arr1 = [1,2,3,4,5,6,7,8];
uint256[4] arr2;
arr2 = [1,2,3,4];
动态数组
动态数组和普通数组的区别在于,动态数组可以不指定数组长度,也可以像定长数组一样初始化:
int256[] flags;
flags = [1,2,3];
字节数组
实际上,上面的 bytes 就是一个字节数组,但和 byte[] 不同的是,byte[] 内每个元素都是 32 个字节(256bits),而 bytes 就是真正的字节串。
它的声明方式有些不同,使用 new 来创建:
bytes myBytes= new bytes(0);
这里的 0 就是初始的数组长度,除了声明时赋值,也可以稍后赋值。
bytes myBytes;
myBytes = new bytes(0);
映射
映射就像 Java 里的 Map 一样,一个唯一的 Key 对应一个值。
和其他类型不同的是,映射的声明有些特别,下面是一个 mapping 声明的例子:
mapping(address => uint) usersWithPoints;
既然 Key 只能是基本数据类型,而 Value 就没有限制了,也可以是引用类型或结构。
对于映射(mapping)还有如下规则:
- 如果 Key 没有对应的 Value,那么它的初始值就是 Value 对应类型的默认值。
- Mapping 不能作为函数的参数或返回值
- 如果 Mapping 的访问修饰符为 public,则 Solidity 会自动生成一个 getter 函数用于查询这个映射。
字符串
字符串已经很熟悉了,声明方式也很简单,直接如下所示即可:
string a = "Hello, World!";
结构
结构的内部只有变量类型和变量名,用 struct [结构名] {}
来定义一个结构。
struct User {
string name;
uint age;
int id;
address addr;
}
有了结构体,实例化的方法也很简单,和大多数语言一样,但是不需要 new 关键字:
student = User("LovelyCat", 20, 1, 0xeBab8C11D9511Df3db2Eee29fcA69EcD49782E6a);
基本语法
上面只是介绍了 Solidity 中的一些数据类型,下面开始正式编写一个简单的 Solidity 智能合约。
初学者可以打开Remix:https://remix.ethereum.org/,这是一个在线的 Solidity 编辑器,支持部署合约到测试网络上运行,同时提供了简单的合约交互功能,可以让开发者快速调试编写的合约。
进入之后,选择学习者模式,然后会打开一个和 VS Code 类似的页面,左边是文件管理器,里面有很多预设的文件,这些都用不上,直接新建一个文件,命名为 SimpleStorage.sol
即可。
接下来开始写我们第一个合约:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract SimpleStorage {
}
sol 文件的第一行一般是 SPDX-License-Identifier
,这个代表此文件使用的 LICENSE,一般是 MIT 开源协议。
第二行指定合约的编译器版本,后面的 ^0.8.0
是至少要求 0.8.0 及以上的编译器版本。
接下来就是合约的正文了,用 contract
关键字 + 名称声明一个合约,这个合约和面向对象中的类一样,内部可以定义变量、函数等。
修饰符
访问修饰符
- public:公开,可以在任何地方访问。
- private:合约私有,只能在合约内部访问。
- internal:类似于 protected,可以在合约以及继承此合约的子类内访问。
- external:仅允许在合约外部访问,只可用于修饰函数。
函数修饰符
- pure:表示该函数既不会读取也不会修改合约内的变量。
- view:表示该函数只会读取合约内的变量。
contract FunctionModifiers {
uint public x = 2233;
function mul(uint a, uint b) public pure returns (uint) {
return a * b;
}
function getX() public view returns (uint) {
return x;
}
}
状态变量修饰符
constant:在变量声明时确定值,之后不允许被更改。
immutable:和 constant 一样,但允许在构造函数或其他函数内声明值,值确定后不可再更改。
contract VariableModifiers {
uint public constant CONSTANT_NUM = 100;
address public immutable CONTRACT_OWNER; // 0.8.21 之前需在声明时初始化
constructor() {
CONTRACT_OWNER= msg.sender;
}
}
本地变量修饰符
storage
:表示变量是某个对象的指针,对此变量的修改也会影响原来的值。memory
:表示变量是某个对象的副本,对此变量的修改不会影响原来的值。calldata
:表示数据只读,仅可出现在函数参数中。
首先要说明的是,在区块链网络上执行智能合约是需要消耗 Gas 费用的,而这里 storage 的费用最高,而 calldata 的费用是最低的。
下面来看一个简单的例子吧:
contract ContractUsers {
struct User{
string username;
uint age;
}
User[] public users;
function addUser(string calldata _username, uint _age) public {
users.push(User(_username, _age));
}
// 这里返回值的 string 被 memory 修饰,表示该返回值是数据的副本
function getUser(uint _index) public view returns (string memory, uint) {
return (users[_index].username, users[_index].age);
}
function updateUser(uint _index, uint _age) public {
// 表示该变量为 User 的指针
User storage user = users[_index];
user.age = _age;
}
}
试想一下,如果对于 user 这个变量,如果去掉 storage 会发生什么呢?
编译时报错:Data location must be "storage", "memory" or "calldata" for variable, but none was given.solidity(6651)
也就是说,对于引用类型来说,必须给对应变量指定一个数据位置(Data Location),也就是 storage 或者 memory,因为 calldata 只能在函数参数中使用。
因此推断出,对应 string 这样的特殊类型,如果作为函数参数,也必须加上 calldata 或者 memory 修饰。
变量声明
看完上面的各种修饰符,对变量的声明应该熟悉了不少,继续回到我们的 SimpleStorage 合约:
contract SimpleStorage {
uint256 public storedNumber;
}
先定义一个简单的变量用来保存数字,下面继续介绍函数声明。
函数声明
函数声明的语法如下:
function [函数名]({[参数类型] [参数名], ...}) [访问修饰符] [函数修饰符] {returns ([返回参数类型], ...)}
其中 []
表示必选,{}
表示可选。
多返回值
学过 Python 或其他支持多返回值的语言应该明白,如果一个函数返回多个值,对应也需要有多个变量来接收对应的返回值。
function getNumberWithSenderAddress() public view returns (uint256, address) {
return (storedNumber, msg.sender);
}
这里定义了多个返回值,返回值需要用小括号 ()
包裹。
function wrapper() public view returns (address) {
(, address b) = getNumberWithSenderAddress();
return b;
}
同样地,在接收多个返回值时也需要用小括号包裹,如果有某个变量是用不到的,直接留空即可。
回到我们的合约,继续完成对数字的保存和读取:
contract SimpleStorage {
uint256 public storedNumber;
function store(uint256 _newNumber) public {
storedNumber = _newNumber;
}
function getNumber() public view returns (uint256) {
return storedNumber;
}
}
合约部署
下面我们在 Remix 的左侧找到 Deploy & Run Transactions
,环境(Environment)默认即可,直接选择你的合约文件,点击部署。
在 Deployed Contracts
中找到刚刚部署的合约,展开发现里面已经有刚才定义的函数了。
这里橙色的 store 就是储存函数了,填入函数参数点击 transact
即可执行。
而下面的蓝色的 getNumber 和 storedNumber 也可以直接单击执行,对于无参函数执行结果就是对应的返回值,对于变量则是直接获取它的值。
结束语
由于篇幅有限,本期先介绍一些 Solidity 的基本知识,如有错误欢迎指出。