Solidity 的代码都包裹在合约里面. 一份合约就是以太应币应用的基本模块, 所有的变量和函数都属于一份合约, 它是你所有应用的起点.
最基本的合约 — 每次建立一个新的项目时的第一段代码(一份名为 HelloWorld 的空合约如下:):
pragma solidity ^0.4.19;
contract HelloWorld {
}
版本指令
pragma solidity ^0.4.19;
状态变量
状态变量是被永久地保存在合约中。也就是说它们被写入以太币区块链中,你可以想象成写入一个数据库。
contract Example {
// 这个无符号整数将会永久的被保存在区块链中
uint myUnsignedInteger = 100;
}
无符号整数: uint
uint 无符号数据类型, 指其值不能是负数,对于有符号的整数存在名为 int 的数据类型。
注: Solidity中, uint 实际上是 uint256代名词。你也可以定义位数少的uints — uint8, uint16,
uint32, 等…… 但一般来讲你愿意使用简单的 uint, 除非在某些特殊情况下。
数学运算
- 加法: x + y
- 减法: x - y,
- 乘法: x * y
- 除法: x / y
- 取模 / 求余: x % y (例如, 13 % 5 余 3, 因为13除以5,余3)
- 乘方: x ** y (x^y)
结构体
struct Person {
uint age;
string name;
}
结构体允许你生成一个更复杂的数据类型,它有多个属性。
创建新的结构体
Person[] public people;
// 创建一个新的Person:
Person satoshi = Person(172, "Satoshi");
// 将新创建的satoshi添加进people数组:
people.push(satoshi);
数组
Solidity 支持两种数组: 静态 数组和动态 数组
// 固定长度为2的静态数组:
uint[2] fixedArray;
// 固定长度为5的string类型的静态数组:
string[5] stringArray;
// 动态数组,长度不固定,可以动态添加元素:
uint[] dynamicArray;
// 结构体类型的数组
Person[] people;
记住:状态变量被永久保存在区块链中。所以在你的合约中创建动态数组来保存成结构的数据是非常有意义的。
公共数组
Person[] public people;
定义 public 数组, Solidity 会自动创建 getter 方法.
其它的合约可以从这个数组读取数据(但不能写入数据),所以这在合约中是一个有用的保存公共数据的模式。
定义函数
function eatHamburgers(string _name, uint _amount) {
}
eatHamburgers("vitalik", 100);
注:习惯上函数里的变量都是以(_)开头 (但不是硬性规定) 以区别全局变量。
私有 / 公共函数
Solidity 定义的函数的属性默认为公共。 这就意味着任何一方 (或其它合约) 都可以调用你合约里的函数。
显然,不是什么时候都需要这样,而且这样的合约易于受到攻击。 所以将自己的函数定义为私有是一个好的编程习惯,只有当你需要外部世界调用它时才将它设置为公共。
// 定义一个私有的函数, 私有函数的名字用(_)起始
uint[] numbers;
function _addToArray(uint _number) private {
numbers.push(_number);
}
只有我们合约中的其它函数才能够调用这个函数,给 numbers 数组添加新成员。
函数的返回值
string greeting = "What's up dog";
function sayHello() public returns (string) {
return greeting;
}
函数的修饰符: view / pure
view: 意味着它只能读取数据不能更改数据
pure: 不读取区块链上的数据
Keccak256生成伪随机数
keccak256("aaaab");
//6e91ec6b618bb462a4a6ee5aa2cb0e9cf30f7a052bb467b0ba58b8748c00d2e5
keccak256("aaaac");
//b1f078126895a1424524de5321b339ab00408010b7cf0e6ed451514981e58aa9
注: 在区块链中安全地产生一个随机数是一个很难的问题
类型转换
uint8 a = 5;
uint b = 6;
// 将会抛出错误,因为 a * b 返回 uint, 而不是 uint8:
uint8 c = a * b;
// 我们需要将 b 转换为 uint8:
uint8 c = a * uint8(b);
事件
事件 是合约和区块链通讯的一种机制。你的前端应用“监听”某些事件,并做出反应。
// 这里建立事件
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 实现如下:
web3.eth.contract(abi).at('0x78e97bcc5b5dd9ed228fed7a4887c0d7287344a9').IntegersAdded(function(error, result) {
// 干些事
}
Addresses (地址)
以太坊区块链由 account (账户)组成,你可以把它想象成银行账户。一个帐户的余额是 以太币 eth,你可以和其他帐户之间支付和接受以太币,就像你的银行帐户可以电汇资金到其他银行帐户一样。
每个帐户都有一个“地址”,你可以把它想象成银行账号。这是账户唯一的标识符,它看起来长这样:
0x0cE446255506E92DF41614C46F1d6df9Cc969183
地址属于特定用户(或智能合约)的。
映射(Mapping)
映射 是另一种在 Solidity 中存储有组织数据的方法。
//对于金融应用程序,将用户的余额保存在一个 uint类型的变量中:
mapping (address => uint) public accountBalance;
//或者可以用来通过userId 存储/查找的用户名
mapping (uint => string) userIdToName;
映射本质上是存储和查找数据所用的键-值对。在第一个例子中,键是一个 address,值是一个
uint,在第二个例子中,键是一个uint,值是一个 string。
Msg.sender
在 Solidity 中,有一些全局变量可以被所有函数调用。 其中一个就是 msg.sender,它指的是当前调用者(或智能合约)的 address。
注意:在 Solidity 中,功能执行始终需要从外部调用者开始。 一个合约只会在区块链上什么也不做,除非有人调用其中的函数。所以
msg.sender总是存在的。
以下是使用 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 很安全,因为它具有以太坊区块链的安全保障 —— 除非窃取与以太坊地址相关联的私钥,否则是没有办法修改其他人的数据的。
Require
require使得函数在执行过程中,当不满足某些条件时抛出错误,并停止执行。
function sayHiToVitalik(string _name) public returns (string) {
// 比较 _name 是否等于 "Vitalik". 如果不成立,抛出异常并终止程序
// (敲黑板: Solidity 并不支持原生的字符串比较, 我们只能通过比较
// 两字符串的 keccak256 哈希值来进行判断)
require(keccak256(_name) == keccak256("Vitalik"));
// 如果返回 true, 运行如下语句
return "Hi!";
}
继承(Inheritance)
当代码过于冗长的时候,最好将代码和逻辑分拆到多个不同的合约中,以便于管理。合约继承用is关键字。
contract Doge {
function catchphrase() public returns (string) {
return "So Wow CryptoDoge";
}
}
contract BabyDoge is Doge {
function anotherCatchphrase() public returns (string) {
return "Such Moon BabyDoge";
}
}
由于 BabyDoge 是从 Doge 那里 inherits (继承)过来的。 这意味着当你编译和部署了 BabyDoge,它将可以访问 catchphrase() 和 anotherCatchphrase()和其他我们在 Doge 中定义的其他公共函数。
引入(Import)
上面继承的例子中,如果分两个文件,就需要引入。
import "./someothercontract.sol";
contract newContract is SomeOtherContract {
}
Storage与Memory
在 Solidity 中,有两个地方可以存储变量 —— storage 或 memory
Storage 变量是指永久存储在区块链中的变量。 Memory 变量则是临时的,当外部函数对某合约调用完成时,内存型变量即被移除。
你可以把它想象成存储在你电脑的硬盘或是RAM中数据的关系。
大多数时候你都用不到这些关键字,默认情况下 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;
// ...如果你想把副本的改动保存回区块链存储
}
}
函数可见性: internal 和 external
internal 和 private 类似,不过, 如果某个合约继承自其父合约,这个合约即可以访问父合约中定义的“内部”函数。
external 与 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();
}
}
与其他合约的交互
如果我们的合约需要和区块链上的其他的合约会话,则需先定义一个 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);
}
在我们的 app 代码中使用这个接口,合约就知道其他合约的函数是怎样的,应该如何调用,以及可期待什么类型的返回值。
使用接口
上面的接口,我们可以在合约中这样使用:
contract MyContract {
address NumberInterfaceAddress = 0xab38...;
// ^ 这是FavoriteNumber合约在以太坊上的地址
NumberInterface numberContract = NumberInterface(NumberInterfaceAddress);
// 现在变量 `numberContract` 指向另一个合约对象
function someFunction() public {
// 现在我们可以调用在那个合约中声明的 `getNum`函数:
uint num = numberContract.getNum(msg.sender);
// ...在这儿使用 `num`变量做些什么
}
}
通过这种方式,只要将您合约的可见性设置为public(公共)或external(外部),它们就可以与以太坊区块链上的任何其他合约进行交互。
处理多返回值
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();
}
if 语句
function eatBLT(string sandwich) public {
// 看清楚了,当我们比较字符串的时候,需要比较他们的 keccak256 哈希码
if (keccak256(sandwich) == keccak256("BLT")) {
eat();
}
}
payable 修饰符
payable 方法是让 Solidity 和以太坊变得如此酷的一部分 —— 它们是一种可以接收以太的特殊函数。
在以太坊中, 因为钱 (以太), 数据 (事务负载), 以及合约代码本身都存在于以太坊。你可以在同时调用函数 并付钱给另外一个合约。
contract OnlineStore {
function buySomething() external payable {
// 检查以确定0.001以太发送出去来运行函数:
require(msg.value == 0.001 ether);
// 如果为真,一些用来向函数调用者发送数字内容的逻辑
transferThing(msg.sender);
}
}
在这里,msg.value 是一种可以查看向合约发送了多少以太的方法,另外 ether 是一个內建单元。
这里发生的事是,一些人会从 web3.js 调用这个函数 (从DApp的前端), 像这样 :
// 假设 OnlineStore
在以太坊上指向你的合约:
OnlineStore.buySomething().send(from: web3.eth.defaultAccount, value: web3.utils.toWei(0.001))
注意这个 value 字段, JavaScript
调用来指定发送多少(0.001)以太。如果把事务想象成一个信封,你发送到函数的参数就是信的内容。 添加一个 value 很像在信封里面放钱
—— 信件内容和钱同时发送给了接收者。
提现
在你发送以太之后,它将被存储进以合约的以太坊账户中, 并冻结在哪里 —— 除非你添加一个函数来从合约中把以太提现。
你可以写一个函数来从合约中提现以太,类似这样:
contract GetPaid is Ownable {
function withdraw() external onlyOwner {
owner.transfer(this.balance);
}
}
你可以通过 transfer 函数向一个地址发送以太, 然后 this.balance 将返回当前合约存储了多少以太。 所以如果100个用户每人向我们支付1以太, this.balance 将是100以太。
你可以通过 transfer 向任何以太坊地址付钱。 比如,你可以有一个函数在 msg.sender 超额付款的时候给他们退钱:
uint itemFee = 0.001 ether;
msg.sender.transfer(msg.value - itemFee);