Solidity是以太坊的主要编程语言,它是一种静态类型的 JavaScript-esque 语言,是面向合约的、为实现智能合约而建立的高级编程语言,设计的目的是能在以太坊虚拟机(EVM)上运行。javascript
本文基于CryptoZombies,教程地址为:https://cryptozombies.io/zh/lesson/2
html
以太坊区块链由 account (帐户)组成,你能够把它想象成银行帐户。一个账户的余额是以太 (在以太坊区块链上使用的币种),你能够和其余账户之间支付和接受以太币,就像你的银行账户能够电汇资金到其余银行账户同样。java
每一个账户都有一个“地址”,你能够把它想象成银行帐号。这是帐户惟一的标识符,它看起来长这样:python
0x0cE446255506E92DF41614C46F1d6df9Cc969183
复制代码
这是 CryptoZombies 团队的地址,为了表示支持CryptoZombies,能够赞扬一些以太币!git
address
:地址类型存储一个 20 字节的值(以太坊地址的大小)。 地址类型也有成员变量,并做为全部合约的基础。web
address
类型是一个160位的值,且不容许任何算数操做。这种类型适合存储合约地址或外部人员的密钥对。编程
Mappings 和哈希表相似,它会执行虚拟初始化,以使全部可能存在的键都映射到一个字节表示为全零的值。小程序
映射是这样定义的:设计模式
//对于金融应用程序,将用户的余额保存在一个 uint类型的变量中:
mapping (address => uint) public accountBalance;
//或者能够用来经过userId 存储/查找的用户名
mapping (uint => string) userIdToName;
复制代码
映射本质上是存储和查找数据所用的键-值对。在第一个例子中,键是一个 address,值是一个 uint,在第二个例子中,键是一个uint,值是一个 string。api
映射类型在声明时的形式为 mapping(_KeyType => _ValueType)。 其中 _KeyType 能够是除了映射、变长数组、合约、枚举以及结构体之外的几乎全部类型。 _ValueType 能够是包括映射类型在内的任何类型。
对映射的取值操做以下:
userIdToName[12]
// 若是键12 不在 映射中,获得的结果是0
复制代码
映射中,实际上并不存储 key,而是存储它的 keccak256 哈希值,从而便于查询实际的值。因此映射是没有长度的,也没有 key 的集合或 value 的集合的概念。,你不能像操做
python
字典那应该获取到当前 Mappings 的全部键或者值。
在 Solidity 中,在全局命名空间中已经存在了(预设了)一些特殊的变量和函数,他们主要用来提供关于区块链的信息或一些通用的工具函数。
msg.sender指的是当前调用者(或智能合约)的 address。
注意:在 Solidity 中,功能执行始终须要从外部调用者开始。 一个合约只会在区块链上什么也不作,除非有人调用其中的函数。因此对于每个外部函数调用,包括 msg.sender 和 msg.value 在内全部 msg 成员的值都会变化。这里包括对库函数的调用。
如下是使用 msg.sender 来更新 mapping 的例子:
mapping (address => uint) favoriteNumber;
function setMyNumber(uint _myNumber) public {
// 更新咱们的 `favoriteNumber` 映射来将 `_myNumber`存储在 `msg.sender`名下
favoriteNumber[msg.sender] = _myNumber;
// 存储数据至映射的方法和将数据存储在数组类似
}
function whatIsMyNumber() public view returns (uint) {
// 拿到存储在调用者地址名下的值
// 若调用者还没调用 setMyNumber, 则值为 `0`
return favoriteNumber[msg.sender];
}
复制代码
在这个小小的例子中,任何人均可以调用 setMyNumber 在咱们的合约中存下一个 uint 而且与他们的地址相绑定。 而后,他们调用 whatIsMyNumber 就会返回他们存储的 uint。
使用 msg.sender 很安全,由于它具备以太坊区块链的安全保障 —— 除非窃取与以太坊地址相关联的私钥,不然是没有办法修改其余人的数据的。
如下是其它的一些特殊变量。
Solidity 使用状态恢复异常来处理错误。这种异常将撤消对当前调用(及其全部子调用)中的状态所作的全部更改,而且还向调用者标记错误。
函数 assert
和 require
可用于检查条件并在条件不知足时抛出异常。
这里主要介绍 require
require使得函数在执行过程当中,当不知足某些条件时抛出错误,并中止执行:
function sayHiToVitalik(string _name) public returns (string) {
// 比较 _name 是否等于 "Vitalik". 若是不成立,抛出异常并终止程序
// (敲黑板: Solidity 并不支持原生的字符串比较, 咱们只能经过比较
// 两字符串的 keccak256 哈希值来进行判断)
require(keccak256(_name) == keccak256("Vitalik"));
// 若是返回 true, 运行以下语句
return "Hi!";
}
复制代码
若是你这样调用函数 sayHiToVitalik("Vitalik")
,它会返回“Hi!”。而若是调用的时候使用了其余参数,它则会抛出错误并中止执行。
所以,在调用一个函数以前,用 require 验证前置条件是很是有必要的。
注意:在 Solidity 中,关键词放置的顺序并不重要
// 如下两个语句等效
require(keccak256(_name) == keccak256("Vitalik"));
require(keccak256("Vitalik") == keccak256(_name));
复制代码
除 public 和 private 属性以外,Solidity 还使用了另外两个描述函数可见性的修饰词:internal(内部) 和 external(外部)。
internal
和 private
相似,不过,若是某个合约继承自其父合约,这个合约便可以访问父合约中定义的“内部(internal)”函数
。
external
与public
相似,只不过external
函数只能在合约以外调用 - 它们不能被合约内的其余函数调用。
声明函数 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();
}
}
复制代码
Solidity 有两种函数调用(内部调用不会产生实际的 EVM 调用或称为消息调用
,而外部调用则会产生一个 EVM 调用), 函数和状态变量有四种可见性类型。 函数能够指定为 external ,public ,internal 或者 private,默认状况下函数类型为 public。 对于状态变量,不能设置为 external ,默认是 internal 。
external :
外部函数做为合约接口的一部分,意味着咱们能够从其余合约和交易中调用。 一个外部函数 f 不能从内部调用(即 f 不起做用,但 this.f() 能够)。 当收到大量数据的时候,外部函数有时候会更有效率。
public :
public 函数是合约接口的一部分,能够在内部或经过消息调用。对于公共状态变量, 会自动生成一个 getter 函数。
internal :
这些函数和状态变量只能是内部访问(即从当前合约内部或从它派生的合约访问),不使用 this 调用。
private :
private 函数和状态变量仅在当前定义它们的合约中使用,而且不能被派生合约使用。
合约中的全部内容对外部观察者都是可见的。设置一些 private 类型只能阻止其余合约访问和修改这些信息, 可是对于区块链外的整个世界它仍然是可见的。
可见性标识符的定义位置,对于状态变量来讲是在类型后面,对于函数是在参数列表和返回关键字中间。
pragma solidity ^0.4.16;
contract C {
// 对于函数是在参数列表和返回关键字中间。
function f(uint a) private pure returns (uint b) { return a + 1; }
function setData(uint a) internal { data = a; }
uint public data; // 对于状态变量来讲是在类型后面
}
复制代码
和 python 相似,Solidity 函数支持多值返回,好比:
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();
}
复制代码
这里留空字段使用
,
的方式太不直观了,还不如 python/go 使用下划线_
代替无用字段。
在 Solidity 中,有两个地方能够存储变量 —— storage 或 memory。
Storage 变量是指永久存储在区块链中的变量。 Memory 变量则是临时的,当外部函数对某合约调用完成时,内存型变量即被移除。 你能够把它想象成存储在你电脑的硬盘或是RAM中数据的关系。
storage 和 memory 放到状态变量名前边,在类型后边,格式以下:
变量类型 <storage|memory> 变量名
大多数时候都用不到这些关键字,默认状况下 Solidity 会自动处理它们。 状态变量(在函数以外声明的变量)默认为“存储”形式,并永久写入区块链;而在函数内部声明的变量是“内存”型的,它们函数调用结束后消失。
然而也有一些状况下,你须要手动声明存储类型,主要用于处理函数内的 结构体
和 数组
时:
contract SandwichFactory {
struct Sandwich {
string name;
string status;
}
Sandwich[] sandwiches;
function eatSandwich(uint _index) public {
// Sandwich mySandwich = sandwiches[_index];
// ^ 看上去很直接,不过 Solidity 将会给出警告
// 告诉你应该明确在这里定义 `storage` 或者 `memory`。
// 因此你应该明肯定义 `storage`:
Sandwich storage mySandwich = sandwiches[_index];
// ...这样 `mySandwich` 是指向 `sandwiches[_index]`的指针
// 在存储里,另外...
mySandwich.status = "Eaten!";
// ...这将永久把 `sandwiches[_index]` 变为区块链上的存储
// 若是你只想要一个副本,可使用`memory`:
Sandwich memory anotherSandwich = sandwiches[_index + 1];
// ...这样 `anotherSandwich` 就仅仅是一个内存里的副本了
// 另外
anotherSandwich.status = "Eaten!";
// ...将仅仅修改临时变量,对 `sandwiches[_index + 1]` 没有任何影响
// 不过你能够这样作:
sandwiches[_index + 1] = anotherSandwich;
// ...若是你想把副本的改动保存回区块链存储
}
}
复制代码
若是你尚未彻底理解究竟应该使用哪个,也不用担忧 —— 在本教程中,咱们将告诉你什么时候使用 storage 或是 memory,而且当你不得不使用到这些关键字的时候,Solidity 编译器也发警示提醒你的。
如今,只要知道在某些场合下也须要你显式地声明 storage 或 memory就够了!
Solidity 的继承和 Python 的继承类似,支持多重继承。 看下面这个例子:
contract Doge {
function catchphrase() public returns (string) {
return "So Wow CryptoDoge";
}
}
contract BabyDoge is Doge {
function anotherCatchphrase() public returns (string) {
return "Such Moon BabyDoge";
}
}
// 能够多重继承。请注意,Doge 也是 BabyDoge 的基类,
// 但只有一个 Doge 实例(就像 C++ 中的虚拟继承)。
contract BlackBabyDoge is Doge, BabyDoge {
function color() public returns (string) {
return "Black";
}
}
复制代码
BabyDoge
从 Doge
那里 inherits(继承)
过来。 这意味着当编译和部署了 BabyDoge
,它将能够访问 catchphrase() 和 anotherCatchphrase()和其余咱们在 Doge 中定义的其余公共函数(private 函数不可访问)。
Solidity使用 is 从另外一个合约派生。派生合约能够访问全部非私有成员,包括内部函数和状态变量,但没法经过 this
来外部访问。
派生合约须要提供基类构造函数须要的全部参数。这能够经过两种方式来完成:
pragma solidity ^0.4.0;
contract Base {
uint x;
// 这是注册 Base 和设置名称的构造函数。
function Base(uint _x) public { x = _x; }
}
contract Derived is Base(7) {
function Derived(uint _y) Base(_y * _y) public {
}
}
contract Derived1 is Base {
function Derived1(uint _y) Base(_y * _y) public {
}
}
复制代码
一种方法直接在继承列表中调用基类构造函数(is Base(7)
)。 另外一种方法是像 修饰器 modifier
使用方法同样, 做为派生合约构造函数定义头的一部分,(Base(_y * _y)
)。 若是构造函数参数是常量而且定义或描述了合约的行为,使用第一种方法比较方便。 若是基类构造函数的参数依赖于派生合约,那么必须使用第二种方法。 若是像这个简单的例子同样,两个地方都用到了,优先使用 修饰器modifier 风格的参数。
合约函数能够缺乏实现,以下例所示(请注意函数声明头由 ; 结尾):
pragma solidity ^0.4.0;
contract Feline {
function utterance() public returns (bytes32); } 复制代码
这些合约没法成功编译(即便它们除了未实现的函数还包含其余已经实现了的函数),但他们能够用做基类合约:
pragma solidity ^0.4.0;
contract Feline {
function utterance() public returns (bytes32); } contract Cat is Feline {
function utterance() public returns (bytes32) { return "miaow"; }
}
复制代码
若是合约继承自抽象合约,而且没有经过重写来实现全部未实现的函数,那么它自己就是抽象的。
接口相似于抽象合约,可是它们不能实现任何函数。还有进一步的限制:
首先,看一下一个interface的例子:
contract NumberInterface {
function getNum(address _myAddress) public view returns (uint); } 复制代码
请注意,这个过程虽然看起来像在定义一个合约,但其实内里不一样:
;
)结束了函数声明。这使它看起来像一个合约框架。编译器就是靠这些特征认出它是一个接口的。
就像继承其余合约同样,合约能够继承接口。
能够在合约中这样使用接口:
contract MyContract {
address NumberInterfaceAddress = 0xab38...;
// ^ 这是FavoriteNumber合约在以太坊上的地址
NumberInterface numberContract = NumberInterface(NumberInterfaceAddress);
// 如今变量 `numberContract` 指向另外一个合约对象
function someFunction() public {
// 如今咱们能够调用在那个合约中声明的 `getNum`函数:
uint num = numberContract.getNum(msg.sender);
// ...在这儿使用 `num`变量作些什么
}
}
复制代码
经过这种方式,只要将合约的可见性设置为public
(公共)或external
(外部),它们就能够与以太坊区块链上的任何其余合约进行交互。
若是一个合约须要和区块链上的其余的合约会话,则需先定义一个 interface (接口)。
先举一个简单的栗子。 假设在区块链上有这么一个合约:
contract LuckyNumber {
mapping(address => uint) numbers;
function setNum(uint _num) public {
numbers[msg.sender] = _num;
}
function getNum(address _myAddress) public view returns (uint) {
return numbers[_myAddress];
}
}
复制代码
这是个很简单的合约,能够用它存储本身的幸运号码,并将其与调用者的以太坊地址关联。 这样其余人就能够经过地址查找幸运号码了。
如今假设咱们有一个外部合约,使用 getNum 函数可读取其中的数据。
首先,咱们定义 LuckyNumber 合约的 interface :
contract NumberInterface {
function getNum(address _myAddress) public view returns (uint); } 复制代码
使用这个接口,合约就知道其余合约的函数是怎样的,应该如何调用,以及可期待什么类型的返回值。
下面是一个示例代码,会用到上边的知识点:
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;
// 建立一个叫作 zombieToOwner 的映射。其键是一个uint,值为 address。映射属性为public
mapping (uint => address) public zombieToOwner;
// 建立一个名为 ownerZombieCount 的映射,其中键是 address,值是 uint
mapping (address => uint) ownerZombieCount;
function _createZombie(string _name, uint _dna) private {
uint id = zombies.push(Zombie(_name, _dna)) - 1;
zombieToOwner[id] = msg.sender;
ownerZombieCount[msg.sender]++;
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 {
// 咱们使用了 require 来确保这个函数只有在每一个用户第一次调用它的时候执行,用以建立初始僵尸
require(ownerZombieCount[msg.sender] == 0);
uint randDna = _generateRandomDna(_name);
_createZombie(_name, randDna);
}
}
// CryptoKitties 合约提供了getKitty 函数,它返回全部的加密猫的数据,包括它的“基因”(僵尸游戏要用它生成新的僵尸)。
// 一个获取 kitty 的接口
contract KittyInterface {
// 在interface里定义了 getKitty 函数 在 returns 语句以后用分号
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 ); } //ZombieFeeding继承自 `ZombieFactory 合约 contract ZombieFeeding is ZombieFactory {
// CryptoKitties 合约的地址
address ckAddress = 0x06012c8cf97BEaD5deAe237070F9587f8E7A266d;
// 建立一个名为 kittyContract 的 KittyInterface,并用 ckAddress 为它初始化
KittyInterface kittyContract = KittyInterface(ckAddress);
function feedAndMultiply(uint _zombieId, uint _targetDna, string _species) public {
// 确保对本身僵尸的全部权
require(msg.sender == zombieToOwner[_zombieId]);
// 声明一个名为 myZombie 数据类型为Zombie的 storage 类型本地变量
Zombie storage myZombie = zombies[_zombieId];
_targetDna = _targetDna % dnaModulus;
uint newDna = (myZombie.dna + _targetDna) / 2;
// Add an if statement here
if (keccak256(_species) == keccak256("kitty")){
newDna = newDna - newDna%100 + 99;
}
_createZombie("NoName", newDna);
}
function feedOnKitty(uint _zombieId, uint _kittyId) public {
uint kittyDna;
// 多值返回,这里只须要最后一个值
(,,,,,,,,,kittyDna) = kittyContract.getKitty(_kittyId);
feedAndMultiply(_zombieId, kittyDna, "kitty");
}
}
复制代码
这段代码看起来内容有点多,能够拆分一下,把
ZombieFactory
代码提取到一个新的文件zombiefactory.sol
,如今就可使用 import 语句来导入另外一个文件的代码。
在 Solidity 中,当你有多个文件而且想把一个文件导入另外一个文件时,可使用 import 语句:
import "./someothercontract.sol";
contract newContract is SomeOtherContract {
}
复制代码
这样当咱们在合约(contract)目录下有一个名为 someothercontract.sol 的文件( ./ 就是同一目录的意思),它就会被编译器导入。
这一点和 go 相似,在同一目录下文件中的内容能够直接使用,而不用使用 xxx.name 的形式。
编译和部署 ZombieFeeding,就能够将这个合约部署到以太坊了。最终完成的这个合约继承自 ZombieFactory,所以它能够访问本身和父辈合约中的全部 public 方法。
下面是一个与ZombieFeeding合约进行交互的例子, 这个例子使用了 JavaScript 和 web3.js:
var abi = /* abi generated by the compiler */
var ZombieFeedingContract = web3.eth.contract(abi)
var contractAddress = /* our contract address on Ethereum after deploying */
var ZombieFeeding = ZombieFeedingContract.at(contractAddress)
// 假设咱们有咱们的僵尸ID和要攻击的猫咪ID
let zombieId = 1;
let kittyId = 1;
// 要拿到猫咪的DNA,咱们须要调用它的API。这些数据保存在它们的服务器上而不是区块链上。
// 若是一切都在区块链上,咱们就不用担忧它们的服务器挂了,或者它们修改了API,
// 或者由于不喜欢咱们的僵尸游戏而封杀了咱们
let apiUrl = "https://api.cryptokitties.co/kitties/" + kittyId
$.get(apiUrl, function(data) {
let imgUrl = data.image_url
// 一些显示图片的代码
})
// 当用户点击一只猫咪的时候:
$(".kittyImage").click(function(e) {
// 调用咱们合约的 `feedOnKitty` 函数
ZombieFeeding.feedOnKitty(zombieId, kittyId)
})
// 侦听来自咱们合约的新僵尸事件好来处理
ZombieFactory.NewZombie(function(error, result) {
if (error) return
// 这个函数用来显示僵尸:
generateZombie(result.zombieId, result.name, result.dna)
})
复制代码
最后,感谢女友支持和包容,比❤️
也能够在公号输入如下关键字获取历史文章:公号&小程序
| 设计模式
| 并发&协程