备注

  • 本文绝大部分是直接从 搭建僵尸工厂 的一个以闯关学习Solidity相关知识的(国外?)网站抄写下来的,该网站自带sodility编辑器,而且每章节都必须按照实战演习的内容完成测试才能进入到下一章节,非常适合用一定编程基础的小伙伴自学solidity编程
  • 我整理这篇文章,一是分享觉得好的solidity学习资源给大伙;二是重新学习下solidity;三是整理上述网站的学习内容,让我自己找起来方便;
  • 上述网站是开源的,如本篇内容有侵权,可联系本博客删除;
  • 好记性不如烂笔头,抄一遍等于过一遍脑子,比扫一遍或看一遍的效果好的多得多;
  • 最后demo的步骤序号与标题序号一致,如果哪个步骤忘了具体含义,可查看对应序号的标题内容;

1. 合约

  • 所有Solidity源码必须冠以version pragrma 标明Solidity编译器的版本,以避免将来新的编译器可能破坏你的代码:
  • Solidity中合约的含义就是一组代码(它的函数)和数据(它的状态) ,它们位于以太坊区块链的一个特定地址上。

2. 状态变量和整数

  • 状态变量 是永久地保存在合约中;

无符号整数:uint

  • uint 无符号整数类型,指其值不能是负数,对于有符号的整数存在名为int的数据类型;

注:Solidity中,uint实际上是uint256代名词,一个256位的无符号整数;你也可以定义位数少的uints—uint8,uint16,uint32等···

3. 数学运算

  • 加法: x + y
  • 减法: x – y
  • 乘法: x * y
  • 除法: x / y
  • 取模/求余: x % y
  • 乘方: x ** y (例子:5 ** 2=25)

4. 结构体

  • 有时你需要更复杂的数据类型,Solidity提供了结构体;结构体允许你生成一个更复杂的数据类型,它有多个属性
struct Person {uint age;string name;}

注: string类型:字符串用于保存任意长度的UFT-8编码数据。

5. 数组

  • Solidity支持两种数据:静态数组和动态数组:
//固定长度为2的静态数组;uint[2] fixedArray;//固定长度为5的string类型的静态数组string[5] stringArray;//动态数组,长度不固定,可以动态添加元素uint[] dynamicArry;

5.1 公共数组

  • 你可以定义public数组,Solidity会自动创建getter方法,语法如下:
Person[] public perple;

其他的合约可以从这个数组读取数据(但不能写入数据),所以这在合约中是一个有用的保存公共数据的模式。

6. 定义函数

  • 在Solidity中函数定义的语法如下:
function eatHamburgers(string _name, uint _amount) {}

这是一个名为 eatHamburgers 的函数,它接受两个参数:一个 string类型的 和 一个 uint类型的。现在函数内部还是空的。

注:习惯上函数里的变量都是以(_)开头(但不是硬性规定)以区别全局变量。

7. 使用结构体和数组

现在我们学习创建新的Person结构,然后把它加入到people的数组中:

//创建一个新的PersonPerson satoshi = Person(172,"satoshi");//将新创建的satoshi添加进people数组people.push(satoshi);

你也可以两步并一步,用一行代码更简洁;

people.push(Person(16,"Vitalik"));

注:array.push() 在数组的 尾部加入新元素,所以元素在数组中的顺序就是我们添加的顺序;

8. 私有/公共函数

  • Solidity定义的函数的属性默认为公共,这意味着任何一方(或其他合约)都可以调用你合约里的函数
    显然,不是什么时候都需要这样,而且这样的合约易于受到攻击,所以将自己的函数定义为私有是一个好的编程习惯,只有当你需要外部世界调用它时才将它设置为公共
    如何定义一个私有的函数呢?
uint[] numbers;function _addToArray(uint _number) private {numbers.push(_number);}

这意味着只有我们合约中的其他函数才能够调用这个函数,给numbers数组添加新成员;
可以看到,在函数名字后面使用关键字private即可 和函数的参数类似,私有函数的名字用(_)起始;

9. 函数的更多属性

返回值

要想函数返回一个数值,按如下定义:

string greeting = "What's up dog";function syHello() public resutns(string) {return greeting;}

Solidity里,函数的定义里可包含返回值的数据类型

函数的修饰符

上面的函数实际上没有改变Solidity里的状态,它没有改变任何值或者写任何东西;
这种情况下我们可以把函数定义为view—意味着它只能读取数据而不能更改数据

functioin sayHello() public view returns (string) {}

Solidity还支持pure函数—表明这个函数甚至都不访问应用里的函数 ,例如:

functioin _multply(uint a,uint b) private pure returns(uint) {return a * b;}

这个函数甚至都不读取应用里的状态—它的返回值完全取决于它的输入参数,在这种情况下我们把函数定义为pure

注:可能很难记住何时把函数标记为pure/view;幸运的是,Solidity编辑器会给出提示,提醒你使用这些修饰符。

10.Keccak256 和 类型转换

Ethereum内部有一个散列函数keccak256 ,它用了SHA3版本;一个散列函数基本上就是把一个字符串转换为一个256位的16进制数字。 字符串的一个微小变化会引起散列数据极大变化。
这在Ethereum中有很多应用,但是我们现在知识用它造一个伪随机数;

