title: solidity教程(一)搭建僵尸工厂
tags: solidity,eth
声明:本系列教程是整理cryptozombies https://cryptozombies.io/zh而来。
人类,欢迎你
你认为你可以当一个合格的 CryptoZombie, 嗯?
这个教程会教你如何搭建一个以太网的游戏。
此课程为 Solidity 初学者设计,需要你对其他的程序语言有所了解(如 JavaScript)。
少年,准备开始了吗?
第1章: 课程概述
第一课你将创造一个"僵尸工厂", 用它建立一支僵尸部队。
- 我们的工厂会把我们部队中所有的僵尸保存到数据库中
- 工厂会有一个函数能产生新的僵尸
- 每个僵尸会有一个随机的独一无二的面孔
在后面的课程里,我们会增加功能。比如,让僵尸能攻击人类或其它僵尸! 但是在实现这些好玩的功能之前,我们先要实现创建僵尸这样的基本功能。
僵尸DNA如何运作
僵尸的面孔取决于它的DNA。它的DNA很简单,由一个16位的整数组成:
8356281049284737
如同真正的DNA, 这个数字的不同部分会对应不同的特点。 前2位代表头型,紧接着的2位代表眼睛,等等。
注: 本教程我们尽量简化。我们的僵尸只有7种头型(虽然2位数字允许100种可能性)。以后我们会加入更多的头型, 如果我们想让僵尸有更多造型。
例如,前两位数字是 83, 计算僵尸的头型,我们做83 % 7 + 1 = 7 运算, 此僵尸将被赋予第七类头型。
第2章: 合约
从最基本的开始入手:
Solidity 的代码都包裹在合约里面. 一份合约
就是以太应币应用的基本模块, 所有的变量和函数都属于一份合约, 它是你所有应用的起点.
一份名为 HelloWorld
的空合约如下:
contract HelloWorld {
}
版本指令
所有的 Solidity 源码都必须冠以 "version pragma" — 标明 Solidity 编译器的版本. 以避免将来新的编译器可能破坏你的代码。
例如: pragma solidity ^0.4.19;
(当前 Solidity 的最新版本是 0.4.19).
综上所述, 下面就是一个最基本的合约 — 每次建立一个新的项目时的第一段代码:
pragma solidity ^0.4.19;
contract HelloWorld {
}
实战演习
为了建立我们的僵尸部队, 让我们先建立一个基础合约,称为 ZombieFactory
。
-
在右边的输入框里输入
0.4.19
,我们的合约基于这个版本的编译器。 -
建立一个空合约
ZombieFactory
。
一切完毕,点击下面 "答案" . 如果没效果,点击 "提示"。
答案:
pragma solidity ^0.4.19;
contract ZombieFactory {
}
第3章: 状态变量
我们已经实现了一个空合约, 下面学习 Solidity 中如何使用变量。
状态变量是被永久地保存在合约中。也就是说它们被写入以太币区块链中. 想象成写入一个数据库。
Solidity的变量类型:
- 值类型 :这类变量在赋值或传参时,总是进行值拷贝。
- 布尔类型(Booleans)
- 整型(Integers)
- 定长浮点型(Fixed Point Numbers)
- 定长字节数组(Fixed-size byte arrays)
- 有理数和整型常量(Rational and Integer Literals)
- 字符串常量(String literals)
- 十六进制常量(Hexadecimal literals)
- 枚举(Enums)
- 函数类型(Function Types)
- 地址类型(Address)
- 地址常量(Address Literals)
- 函数类型及地址类型(Address)
- 引用类型 :这里变量在赋值或传参时,传递的是地址。
- 数组(Arrays)
- 结构体(Structs)
虽然列举了很多,但是本文涉及计部分简单类型。所以有不懂的小伙伴们不要害怕,以后会慢慢深入讲解的。
例子:
contract Example {
// 这个无符号整数将会永久的被保存在区块链中
string name ="Jay";
uint age = 25;
}
在上面的例子中,定义name为string类型,并赋值为“Jay”和age 为 uint 类型,并赋值25。
字符串类型:string
string字符串用于保存任意长度的 UTF-8 编码数据。 如: string greeting = "Hello world!"。
无符号整数: uint
uint 无符号数据类型, 指其值不能是负数,对于有符号的整数存在名为 int 的数据类型。
注: Solidity中, uint 实际上是 uint256代名词, 一个256位的无符号整数。你也可以定义位数少的uints — uint8, uint16, uint32 等,但一般来讲我们都使用简单的 uint。如果使用到其他的我后面会讲。
保存字符我们也可以选择使用bytes类型,这个类型我们以后会讲。
实战演习
我们的僵尸DNA将由一个十六位数字组成。
- 定义 dnaDigits 为 uint 数据类型, 并赋值 16。
答案:
pragma solidity ^0.4.19;
contract ZombieFactory {
//插入代码
uint dnaDigits = 16;
}
注意: 变量直接声明在合约中,是不可直接变更的。
第4章: 数学运算
在 Solidity 中,数学运算很直观明了,与其它程序设计语言相同:
- 加法: x + y
- 减法: x - y,
- 乘法: x * y
- 除法: x / y
- 取模 / 求余: x % y (例如, 13 % 5 余 3, 因为13除以5,余3)
- 次方:x ** y (x 的 y次方 // 例如: 5 ** 2 = 25)
uint x = 5 ** 2; // equal to 5^2 = 25
实战演习
为了保证我们的僵尸的DNA只含有16个字符,我们先造一个uint数据,让它等于10^16。这样一来以后我们可以用模运算符 % 把一个整数变成16位。
- 建立一个uint类型的变量,名字叫dnaModulus, 令其等于 10 的 dnaDigits 次方.
答案:
pragma solidity ^0.4.19;
contract ZombieFactory {
uint dnaDigits = 16;
uint dnaModulus = 10 ** dnaDigits;
}
第5章: 结构体
有时你需要更复杂的数据类型,Solidity 提供了 结构体:
struct Person {
uint age;
string name;
}
结构体允许你生成一个更复杂的数据类型,它有多个属性。
注:我们刚刚引进了一个新类型,
string
。 字符串用于保存任意长度的 UTF-8 编码数据。 如:string greeting = "Hello world!"
。
实战演习
在我们的程序中,我们将创建一些僵尸!每个僵尸将拥有多个属性,所以这是一个展示结构体的完美例子。
-
建立一个
struct
命名为Zombie
. -
我们的
Zombie
结构体有两个属性:name
(类型为string
), 和dna
(类型为uint
)。
pragma solidity ^0.4.19;
contract ZombieFactory {
uint dnaDigits = 16;
uint dnaModulus = 10 ** dnaDigits;
//插入结构体
struct Zombie {
string name;
uint dna;
}
}
第6章: 数组
如果你想建立一个集合,可以用 数组 这样的数据类型. Solidity 支持两种数组: 静态 数组和 动态 数组:
// 固定长度为2的静态数组:
uint[2] fixedArray;
// 固定长度为5的string类型的静态数组:
string[5] stringArray;
// 动态数组,长度不固定,可以动态添加元素:
uint[] dynamicArray;
你也可以建立一个 结构体 类型的数组 例如,上一章提到的 Person
:
Person[] people; // dynamic Array, we can keep adding to it
记住:状态变量被永久保存在区块链中。所以在你的合约中创建动态数组来保存成结构的数据是非常有意义的。
公共数组
你可以定义 public
数组, Solidity 会自动创建 getter 方法. 语法如下:
Person [] public people;
其它的合约可以从这个数组读取数据(但不能写入数据),所以这在合约中是一个有用的保存公共数据的模式。
实战演习
为了把一个僵尸部队保存在我们的APP里,并且能够让其它APP看到这些僵尸,我们需要一个公共数组。
- 创建一个数据类型为
Zombie
的结构体数组,用public
修饰,命名为:zombies
.
答案:
==注意: 为了文章的简洁性,我用“...” 代表前面出现过的代码。==
pragma solidity ^0.4.19;
contract ZombieFactory {
...
struct Zombie {
string name;
uint dna;
}
//这里开始
Zombie[] public zombies;
}
第7章: 定义函数
我们每天需要吃饭、睡觉等等一系列活动,而这一系列的活动在程序中叫做方法,或者函数,下面我们定一个吃汉堡的方法(函数)。
在 Solidity 中函数定义的句法如下:
function eatHamburgers(string _name, uint _amount) {
}
这是一个名为 eatHamburgers 的函数,它接受两个参数:一个 string类型的 和 一个 uint类型的。_name 代表着汉堡的品种,_amount代表我们吃的数量。现在函数内部还是空的。
注:: 习惯上函数里的变量都是以“_”开头 (但不是硬性规定) 以区别全局变量。我们整个教程都会沿用这个习惯。
我们的函数是用如下:
eatHamburgers("vitalik", 100);
实战演习
在我们的应用里,我们要能创建一些僵尸,让我们写一个函数做这件事吧!
建立一个函数 createZombie。 它有两个参数: _name (类型为string), 和 _dna (类型为uint)。
暂时让函数空着——我们在后面会增加内容。
pragma solidity ^0.4.19;
contract ZombieFactory {
...
Zombie[] public zombies;
// 这里开始
function createZombie(string _name, uint _dna) {
}
}
第8章: 使用结构体和数组
创建新的结构体
还记得上个例子中的 Person
结构吗?
struct Person {
uint age;
string name;
}
Person[] public people;
现在我们学习创建新的 Person
结构,然后把它加入到名为 people
的数组中.
// 创建一个新的Person:
Person satoshi = Person(172, "Satoshi");
// 将新创建的satoshi添加进people数组:
people.push(satoshi);
你也可以两步并一步,用一行代码更简洁:
people.push(Person(16, "Vitalik"));
注:
array.push()
在数组的 尾部 加入新元素 ,所以元素在数组中的顺序就是我们添加的顺序, 如:
uint[] numbers;
numbers.push(5);
numbers.push(10);
numbers.push(15);
// numbers is now equal to [5, 10, 15]
实战演习
让我们创建名为createZombie的函数来做点儿什么吧。
- 在函数体里新创建一个
Zombie
, 然后把它加入zombies
数组中。 新创建的僵尸的name
和dna
,来自于函数的参数。 - 让我们用一行代码简洁地完成它。
我要看答案! 交卷
答案:
pragma solidity ^0.4.19;
contract ZombieFactory {
...
Zombie[] public zombies;
function createZombie(string _name, uint _dna) {
// 这里开始
zombies.push(Zombie(_name, _dna));
}
}
第9章: 私有 / 公共函数
Solidity 定义的函数的属性默认为公共(public)。 这就意味着任何一方 (或其它合约) 都可以调用你合约里的函数。
显然,不是什么时候都需要这样,而且这样的合约易于受到攻击。 所以将自己的函数定义为私有(private)是一个好的编程习惯,只有当你需要外部世界调用它时才将它设置为公共。
如何定义一个私有的函数呢?
uint[] numbers;
function _addToArray(uint _number) private {
numbers.push(_number);
}
这意味着只有我们合约中的其它函数才能够调用这个函数,给 numbers 数组添加新成员。
可以看到,在函数名字后面使用关键字 private 即可。和函数的参数类似,私有函数的名字用(_)起始。
实战演习
我们合约的函数 createZombie 的默认属性是公共的,这意味着任何一方都可以调用它去创建一个僵尸。 咱们来把它变成私有吧!
- 变 createZombie 为私有函数,不要忘记遵守命名的规矩哦!
答案:
pragma solidity ^0.4.19;
contract ZombieFactory {
...
function _createZombie(string _name, uint _dna) private {
zombies.push(Zombie(_name, _dna));
}
}
第10章: 函数的更多属性
本章中我们将学习函数的返回值和修饰符。
返回值 returns
要想函数返回一个数值,按如下定义:
string greeting = "What's up dog";
function sayHello() public returns (string) {
return greeting;
}
本例 执行sayHello函数 将返回"What's up dog" 。
Solidity中,函数的定义,可包含返回值的数据类型(如本例中 string)。
函数的修饰符 view、pure
上面的函数实际上没有改变 Solidity 里的状态,即,它没有改变任何值或者写任何东西。
这种情况下我们可以把函数定义为 view, 意味着它只能读取数据不能更改数据:
function sayHello() public view returns (string) {
Solidity 还支持 pure 函数, 表明这个函数甚至都不访问应用里的数据,例如:
function _multiply(uint a, uint b) private pure returns (uint) {
return a * b;
}
这个函数甚至都不读取应用里的状态 — 它的返回值完全取决于它的输入参数,在这种情况下我们把函数定义为 pure.
注:可能很难记住何时把函数标记为 pure/view。 幸运的是, Solidity 编辑器会给出提示,提醒你使用这些修饰符。
实战演习
我们想建立一个帮助函数,它根据一个字符串随机生成一个DNA数据。
-
创建一个 private 函数,命名为 _generateRandomDna。它只接收一个输入变量 _str (类型 string), 返回一个 uint 类型的数值。
-
此函数只读取我们合约中的一些变量,所以标记为view。
-
函数内部暂时留空,以后我们再添加代码。
答案:
pragma solidity ^0.4.19;
contract ZombieFactory {
...
function _createZombie(string _name, uint _dna) private {
zombies.push(Zombie(_name, _dna));
}
// 这里开始
function _generateRandomDna(string _str) private view returns (uint) {
}
}
第11章: Keccak256 和 类型转换
如何让 _generateRandomDna 函数返回一个随机的 uint?
Ethereum 内部有一个散列函数keccak256,它用了SHA3版本。一个散列函数基本上就是把一个字符串转换为一个256位的16进制数字。字符串的一个微小变化会引起散列数据极大变化。
这在 Ethereum 中有很多应用,但是现在我们只是用它造一个伪随机数。
例子:
//6e91ec6b618bb462a4a6ee5aa2cb0e9cf30f7a052bb467b0ba58b8748c00d2e5
keccak256("aaaab");
//b1f078126895a1424524de5321b339ab00408010b7cf0e6ed451514981e58aa9
keccak256("aaaac");
显而易见,输入字符串只改变了一个字母,输出就已经天壤之别了。
注: 在区块链中安全地产生一个随机数是一个很难的问题, 本例的方法不安全,但是在我们的Zombie DNA算法里不是那么重要,已经很好地满足我们的需要了。
类型转换
有时你需要变换数据类型。例如:
uint8 a = 5;
uint b = 6;
// 将会抛出错误,因为 a * b 返回 uint, 而不是 uint8:
uint8 c = a * b;
// 我们需要将 b 转换为 uint8:
uint8 c = a * uint8(b);
上面, a * b 返回类型是 uint, 但是当我们尝试用 uint8 类型接收时, 就会造成潜在的错误。如果把它的数据类型转换为 uint8, 就可以了,编译器也不会出错。
实战演习
给 _generateRandomDna 函数添加代码! 它应该完成如下功能:
-
第一行代码取 _str 的 keccak256 散列值生成一个伪随机十六进制数,类型转换为 uint, 最后保存在类型为 uint 名为 rand 的变量中。
-
我们只想让我们的DNA的长度为16位 (还记得 dnaModulus?)。所以第二行代码应该 return 上面计算的数值对 dnaModulus 求余数(%)。
答案:
pragma solidity ^0.4.19;
contract ZombieFactory {
...
function _generateRandomDna(string _str) private view returns (uint) {
uint rand = uint(keccak256(_str));
return rand % dnaModulus;
}
}
第12章: 放在一起
我们就快完成我们的随机僵尸制造器了,来写一个公共的函数把所有的部件连接起来。
写一个公共函数,它有一个参数,用来接收僵尸的名字,之后用它生成僵尸的DNA。
实战演习
-
创建一个 public 函数,命名为createRandomZombie. 它将被传入一个变量 _name (数据类型是 string)。 (注: 定义公共函数 public 和定义一个私有 private 函数的做法一样)。
-
函数的第一行应该调用 _generateRandomDna 函数,传入 _name 参数, 结果保存在一个类型为 uint 的变量里,命名为 randDna。
-
第二行调用 _createZombie 函数, 传入参数: _name 和 randDna。
-
整个函数应该是4行代码 (包括函数的结束符号 } )。
pragma solidity ^0.4.19;
contract ZombieFactory {
...
function _generateRandomDna(string _str) private view returns (uint) {
uint rand = uint(keccak256(_str));
return rand % dnaModulus;
}
// 事件
function createRandomZombie(string _name) public {
uint randDna = _generateRandomDna(_name);
_createZombie(_name, randDna);
}
}
第13章: 事件
我们的合约几乎就要完成了!让我们加上一个事件.
事件 :是合约和区块链通讯的一种机制。你的前端应用“监听”某些事件,并做出反应。
例子:
// 这里建立事件
event IntegersAdded(uint x, uint y, uint result);
function add(uint _x, uint _y) public {
uint result = _x + _y;
//触发事件,通知app
IntegersAdded(_x, _y, result);
return result;
}
你的 app 前端可以监听这个事件。JavaScript 实现如下:
YourContract.IntegersAdded(function(error, result) {
// 干些事
}
实战演习
我们想每当一个僵尸创造出来时,我们的前端都能监听到这个事件,并将它显示出来。
- 定义一个 事件 叫做 NewZombie。 它有3个参数: zombieId (uint), name (string), 和 dna (uint)。
2.修改 _createZombie 函数使得当新僵尸造出来并加入zombies数组后,生成事件NewZombie。
3.需要定义僵尸id。 array.push() 返回数组的长度类型是uint - 因为数组的第一个元素的索引是 0, array.push() - 1 将是我们加入的僵尸的索引。 zombies.push() - 1 就是 id,数据类型是 uint。在下一行中你可以把它用到 NewZombie 事件中。
答案:
pragma solidity ^0.4.19;
contract ZombieFactory {
//插入事件
event NewZombie(uint zombieId, string name, uint dna);
uint dnaDigits = 16;
uint dnaModulus = 10 ** dnaDigits;
struct Zombie {
string name;
uint dna;
}
Zombie[] public zombies;
function _createZombie(string _name, uint _dna) private {
//
uint id = zombies.push(Zombie(_name, _dna)) - 1;
NewZombie(id, _name, _dna);
}
function _generateRandomDna(string _str) private view returns (uint) {
uint rand = uint(keccak256(_str));
return rand % dnaModulus;
}
function createRandomZombie(string _name) public {
uint randDna = _generateRandomDna(_name);
_createZombie(_name, randDna);
}
}
第14章: Web3.js
我们的 Solidity 合约完工了! 现在我们要写一段 JavaScript 前端代码来调用这个合约。
以太坊有一个 JavaScript 库,名为Web3.js。
在后面的课程里,我们会进一步地教你如何安装一个合约,如何设置Web3.js。 但是现在我们通过一段代码来了解 Web3.js 是如何和我们发布的合约交互的吧。
如果下面的代码你不能全都理解,不用担心。
// 下面是调用合约的方式:
var abi = /* abi是由编译器生成的 */
var ZombieFactoryContract = web3.eth.contract(abi)
var contractAddress = /* 发布之后在以太坊上生成的合约地址 */
var ZombieFactory = ZombieFactoryContract.at(contractAddress)
// `ZombieFactory` 能访问公共的函数以及事件
// 某个监听文本输入的监听器:
$("#ourButton").click(function(e) {
var name = $("#nameInput").val()
//调用合约的 `createRandomZombie` 函数:
ZombieFactory.createRandomZombie(name)
})
// 监听 `NewZombie` 事件, 并且更新UI
var event = ZombieFactory.NewZombie(function(error, result) {
if (error) return
generateZombie(result.zombieId, result.name, result.dna)
})
// 获取 Zombie 的 dna, 更新图像
function generateZombie(id, name, dna) {
let dnaStr = String(dna)
// 如果dna少于16位,在它前面用0补上
while (dnaStr.length < 16)
dnaStr = "0" + dnaStr
let zombieDetails = {
// 前两位数构成头部.我们可能有7种头部, 所以 % 7
// 得到的数在0-6,再加上1,数的范围变成1-7
// 通过这样计算:
headChoice: dnaStr.substring(0, 2) % 7 + 1,
// 我们得到的图片名称从head1.png 到 head7.png
// 接下来的两位数构成眼睛, 眼睛变化就对11取模:
eyeChoice: dnaStr.substring(2, 4) % 11 + 1,
// 再接下来的两位数构成衣服,衣服变化就对6取模:
shirtChoice: dnaStr.substring(4, 6) % 6 + 1,
//最后6位控制颜色. 用css选择器: hue-rotate来更新
// 360度:
skinColorChoice: parseInt(dnaStr.substring(6, 8) / 100 * 360),
eyeColorChoice: parseInt(dnaStr.substring(8, 10) / 100 * 360),
clothesColorChoice: parseInt(dnaStr.substring(10, 12) / 100 * 360),
zombieName: name,
zombieDescription: "A Level 1 CryptoZombie",
}
return zombieDetails
}
我们的 JavaScript 所做的就是获取由zombieDetails 产生的数据, 并且利用浏览器里的 JavaScript 神奇功能 (我们用 Vue.js),置换出图像以及使用CSS过滤器。在后面的课程中,你可以看到全部的代码。
网友评论