「区块链」:Solidity-basic
Solidity的官方教程笔记:basic。
Part 1
第2章: 合约
从最基本的开始入手:
Solidity 的代码都包裹在合约里面. 一份合约
就是以太应币应用的基本模块, 所有的变量和函数都属于一份合约, 它是你所有应用的起点.
一份名为 HelloWorld
的空合约如下:
1 | contract HelloWorld { |
版本指令
所有的 Solidity 源码都必须冠以 “version pragma” — 标明 Solidity 编译器的版本. 以避免将来新的编译器可能破坏你的代码。
例如: pragma solidity ^0.4.19;
(当前 Solidity 的最新版本是 0.4.19).
要有分号!
1 | pragma solidity ^0.4.19; |
第3章: 状态变量和整数
状态变量是被永久地保存在合约中。也就是说它们被写入以太币区块链中. 想象成写入一个数据库。
无符号整数: uint
uint
无符号数据类型, 指其值不能是负数,对于有符号的整数存在名为 int
的数据类型。
注: Solidity中, uint
实际上是 uint256
代名词, 一个256位的无符号整数。你也可以定义位数少的uints — uint8
, uint16
, uint32
, 等…… 但一般来讲你愿意使用简单的 uint
, 除非在某些特殊情况下,这我们后面会讲。
第4章: 数学运算
在 Solidity 中,数学运算很直观明了,与其它程序设计语言相同:
- 加法:
x + y
- 减法:
x - y
, - 乘法:
x * y
- 除法:
x / y
- 取模 / 求余:
x % y
(例如,13 % 5
余3
, 因为13除以5,余3)
Solidity 还支持 乘方操作\ (如:x 的 y次方)
如: 5 ** 2 = 25
1 | uint x = 5 ** 2; // equal to 5^2 = 25 |
第5章: 结构体
1 | struct Person { |
第6章: 数组
如果你想建立一个集合,可以用 数组这样的数据类型. Solidity 支持两种数组: 静态 数组和动态 数组:
1 | // 固定长度为2的静态数组: |
记住:状态变量被永久保存在区块链中。所以在你的合约中创建动态数组来保存成结构的数据是非常有意义的。
公共数组
你可以定义 public
数组, Solidity 会自动创建 getter 方法. 语法如下:
1 | Person[] public people; |
其它的合约可以从这个数组读取数据(但不能写入数据),所以这在合约中是一个有用的保存公共数据的模式。
第7章: 定义函数
在 Solidity 中函数定义的句法如下:
1 | function eatHamburgers(string _name, uint _amount) { |
注:习惯上函数里的变量都是以(*_
)开头 (但不是硬性规定) 以区别全局变量。(整个教程都会沿用这个习惯。)
第8章: 使用结构体和数组
现在我们学习创建新的 Person
结构,然后把它加入到名为 people
的数组中.
1 | // 创建一个新的Person: |
你也可以两步并一步,用一行代码更简洁:
1 | people.push(Person(16, "Vitalik")); |
注:
array.push()
在数组的 尾部 加入新元素 ,所以元素在数组中的顺序就是我们添加的顺序, 如:
第9章: 私有 / 公共函数
Solidity 定义的函数的属性默认为公共
。 这就意味着任何一方 (或其它合约) 都可以调用你合约里的函数。
显然,不是什么时候都需要这样,而且这样的合约易于受到攻击。 所以将自己的函数定义为私有
是一个好的编程习惯,只有当你需要外部世界调用它时才将它设置为公共
。
如何定义一个私有的函数呢?
1 | uint[] numbers; |
这意味着只有我们合约中的其它函数才能够调用这个函数,给 numbers
数组添加新成员。
可以看到,在函数名字后面使用关键字 private
即可。和函数的参数类似,私有函数的名字用(_
)起始。
第10章: 函数的更多属性
本章中我们将学习函数的返回值和修饰符。
返回值
要想函数返回一个数值,按如下定义:
1 | string greeting = "What's up dog"; |
Solidity 里,函数的定义里可包含返回值的数据类型(如本例中 string
)。
函数的修饰符
上面的函数实际上没有改变 Solidity 里的状态,即,它没有改变任何值或者写任何东西。
这种情况下我们可以把函数定义为 view, 意味着它只能读取数据不能更改数据:
1 | function sayHello() public view returns (string) { |
Solidity 还支持 pure 函数, 表明这个函数甚至都不访问应用里的数据,例如:
1 | function _multiply(uint a, uint b) private pure returns (uint) { |
这个函数甚至都不读取应用里的状态 — 它的返回值完全取决于它的输入参数,在这种情况下我们把函数定义为 pure.
注:可能很难记住何时把函数标记为 pure/view。 幸运的是, Solidity 编辑器会给出提示,提醒你使用这些修饰符。
第11章: Keccak256 和 类型转换
如何让 _generateRandomDna
函数返回一个全(半) 随机的 uint
?
Ethereum 内部有一个散列函数keccak256
,它用了SHA3版本。一个散列函数基本上就是把一个字符串转换为一个256位的16进制数字。字符串的一个微小变化会引起散列数据极大变化。
例子:
1 | //6e91ec6b618bb462a4a6ee5aa2cb0e9cf30f7a052bb467b0ba58b8748c00d2e5 |
注: 在区块链中安全地产生一个随机数是一个很难的问题, 本例的方法不安全,但是在我们的Zombie DNA算法里不是那么重要,已经很好地满足我们的需要了。*
类型转换
有时你需要变换数据类型。例如:
1 | uint8 a = 5; |
上面, a * b
返回类型是 uint
, 但是当我们尝试用 uint8
类型接收时, 就会造成潜在的错误。如果把它的数据类型转换为 uint8
, 就可以了,编译器也不会出错。
第13章: 事件
事件 是合约和区块链通讯的一种机制。你的前端应用“监听”某些事件,并做出反应。
例子:
1 | // 这里建立事件 |
你的 app 前端可以监听这个事件。JavaScript 实现如下:
1 | YourContract.IntegersAdded(function(error, result) { |
array.push()
返回数组的长度类型是uint
- 因为数组的第一个元素的索引是 0, array.push() - 1
将是我们加入的僵尸的索引。 zombies.push() - 1
就是 id
,数据类型是 uint
。
第14章: Web3.js
以太坊有一个 JavaScript 库,名为Web3.js。
在后面的课程里,我们会进一步地教你如何安装一个合约,如何设置Web3.js。 但是现在我们通过一段代码来了解 Web3.js 是如何和我们发布的合约交互的吧。
1 | // 下面是调用合约的方式: |
我们的 JavaScript 所做的就是获取由zombieDetails
产生的数据, 并且利用浏览器里的 JavaScript 神奇功能 (我们用 Vue.js),置换出图像以及使用CSS过滤器。在后面的课程中,你可以看到全部的代码。
Part 2
第2章: 映射(Mapping)和地址(Address)
如此一来,我们需要引入2个新的数据类型:mapping
(映射) 和 address
(地址)。
Addresses (地址)
以太坊区块链由 account ** (账户)组成,你可以把它想象成银行账户。一个帐户的余额是 **以太 (在以太坊区块链上使用的币种),你可以和其他帐户之间支付和接受以太币,就像你的银行帐户可以电汇资金到其他银行帐户一样。
每个帐户都有一个“地址”,你可以把它想象成银行账号。这是账户唯一的标识符,它看起来长这样:
1 | 0x0cE446255506E92DF41614C46F1d6df9Cc969183 |
现在你只需要了解地址属于特定用户(或智能合约)的。
Mapping(映射)
在第1课中,我们看到了 结构体 ** 和 **数组 。 映射 是另一种在 Solidity 中存储有组织数据的方法。
映射是这样定义的:
1 | //对于金融应用程序,将用户的余额保存在一个 uint类型的变量中: |
映射本质上是存储和查找数据所用的键-值对。
第3章: Msg.sender
msg.sender
在 Solidity 中,有一些全局变量可以被所有函数调用。 其中一个就是 msg.sender
,它指的是当前调用者(或智能合约)的 address
。
注意:在 Solidity 中,功能执行始终需要从外部调用者开始。 一个合约只会在区块链上什么也不做,除非有人调用其中的函数。所以
msg.sender
总是存在的。
1 | mapping (address => uint) favoriteNumber; |
在这个小小的例子中,任何人都可以调用 setMyNumber
在我们的合约中存下一个 uint
并且与他们的地址相绑定。 然后,他们调用 whatIsMyNumber
就会返回他们存储的 uint
。
使用 msg.sender
很安全,因为它具有以太坊区块链的安全保障 —— 除非窃取与以太坊地址相关联的私钥,否则是没有办法修改其他人的数据的。
跟在 JavaScript 中一样, 在 Solidity 中你也可以用 ++
使 uint
递增。
1 | uint number = 0; |
第4章: Require
require
使得函数在执行过程中,当不满足某些条件时抛出错误,并停止执行:
1 | function sayHiToVitalik(string _name) public returns (string) { |
如果你这样调用函数 sayHiToVitalik(“Vitalik”)
,它会返回“Hi!”。而如果调用的时候使用了其他参数,它则会抛出错误并停止执行。
因此,在调用一个函数之前,用 require
验证前置条件是非常有必要的。
第5章: 继承(Inheritance)
当代码过于冗长的时候,最好将代码和逻辑分拆到多个不同的合约中,以便于管理。
有个让 Solidity 的代码易于管理的功能,就是合约 inheritance (继承):
1 | contract Doge { |
由于 BabyDoge
是从 Doge
那里 inherits (继承)过来的。 这意味着当你编译和部署了 BabyDoge
,它将可以访问 catchphrase()
和 anotherCatchphrase()
和其他我们在 Doge
中定义的其他公共函数。
这可以用于逻辑继承(比如表达子类的时候,Cat
是一种 Animal
)。 但也可以简单地将类似的逻辑组合到不同的合约中以组织代码。
第6章: 引入(Import)
在 Solidity 中,当你有多个文件并且想把一个文件导入另一个文件时,可以使用 import
语句:
1 | import "./someothercontract.sol"; |
这样当我们在合约(contract)目录下有一个名为 someothercontract.sol
的文件( ./
就是同一目录的意思),它就会被编译器导入。
第7章: Storage与Memory
在 Solidity 中,有两个地方可以存储变量 —— storage
或 memory
。
Storage 变量是指永久存储在区块链中的变量。 Memory 变量则是临时的,当外部函数对某合约调用完成时,内存型变量即被移除。 你可以把它想象成存储在你电脑的硬盘或是RAM中数据的关系。
大多数时候你都用不到这些关键字,默认情况下 Solidity 会自动处理它们。 状态变量(在函数之外声明的变量)默认为“存储”形式,并永久写入区块链;而在函数内部声明的变量是“内存”型的,它们函数调用结束后消失。
然而也有一些情况下,你需要手动声明存储类型,主要用于处理函数内的 结构体 和 数组 时:
1 | contract SandwichFactory { |
当你不得不使用到这些关键字的时候,Solidity 编译器会发警示提醒你的。
第9章: 更多关于函数可见性
错误在于,我们尝试从 ZombieFeeding
中调用 _createZombie
函数,但 _createZombie
却是 ZombieFactory
的 private
(私有)函数。这意味着任何继承自 ZombieFactory
的子合约都不能访问它。
internal 和 external
除 public
和 private
属性之外,Solidity 还使用了另外两个描述函数可见性的修饰词:internal
(内部) 和 external
(外部)。
internal
和 private
类似,不过, 如果某个合约继承自其父合约,这个合约即可以访问父合约中定义的“内部”函数。而private只允许合约内部函数访问。
external
与public
类似,只不过这些函数只能在合约之外调用,它们不能被合约内的其他函数调用。稍后我们将讨论什么时候使用 external
和 public
。而public允许合约内和合约外的函数调用。
声明函数 internal
或 external
类型的语法,与声明 private
和 public
类 型相同:
1 | contract Sandwich { |
与其他合约的交互
如果我们的合约需要和区块链上的其他的合约会话,则需先定义一个 interface (接口)。
先举一个简单的栗子。 假设在区块链上有这么一个合约:
1 | contract LuckyNumber { |
这是个很简单的合约,您可以用它存储自己的幸运号码,并将其与您的以太坊地址关联。 这样其他人就可以通过您的地址查找您的幸运号码了。
现在假设我们有一个外部合约,使用 getNum
函数可读取其中的数据。
首先,我们定义 LuckyNumber
合约的 interface :
1 | contract NumberInterface { |
请注意,这个过程虽然看起来像在定义一个合约,但其实内里不同:
首先,我们只声明了要与之交互的函数 (在本例中为 getNum
),在其中我们没有使用到任何其他的函数或状态变量。
其次,我们并没有使用大括号({
和 }
)定义函数体,我们单单用分号(;
)结束了函数声明。这使它看起来像一个合约框架。
编译器就是靠这些特征认出它是一个接口的。
在我们的 app 代码中使用这个接口,合约就知道其他合约的函数是怎样的,应该如何调用,以及可期待什么类型的返回值。
我们已经为你查看过了 CryptoKitties 的源代码,并且找到了一个名为 getKitty
的函数,它返回所有的加密猫的数据,包括它的“基因”(我们的僵尸游戏要用它生成新的僵尸)。
该函数如下所示:
1 | function getKitty(uint256 _id) external view returns ( |
这个函数看起来跟我们习惯的函数不太一样。 它竟然返回了…一堆不同的值! 在 Solidity中,可以让一个函数返回多个值。
第11章: 使用接口
继续前面 NumberInterface
的例子,我们既然将接口定义为:
1 | contract NumberInterface { |
我们可以在合约中这样使用:
- 获得合约地址(该合约必须为external或者public)
- 定义一个变量:指向该合约地址的合约对象(之前定义的与之交互的接口对象)
- 调用该合约对象中的函数
1 | contract MyContract { |
通过这种方式,只要将您合约的可见性设置为public
(公共)或external
(外部),它们就可以与以太坊区块链上的任何其他合约进行交互。
第12章: 处理多返回值
getKitty
是我们所看到的第一个返回多个值的函数。我们来看看是如何处理的:
1 | function multipleReturns() internal returns(uint a, uint b, uint c) { |
第13章: 奖励: Kitty 基因
if 语句
if语句的语法在 Solidity 中,与在 JavaScript 中差不多:
1 | function eatBLT(string sandwich) public { |
第14章: 放在一起
JavaScript 实现
我们只用编译和部署 ZombieFeeding
,就可以将这个合约部署到以太坊了。我们最终完成的这个合约继承自 ZombieFactory
,因此它可以访问自己和父辈合约中的所有 public 方法。
我们来看一个与我们的刚部署的合约进行交互的例子, 这个例子使用了 JavaScript 和 web3.js:
1 | var abi = /* abi generated by the compiler */ |
「区块链」:Solidity-basic