//6e91ec6b618bb462a4a6ee5aa2cb0e9cf30f7a052bb467b0ba58b8748c00d2e5keccak256("aaaab");//b1f078126895a1424524de5321b339ab00408010b7cf0e6ed451514981e58aa9keccak256("aaaac");

显而易见,输入字符串只改变了一个字母,输出就已经天壤之别了。

注:在区块链中安全地生产一个随机数是一个很难的问题,本例的方法不安全;

类型转换

uint8 a = 5;uint b = 6;//将会抛出错误,因为a*b返回uint,而不是uint8uint8 c = a * b;//我们需要将b转换为uint8uint8 c = a * uint8(b);

上面, a * b 返回类型是 uint, 但是当我们尝试用 uint8 类型接收时, 就会造成潜在的错误。如果把它的数据类型转换为 uint8, 就可以了,编译器也不会出错。

12. 事件

事件是合约和区块链通讯的一种机制。你的前端应用“监听”某些事件,并做出反应;

//这里建立事件event IntegersAdded(uint x, uint y, uint result);function add(uint _x, uint _y) public {uint result = _x + _y;//触发事件,通知appIntegersAdded(_x,_y,result);return result;}

你的app前端可以监听这个事件,JavaScript实现如下:

YourContract.IntegersAdded(function(error,result){})

demo

pragma solidity ^0.4.19;//1.1这里写版本指令//1.2 这里建立智能合约contract ZombieFactory {//12.1定义一个 事件 叫做 NewZombie。 它有3个参数: zombieId (uint), name (string), 和 dna (uint)。event NewZombie(uint zombieId, string name, uint dna);//2. 定义 dnaDigits 为 uint 数据类型, 并赋值 16。uint dnaDigits = 16;// 3. 建立一个uint类型的变量,名字叫dnaModulus, 令其等于 10 的 dnaDigits 次方. uint dnaModulus = 10 ** dnaDigits;//4.1 建立一个struct 命名为 Zombie. //4.2 Zombie 结构体有两个属性: name (类型为 string), 和 dna (类型为 uint)。 struct Zombie {string name;uint dna;}//5. 创建一个数据类型为 Zombie 的结构体数组,用 public 修饰,命名为:zombies.Zombie[] public zombies;////6. 建立一个函数 createZombie。 它有两个参数: _name (类型为string), 和 _dna (类型为uint)。//function createZombie(string _name, uint _dna) {////7 在函数体里新创建一个 Zombie, 然后把它加入 zombies 数组中。 新创建的僵尸的 name 和 //dna,来自于函数的参数。(用一行代码简洁地完成它。)//zombies.push(Zombie(_name,_dna));//}//8. 我们合约的函数 createZombie 的默认属性是公共的,这意味着任何一方都可以调用它去创建一个僵尸。 变 createZombie 为私有函数,不要忘记遵守命名的规矩哦function _createZombie(string _name, uint _dna) private {//7 在函数体里新创建一个 Zombie, 然后把它加入 zombies 数组中。 新创建的僵尸的 name 和 dna,来自于函数的参数。(用一行代码简洁地完成它。)//zombies.push(Zombie(_name,_dna));//12.2需要定义僵尸id。 array.push() 返回数组的长度类型是uint - 因为数组的第一个元素的索引是 0, array.push() - 1 将是我们加入的僵尸的索引。 zombies.push() - 1 就是 id,数据类型是 uint。在下一行中你可以把它用到 NewZombie 事件中。//12.3 修改 _createZombie 函数使得当新僵尸造出来并加入 zombies数组后,生成事件NewZombie。uint id = zombies.push(Zombie(_name, _dna)) - 1;NewZombie(id, _name, _dna);}//9.1 创建一个 private 函数,命名为 _generateRandomDna。它只接收一个输入变量 _str (类型 string), 返回一个 uint 类型的数值。//9.2 此函数只读取我们合约中的一些变量,所以标记为view。function _generateRandomDna(string _str) private view returns(uint) {//10.1第一行代码取 _str 的 keccak256 散列值生成一个伪随机十六进制数,类型转换为 uint, 最后保存在类型为 uint 名为 rand 的变量中。//10.2 我们只想让我们的DNA的长度为16位 (还记得 dnaModulus?)。所以第二行代码应该 return 上面计算的数值对 dnaModulus 求余数(%)。 uint rand = uint(keccak256(_str)); return rand % dnaModulus;}//11.1 创建一个 public 函数,命名为 createRandomZombie. 它将被传入一个变量 _name (数据类型是 string)。 (注: 定义公共函数 public 和定义一个私有 private 函数的做法一样)。function createRandomZombie(string _name) public {//11.2 函数的第一行应该调用 _generateRandomDna 函数,传入 _name 参数, 结果保存在一个类型为 uint 的变量里,命名为 randDna。uint randDna = _generateRandomDna(_name);//11.3 第二行调用 _createZombie 函数, 传入参数: _name 和 randDna。_createZombie(_name,randDna);}}