关于区块链和智能合约开发者的区别解释
我发现很多人都表述不清楚区块链和智能合约。我认识几位程序员朋友,他们都自称是在做区块链开发,但实际上是在做智能合约的开发。大多数外行分不清楚区块链和智能合约我能理解,但是很多从事智能合约开发的程序员竟然也分不清楚,我不知道是不是表述问题还是理解问题。
区块链是区块链,智能合约是智能合约,两者的关系就像是微信和微信小程序一样,一个是 App 开发,一个是小程序开发,根本不一样,不能混为一谈。
据我了解,区块链的需求没那么多,特别是中国这个环境下。大多数区块链相关的程序员都是在做智能合约开发,而不是真的在开发区块链。
区块链是可以用很多后端语言去开发的,比如用 Go、Node.js、Rust、Java 等。
但是智能合约不可以随便选择编程语言,我这里讲的智能合约是指以太坊智能合约。目前它只能选择 Solidity、Vyper、YUL、YUL+ 和 Fe 这 5 种语言。其中 solidity 最受欢迎,大多数项目和开发者都是选择了 solidity。我们几乎可以说 solidity 是智能合约的首选编程语言。
这篇文章会讲什么?
这篇文章将会介绍我认为使用 Solidity 编写智能合约时 90% 以上的场景中能够用到的语法和特性。
但是 Solidity 是一门完整的编程语言,想要把它彻底学明白,一篇文章肯定是不够的。因为很多语言都被写成了一厚厚地本书。不过通常写编程语言的书都会非常全体、体系化地介绍语言的全部,包括那些平时压根用不到的知识,或者一些已经落伍,语言设计上糟粕的部分。总体来说,通过一本厚厚的书来讲一门编程语言,多少是从研究的角度出发的,如果你只想快速用 Solidity 开发智能合约,不想把这门语言研究的这么透彻,那么本文很适合你。
同时本文会拿 solidity 和一些面向对象的语言做对比,如果你完全不懂其他编程语言,那么本文不适合你。
面向合约
Solidity 的设计理念和面向对象编程语言很相似,不过 Solidity 是面相合约的编程语言,如果你有面向对象编程语言的开发经验,那么学习 Solidity 就没有那么难。
Solidity 语言被设计为编写合约的语言,目前来说也只能写合约,所以它不像其他语言那样可以做很多事情。
合约构成解读
我们先来看一个最简单的合约构成,做一个整体的感受。
// SPDX-License-Identifier: MIT pragma solidity ^0.8.0; contract HelloWorld { address private owner; unit public state; modifier onlyOwner() { require(msg.sender == owner, "only owner"); _; } event StateChanged(unit state); constructor() public { owner = msg.sender; } function setState(uint _state) external onlyOwner { state = state; emit StateChanged(_state) } }
我简单解释下这个合约的代码,不会详细介绍。
第 1 行是指定版本许可。
第 2 行是指定使用的语言版本。
第 4 行是声明一个名为 HelloWorld 的合约。
第 5-6 行是状态变量,它们会永久存储在合约中。
第 8 -11 行是函数修改器,它可以用在函数修饰符上,可以改变函数的行为。
第 13 行是声明一个事件,事件可以被触发和监听。
第 15-17 行是构造函数,在部署时会被调用。
第 19-22 行是声明了一个名为 setState 的函数。
版本
solidity 有很多种版本,目前最新的版本是 8.x。
但是在早期比较流行的是 5.x、6.x 这两个版本。
solidity 的版本命名规范采用 。
和其他大多数编程语言不同的是,solidity 的版本是直接写在源代码里的。
在任意一个 sol 文件的最开始,都应该是版本代码。
语法为:
pragma solidity 0.8.0;
如果你用过 npm 的话,那这个版本语言一定不会陌生,因为 solidity 同样使用了 semver 版本规范。
合约
合约的概念有点像面向对象编程语言的类,属于一等公民。
通过关键字 contract 创建。
语法:
contract MyContract { }
可以通过 new 关键字创建合约。
new MyContract();
继承
面向对象的语言通常会使用 extends 关键字来继承,但是 solidity 没有这样做,它使用 is 来继承合约。
contract MyContract1 { uint256 num = 2022; } contract MyContract2 is MyContract1 { }
子合约被部署时,会把所有父合约的代码一起打包,所以对父合约中函数的调用都属于内部调用。
子合约可以隐式转换父合约,合约也可以显式转换为地址。
address addr = address(c);
重写函数使用 override 关键字。父合约中支持重写的函数必须是 virtual 的。
contract Parent { function fn() public virtual {} } contract Child is Parent { function fn() public override {} }
调用父合约中的方法,使用 super 关键字。
contract Parent { function fn() public {} } contract Child is Parent { function fn2() public { super.fn(); } }
支持多重继承。
contract Parent1 { function fn() public virtual {} } contract Parent2 { function fn() public virtual {} } contract Child is Parent1, Parent2 { function fn() public override(Parent1, Parent2) {} }
变量与基础类型
变量是永久存储在合约中的值,通常用来记录业务信息。
每个变量都需要声明类型,solidity 中的类型有如下几种:
- string:字符串类型
- bool:布尔值,true/false。
- uint:无符号整型,有 uint 和 uint8/16/32/64/128/256 几个类型。uint 是 uint256 的别名。
- int:有符号整型,规则和 uint 一样。
- bytes:定长字节数组。从 bytes1 到 bytes32,byte 是 bytes1 的别名。它和数组类似,通过下标获取元素,通过 length 获取成员数量。
- address:地址类型。保存一个 20 字节的地址。
- address payable:可支付的地址,有成员函数 transfer 和 send。
contract MyContract { string name = "" }
uint
对于整型变量,我们可以通过 type(x).min 和 type(x).max 来获取某个类型的最大值和最小值。
address
address payable 可以隐式转换到 address,但是 address 必须通过 payable(address) 这种方式显示转换。
address 还可以显示转换为 uint160 和 bytes20。
bytes 和 string
bytes 和 string 都是数组,而不是普通的值类型。
bytes 和 byte[] 非常像,但是它在 calldata 和 memory 中会紧打包。紧打包的意思是将元素连续存储在一起,不会按照每 32 字节为一个单元进行存储。
string 是变长 utf-8 编码的字节数组,和 bytes 不同的是它不可以用索引来访问。
字符串没有操作函数,一般都是通过第三方 string 库来操作字符串。
string 可以转换为 bytes,转换时是创建引用而不是创建拷贝。
function stringToBytes() public pure returns (bytes memory) { string memory str = "hello"; bytes memory bts = bytes(str); return bts; }
由于 bytes 和 string 很相似,所以我们在使用它们时应该有对应的原则。
- 对于任意长度的原始字节使用 bytes。
- 对于任意长度的 UTF-8 字符串使用 string。
- 当需要对字节数组长度进行限制时,应该使用 byte1-byte32 之间的具体类型。
合理使用 bytes 可以节省 Gas 费。
变量修饰符
我们也可以为变量指定访问修饰符。
语法是 类型 访问修饰符(可选) 字段名。
访问修饰符有三种:
- public:公开,外部可以访问,声明为 public 的话会自动生成 getter 函数。
- internal:默认,只有合约自身和派生的合约可以访问。
- private:只有合约自身可以访问。
solidity 中的变量与传统语言的变量有些不同。
- 字符串的值默认不可以包含中文。如果要使用除了英文外的其他语言,必须加 unicode 前缀。
string name = unicode"小明";
结构体
使用关键字 struct 创建结构,有点类似 Go/C 的 struct,或者类似 TypeScript 中的 type/interface。
struct User { string name; string password; uint8 age; bool state; }
初始化结构体和调用函数类似,参数的顺序和结构体的顺序保持一致。
User user = User("章三", "123", 12, false);
访问某一个属性使用点号。
user.name;
属性也可以直接赋值。
user.name = "里斯";
数组
和 TypeScript 中的数组语法一致,语法是 type[]。
User[] users;
访问数组元素,使用 array[index] 的方式。
users[0];
访问不存在的下标,会直接报错。
在创建数组时可以声明长度,如果不声明,那就是可以动态调整大小的数组。
uint256[10] nums;
数组具有 pop 和 push 方法,分别用于弹出一个元素和添加一个元素。但是它们不可以用在定长数组中。
push 方法可以不传递参数,这时表示它添加一个该元素类型的零值。
strs.push("1"); strs.pop();
映射
类似于很多语言中的 Map 结构。语法是 mapping(keyType => valueType)。
mapping(address => User) userMapping;
key 的类型只允许是基本类型,不可以是复杂类型,比如合约、枚举、映射和结构体。
value 的类型没有限制。
访问 mapping 元素,使用 mapping[key] 的方式。
userMapping[0x021221]
访问不存在的 key,会返回 value 类型的默认值。
mapping 不可以作为公有函数的参数和返回值,只可以作为变量或者函数内的存储或者库函数的参数。
声明为 public 的 mapping,会自动创建 getter 函数。KeyType 作为参数,ValueType 作为返回值。
mapping 无法被遍历。不过有一些开源库用一种结构来实现了可遍历的 mapping。可以直接拿过来用。