链接
https://cryptozombies.io/zh/course
这个小游戏非常不错.它一步一步教我们如何完成一个DAPP.它详细的展示了如何完整的开发一个区块链项目(除了将智能合约部署到区块链这一部分).学习的过程中.还会讲到一些基本概念和solidity语法的应用.适合想学习以太坊智能合约开发的小白用来入门.
教程一:搭建僵尸工厂
教程一的实现目标是创造一个”僵尸工厂”, 用它建立一支僵尸部队。
- 我们的工厂会把我们部队中所有的僵尸保存到数据库中
- 工厂会有一个函数能产生新的僵尸
- 每个僵尸会有一个随机的独一无二的面孔
其实教程一的目的主要是通过搭建僵尸工厂这个功能来帮助大家熟悉sodility的基本语法.
一:创建合约
pragma solidity ^0.4.19;//1. 这里写版本指令
//2. 这里建立僵尸工厂智能合约
contract ZombieFactory {
}
Solidity 的代码都包裹在合约里面. 一份合约就是以太应币应用的基本模块, 所有的变量和函数都属于一份合约, 它是你所有应用的起点.
注意:
1.
注意版本指令里的^和结尾的分号容易忘掉写;
2. 合约和版本指令之间空2行
二:状态变量和整数
pragma solidity ^0.4.19;
contract ZombieFactory {
// 这个无符号整数将会永久的被保存在区块链中
// 定义一个僵尸的DNA位数(我们的僵尸DNA将由一个十六位数字组成)
uint dnaDigits = 16;
}
状态变量是被永久地保存在合约中。也就是说它们被写入以太币区块链中. 想象成写入一个数据库。
uint 无符号数据类型, 指其值不能是负数,对于有符号的整数存在名为 int 的数据类型。
注: Solidity中, uint 实际上是 uint256代名词, 一个256位的无符号整数。它是8位步进的.你也可以定义位数少的uints — uint8, uint16, uint32, 等…… 但一般来讲你愿意使用简单的 uint, 但在某些特殊情况下是需要使用指定位数的,因为可以节省gas.
三:数学运算
与其它程序设计语言相同, + , – , * , /, %
Solidity 还支持 乘方操作 (如:x 的 y次方) // 例如: 5 ** 2 = 25
uint x = 5 ** 2; // equal to 5^2 = 25
四:结构体
struct Person {
uint age;
string name;
}
结构体允许你生成一个更复杂的数据类型,它有多个属性。
注:我们刚刚引进了一个新类型, string。 字符串用于保存任意长度的 UTF-8 编码数据。 如: string greeting = “Hello world!”。
结构体也是一种类型,类似于对象类型.
五:数组
如果想建立一个集合,可以用 数组这样的数据类型. 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;
其他合约可以从这个数组中读取数据
(但不能写入数据)
,所以这在合约中是一个有用的保存公共数据的模式.
使用结构体和数组
// 创建一个新的Person:
Person satoshi = Person(172, "Satoshi");
// 将新创建的satoshi添加进people数组:
people.push(satoshi);
people.push(Person(16, "Vitalik"));
注:
array.push()
在数组的 尾部 加入新元素 ,所以元素在数组中的顺序就是我们添加的顺序
六:定义函数
在 Solidity 中函数定义的句法如下:
function eatHamburgers(string _name, uint _amount) {
}
这是一个名为
eatHamburgers
的函数,它接受两个参数:一个
string
类型的 和 一个
uint
类型的。现在函数内部还是空的。
注:习惯上函数里的变量都是以(
_
)开头 (但不是硬性规定) 以区别全局变量。
我们的函数定义如下:
eatHamburgers("vitalik", 100);
私有/公共函数
Solidity 定义的函数的属性默认为
公共(public)
。 这就意味着任何一方 (或其它合约) 都可以调用你合约里的函数。
显然,不是什么时候都需要这样,而且这样的合约易于受到攻击。 所以将自己的函数定义为私有是一个好的编程习惯,只有当你需要外部世界调用它时才将它设置为公共。
uint[] numbers;
function _addToArray(uint _number) private {
numbers.push(_number);
}
这意味着只有我们合约中的其它函数才能够调用这个函数,给 numbers 数组添加新成员。
可以看到,
在函数名字后面使用关键字 private 即可,私有函数的名字用(_)下划线起始,而公有函数不需要下划线开头,这属于命名规范,我们可以通过这些区分合约中那些函数是私有的
。
返回值
Solidity 里,函数的定义里可包含返回值的数据类型(如本例中 string)。
string greeting = "What's up dog";
function sayHello() public returns (string) {
return greeting;
}
函数修饰符
函数定义为 view, 意味着它只能读取数据不能更改数据:
function sayHello() public view returns (string) {
}
Solidity 还支持 pure 函数, 表明这个函数甚至都不访问应用里的数据,例如:
function _multiply(uint a, uint b) private pure returns (uint) {
return a * b;
}
这个函数甚至都不读取应用里的状态 — 它的返回值完全取决于它的输入参数,在这种情况下我们把函数定义为 pure.
更多关于函数的可见性
除 public 和 private 属性之外,Solidity 还使用了另外两个描述函数可见性的修饰词:internal(内部) 和 external(外部)。
internal 和 private 类似,不过,如果某个合约继承自其父合约,这个合约即可以访问父合约中定义的“内部”函数。
external 与public 类似,只不过这些函数只能在合约之外调用 – 它们不能被合约内的其他函数调用。稍后我们将讨论什么时候使用 external 和 public。
声明函数 internal 或 external 类型的语法,与声明 private 和 public类 型相同.
contract Sandwich {
uint private sandwichesEaten = 0;
function eat() internal {
sandwichesEaten++;
}
}
contract BLT is Sandwich {
uint private baconSandwichesEaten = 0;
function eatWithBacon() public returns (string) {
baconSandwichesEaten++;
// 因为eat() 是internal 的,所以我们能在这里调用
eat();
}
}
多返回值函数如何处理
contract KittyInterface {
function getKitty(uint256 _id) external view returns (
bool isGestating,
bool isReady,
uint256 cooldownIndex,
uint256 nextActionAt,
uint256 siringWithId,
uint256 birthTime,
uint256 matronId,
uint256 sireId,
uint256 generation,
uint256 genes
);
}
这是从迷恋猫项目中拿出的一个接口.这里这个函数
getKitty
就是一个多返回值函数.我们是如何处理多返回值函数呢:
function multipleReturns() internal returns(uint a, uint b, uint c) {
return (1, 2, 3);
}
function processMultipleReturns() external {
uint a;
uint b;
uint c;
// 这样来做批量赋值:
(a, b, c) = multipleReturns();
}
// 或者如果我们只想返回其中一个变量:
function getLastReturnValue() external {
uint c;
// 可以对其他字段留空:
(,,c) = multipleReturns();
}
结构体可以作为参数传入函数
由于结构体的存储指针可以以参数的方式传递给一个 private 或 internal 的函数,因此结构体可以在多个函数之间相互传递。
遵循这样的语法:
function _doStuff(Zombie storage _zombie) internal {
// do stuff with _zombie
}
公有函数和安全性
函数修饰符onlyOwner 只允许用户自己调用
或者设置函数可见性为internal
进一步了解函数修饰符-带参数的函数修饰符
// 存储用户年龄的映射
mapping (uint => uint) public age;
// 限定用户年龄的修饰符
modifier olderThan(uint _age, uint _userId) {
require(age[_userId] >= _age);
_;
}
// 必须年满16周岁才允许开车 (至少在美国是这样的).
// 我们可以用如下参数调用`olderThan` 修饰符:
function driveCar(uint _userId) public olderThan(16, _userId) {
// 其余的程序逻辑
}
看到了吧, olderThan 修饰符可以像函数一样接收参数,是“宿主”函数 driveCar 把参数传递给它的修饰符的。
七:Keccak256 和 类型转换
Ethereum 内部有一个散列函数keccak256,它用了SHA3版本。一个散列函数基本上就是把一个字符串转换为一个256位的16进制数字。字符串的一个微小变化会引起散列数据极大变化。
这在 Ethereum 中有很多应用,但是现在我们只是用它造一个伪随机数。
//6e91ec6b618bb462a4a6ee5aa2cb0e9cf30f7a052bb467b0ba58b8748c00d2e5
keccak256("aaaab");
//b1f078126895a1424524de5321b339ab00408010b7cf0e6ed451514981e58aa9
keccak256("aaaac");
显而易见,输入字符串只改变了一个字母,输出就已经天壤之别了.
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, 就可以了,编译器也不会出错。
八:事件
事件
是合约和区块链通讯的一种机制。你的前端应用“监听”某些事件,并做出反应。
// 这里建立事件
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) {
// 干些事
}