1、编写智能合约 1 Solidity源文件 Solidity源文件使用的扩展名为.sol。在源文件中,可以使用pragma Solidity说明编写代码时用的编译器版本。例如: pragma Solidity ^0.4.2; 现在,源文件不会低于0.4.2的编译器版本,也不会用高于0.5.0的编译器版本进行编译(第二个条件使用^添加)。 2 智能合约的结构 合约就像一个类(class), 其中包含状态变量(state variable)、函数(function)、函数修改器(function modifier)、事件(event)、结构(structure)和枚举(enum)。合约
2、还支持继承,通过在编译时备份代码来实现。最后,合约还支持多态。下面来看一个智能合约的例子: contract Sample { //state variable uint256 data; address owner; event logData(uint256 dataToLog); modifier onlyOwner(){ if (msg.sender != owner) throw; _; } function Sample(uint256 initData, address initOwner){ data = initData;
3、owner = initOwner; } function getData() returns (uint256 returnedData){ return data; } function setData(uint256 newData) onlyOwner{ logData(newData); data = newData; } } 上述代码的工作原理如下: 1) 使用contract关键字声明一个合约。 2) 声明两个状态变量data和owner。data包含一些数据,owner包含所有者的以太坊钱包地址,即部署合约者的以太坊地址。 3) 定义一个事件
4、event)。事件用于通知客户端。一旦data发生变化,将触发这个事件。所有事件都保存在区块链中。 4) 定义一个函数修改器(function modifier)。修改器用于在执行一个函数之前自动检测条件。这里,修改器检测合约所有者是否在调用函数。如果不是,就抛出异常。 5) 得到合约构造函数(constructor)。在部署合约时,调用构造函数。构造函数用于初始化状态变量。 6) 定义两个方法。第一个方法用于得到data状态变量的值,第二个方法用于改变data的值。 在更深入地学习智能合约的函数之前,我们先来学习一些与Solidity有关的其他知识,然后再回答合约。 3 数据位置
5、 截止目前,我们学过的所有 编程语言可能都把变量存储在内存中。但是在Solidity中,根据情况的不同,变量可能不存储在内存和文件系统中。 根据情况的不同,数据总有一个默认位置。但是对于复杂数据类型,例如字符串(string)、数组(array)和结构类型(struct),可以用向类型添加storage或者memory进行重写。函数参数(包括返回参数)默认用memory,本地变量默认用storage.显然,对于状态变量来说,位置强制用storage。 数据位置很重要,因为它们会改变分配的行为: l storage变量和memory变量之间的分配总是创建一个独立的备份。但如果分配是从me
6、mory存储的一种复杂类型到另一种复杂类型,则不创建备份。 l 到一个状态变量的分配(即使是来自其他状态变量)总是创建一个独立的备份。 l 不能把memory中国存储的复杂类型分配给本地存储变量。 l 在分配状态变量和本地存储变量的情况下,本地存储变量指向状态变量,也就是说,本地存储变量变为指针。 4 什么是不同的数据类型 Solidity是一种静态类型语言,变量存储的数据类型需要预先定义。所有变量默认都是0。在Solidity中,变量是有函数作用范围的,在函数中任何地方声明的变量将对整个函数存在适用范围,无论它是在哪里声明的。 现在让我们看看Solidity提供的不同数据类型:
7、 l 最简单的数据类型是布尔值,可以是true或者false。 l uint8,uint16,uint24,...,uint256分别用于存储无符号的8位,16位,24位,...246位整数。同理,int8,int16,..., int256分别用于存储8位,16位,24位,..,256位整数。uint和int是uint256和int256的别名。类似于uint和int,ufixed和fixed代码分数。ufixed0x8, ufixed0x16,..., ufixed0x256分别用于存储未签名的8位,16位,24位,...,256位分数。同理,fixed0x8,fixed0x16,...
8、 fixed0x256分别用于存储8位,16位,24位,...,256位分数。如果一个数字超过256位,则使用256位数据类型存储该数字的近似值。 l address可以用于存储最大20字节的值(十六进制表示)。它用于存储以太坊地址。address类型有两个属性:balance和send。balance用于检测地址余额,send用于向地址发送以太币。send方法拿出需要转账的那些数量的wei,并根据转账是否成功返回ture或者false。wei从调用send方法的合约中扣除。用户可以在Solidity中使用0x前缀给变量分配一个十六进制的数值。 4.1 数组类型 Solidity支持g
9、eneric和byte两种数组类型。它们支持固定长度和动态长度两种数组,也支持多维数组。 bytes1,bytes2,bytes3,..., bytes32是字节数组的类型。byte是bytes1的别名。 下面给出了generic数组语法的一个示例: constract sample { int[] myArray = [0,0]; function sample(uint index, int value){ myArray[index]=value; int[] myArray2 = myArray; uint24[3] memory myArray3 =
10、[1,3,9999]; uint8[2] myArray4 = [1,2]; } } 关于数组的重要内容如下: l 数组还有length属性,用于发现数组的长度。用户还可以给length属性分配一个值,以改变数组的大小,但不可以在内存中改变数组的大小,也不同可以改变非动态数组的大小。 l 如果想访问动态数组的微设置索引(unset index),会抛出异常。 array、structs和map都不可以作为函数参数,也不可以用作函数返回值。 4.2 字符串类型 在Solidity中,有两种方法创建字符串:使用bytes和string。bytes用于创建原始字符串,而stri
11、ng用于创建UTF-8字符串。字符串长度总是动态的。下面给出了字符串语法的一个示例: constract sample { string myString = “”; bytes myRawString; function sample(string initString, bytes rawStringInit){ myString = initString; string myString2 = myString; string memory myString3 = “ABCDE”; myString3 = “XYZ”; myRawString
12、 rawStringInit; myRawString.length++; string myString4 = “Example”; // throws exception while compiling string myString5 = initString; // throws exception while compiling } } 4.3 结构类型 Solidity还支持结构类型(struct)。下面给出了struct语法的一个示例: contract sample { struct myStruct { bool myBool;
13、 string myString; } myStruct s1; myStruct s2 = myStruct(true,””); function sample(bool initBool, string initString){ s1 = myStruct(initBool, initString); myStruct memory s3 = myStruct(initBool, initString); } } 函数参数不可以是结构类型,且函数不可以返回结构类型。 4.4 枚举类型 Solidity还支持枚举类型(enum)。下面给出了enum语法的一个示例:
14、 contract sample { enum OS {Windows, Linux, OSX, UNIX} OS choice; function sample(OS chosen){ choice = chosen; } function setLinuxOS(){ choice = OS.L inux; } function getChoice() returns (OS chosenOS){ return choice; } } 4.5 mapping类型 mapping数据类型是一个哈希表。mapping类型只可以存在与stor
15、age中,不存在与memory中,因此它们是作为状态变量声明的。可以认为mapping类型包含key/value对,不是时间存储key,而是存储key和keccak256哈希,用于查询value。mapping类型没有长度。mapping不可以被分配给另一个mapping。 下面给出一个创建和使用mapping的示例: contract sample { mapping(int => string) myMap; function sample(int key, string value){ myMap[key] = value; mapping(int=>string
16、) myMap2 = myMap; } } 如果想访问mapping中不存在的key,返回的value均为0。 4.6 delete操作符 delete操作符可以用于任何变量,将其设置成默认值。默认值均为0. 如果对动态数组使用delete操作符,则删除所有元素,其长度变为0。如果对静态数组使用delete操作符, 则重置所有索引。还可以通过对特定索位置使用delete来重置索引。 如果读map类型使用delete操作符,什么都不会发生。但是如果对map类型的一个键使用delete操作符,则会删除与该键相关的值。 下面给出了delete操作符的一个示例: contract
17、 sample { struct Struct { mapping (int => int) myMap; int myNumber; } int[] myArray; Struct myStruct; function sample(int key, int value, int number, int[] array){ myStruct = Struct(number); myStruct.myMap[key] = value; myArray = array; } function reset(){ delete myArray; de
18、lete myStruct; } function deleteKey(int key){ delete myStruct.myMap[key]; } } 4.7 基本类型之间的转换 除了数组类型、字符串类型、结构类型、枚举类型和map类型外,其他类型均称为基本类型。 如果把一个操作符应用与不同的类型,编译器将尝试把一个操作数隐式转换微另一种类型。通常来说,如果没有语义信息丢失,值和类型之间可以进行隐式转换:uint8可转换微uint16,uint128可转换微int256,但int8不可转换微uint256(因为uint256不能存储,例如-1)。此外,无符号整数可以转换成
19、同等大小或更大的字节,但是反之则不然。任何可以转换成uint160的类型都可以转换成地址。 Solidity也支持显示转换,所以如果编译器不允许在两种两种数据类型之间隐式转换,则可以进行显示转换。建议尽量避免显式转换,因为可能返回难以预料的结果。 来看一个例子: uint32 a = 0x12345678; uint16 b = uint16(a); //b will be 0x5678 now 这里是将uint32类型显示转换微uint16,也就是说,把较大类型转换为较小类型,因此高位被砍掉了。 4.8 使用var Solidity提供了用于声明变量的var关键字。变量类型
20、根据分配给它的第一个值来动态确定。一旦分配了值,类型剧固定了,所以如果给它知道另一类型,将引起类型转换。示例如下: int256 x = 12; var y = x; uint256 z = 9; y = z; 在定义数组array和map时不能使用var。var也不能用于定义函数参数和状态变量。 5 控制结构 solidity支持if/else/while/for/break/continue/return ? :等控制结构。下面给出了控制结构的一个示例: constract sample { int a = 12; int[] b; function s
21、ample(){
if(a == 12){
}esle if(a==34)
{
}
else
{
}
var temp = 10;
while(temp<20)
{
if(temp == 17)
{
break;
}
else
{
continue;
}
temp++;
}
for(var iii = 0;iii 22、1
{
int a;
function assign(int b)
{
a =b;
}
}
contract sample2 {
function sample2(){
sample1 s = new sample1();
s.assign(12);
}
}
7 异常
在一些情况下,异常会被自动抛出。也可以使用throw手动抛出异常。抛出异常会停止回滚目前执行的调用(也就是说,撤销对状态和余额的所有改变)。捕获异常是不可能的:
constract sample
{
function myFunction()
{
throw;
}
23、
}
8 外部函数调用
在Solidity中,有两种函数调用:内部函数调用和外部函数调用。内部函数调用是指一个函数在同一个合约中调用另一个函数。示例如下:
contract sample1
{
int a;
function sample1(int b) payable
{
a =b;
}
function assign(int c)
{
a =c ;
}
function makePayment(int d) payable{
a =d;
}
}
contract sample2 {
sample1 s = (new sample1).v 24、alue(12)(23);
s.makePayment(22);
s.makePayment.value(45)(12);
s.makePayment.value(4).gas(900)(12);
this.hello();
sample1 s2 = sample1(addressOfContract);
s2.makePayment(112);
}
使用this关键字进行的调用称为外部调用。在函数中,this关键字代表当前合约实例。
9 合约功能
现在是时候深入学习合约了。我们将看看一些新的功能,还将深入学习已经见过的一些功能。
9.1 可见性
函数或者状态 25、变量的可见性定义了谁可以看到它。函数和状态变量的四种可见性:external/public/internal和private
函数可见性默认为public,状态变量可见性默认为internal。各可见性函数的含义如下:
l external。外部函数只能有其他合约调用,或者通过交易调用。外部函数f不能被内部函数调用,也就是说,f()没有用,但this.f()有用。不能把external可见性应用到状态变量。
l public。公共函数和状态变量可以用所有可行办法访问。编译器生成的存取器(accessor)函数都是公共状态变量。用户不能创建自己的存取器。事实上,它只生成getters,而不 26、生成setters。
l internal。内部函数和状态变量只可以内部访问,也就是说,从当前合约内核继承它的合约访问。不可以使用this访问它。
l private。私有函数和状态变量类似于内部函数,但是继承合约部可以访问它们。
下面给出了可见性和存取器(accessor)的一个示例:
contract sample1
{
int public b = 78;
int internal c = 90;
function sample1()
{
this.a();
a();
b = 21;
this.b;
this.b();
this.b(8);
t 27、his.c();
c = 9;
}
function a() external
{
}
}
contract sample2
{
int internal d = 9;
int private e = 90;
}
contract sample3 is sample2
{
sample1 s;
function sample3()
{
s = new sample1();
s.a();
var f = s.b;
s.b = 18;
s.c();
d=8;
e = 7;
}
}
9.2 函数修改器
我们之前看到了函数修改 28、器(function modifier)的概念,还编写了一个基本的函数修改器,现在来深入学习修改器。
修改器由子合约(child contract)继承,且子合约可以对其重写。可以通过用空格分隔的列表指定修改器将多个修改器应用到一个函数,并将多个修改器按顺序估值;还可以像修改器传送实参。
在修改器中,无论下一个修改器或者函数体二者哪个先到达,会白插入到”_”;出现的地方。
让我们来看一个函数修改器的复杂代码例子:
contract sample
{
int a = 90;
modifier myModifier1(int b){
int c =b;
_;
c 29、a;
a =8;
}
modifier myModifier2{
int c = a;
_;
}
modifier myModifier3{
a = 96;
return ;
_;
a=99;
}
modifier myModifier4 {
int c = a;
_;
}
function myFunction() myModifier1(a) myModifier2 myModifier3 return (int d)
{
a = 1;
return a;
}
}
myFunction()的执行代码如下:
int 30、c =b;
int c = a;
a = 96;
return;
int c = a;
a = 1;
return a;
a = 99;
c =a;
a = 8;
在上述代码中调用myFunction()方法时,将返回0.但是之后返回状态变量a时,将得到8.
修改器或者函数体中的return(返回)立即离开整个函数,返回值被分配成它需要成为的任何变量。
就函数来说,return之后的代码在调用者的代码完成运行后再执行,就修改器来说,上述修改器中的_;之后的代码在调用者的代码完成运行后在执行。在上面的例子中,第5、6和 31、7行从未执行过。在第4行之后,执行从第8-10行开始。
修改器中的return不可以有相关值,它总是返回全0。
9.3 回退函数
一个合约可以有唯一的未命名函数,称为回退函数(fallback function)。该函数不能有实参,不能返回任何值。如果其他函数都不能匹配给定的函数标识符,就在合约上执行回退函数。
当合约不用任何函数调用及接受以太币(即交易发生以太币给合约却不调用任何方法)时,也执行该函数。在此情况下,用于函数调用的gas通常很少(准确地说是2300gas),所以使回退函数尽可能便宜很重要。
接收以太币但是却不定义回退函数的合约会抛出异常,把以太币发送回去,所以如果你想 32、让你的合约接收以太币,就必须要实现回退函数。
下面给出了回退函数的一个示例:
construct sample
{
function() payable
{
//keep a note of how much Ether has been sent by whom
}
}
9.4 继承
Slidity通过代码备份(包括多态)支持多重继承(multiple inheritance)。即使一个合约继承自其它多个合约,在区块链上值创建一个合约,来自父合约(parent contract)的代码总是被复制到最终合约里。示例如下:
contract sample1
{
f 33、unction a(){}
function b(){}
}
contract sample2 is sample1
{
function b(){}
}
contract sample3
{
function sample3(int b)
{
}
}
contract sample4 is sample1, sample2
{
function a(){}
function c(){
a();
sample1.a();
b();
}
}
contract sample5 is sample3(122)
{
}
34、
1. super关键字
super关键字用于引用最终继承链中的下一个合约,示例如下:
constract sample1
{}
constract sample2{}
constract sample3 is sample2{}
constract sample4 is sample2{}
contract sample5 is sample4{
function myFunc(){}
}
contract sample6 is sample1, sample2, sample3,sample5
{
function myFunc()
{
super.m 35、yFunc();
}
}
其中,引用sample6合约的最终继承链式sample6,sample5,sample4,sample2,sample3和sample1。继承链始于衍生最充分的合约,终于衍生最不充分的合约。
2. 抽象合约
仅包含函数原型而不包含函数实现的合约称为抽象合约(abstract contract)。这些合约不能被编译(即使包含实现函数和非实现函数)。如果一个合约继承自抽象合约且不重写并实现所有非实现函数,那么它自己也是抽象的。
抽象合约仅在创建编译器已知的接口时提供。这在引用已部署的合约和调用器函数时很有用的。示例如下:
contract sample1
36、{
function a() returns (int b);
}
constract sample2
{
function myFunc()
{
sample1 s = sample1(0x...);
s.a();
}
}
10 库
库类似于合约,但其目的是在一个特定地址只部署一次,且器代码有不同合约反复使用。这意味着如果调用库函数,其代码在调用合约(calling contract)中执行,也就是说,this指向调用合约,特别是来自调用合约的storage可以被访问。由于库是源代码中独立的一部分,它只能访问调用合约的状态变量,因此这些变量是显示的(否则无 37、法命名这些变量)。
库没有状态变量--它们不支持继承,也不能接收以太币。库可以包含结构类型(struct)和枚举类型(enum)。
一旦在区块链中部署Solidity库,任何知道器地址和源代码(纸质的原型或者知道完整实现)的人都可以使用它。Solidity编译器需要有源代码,这能确保所访问的方法在库中真实存在。示例如下:
library math
{
function addInt(int a, int b) returns (int c)
{
return a + b;
}
}
contract sample
{
function data() returns 38、int d)
{
return math.addInt(1,2);
}
}
不能在合约源代码中添加库地址,而是需要在编译时想编译器提供库地址。
库有许多使用示例。两个主要的示例如下:
l 如果有许多合约,它们有一些共同代码,则可以把共同代码部署成一个库。这样节省gas,因为gas也依赖于合约的规模。因此,可以把库想象成使用其合约的基础合约。使用基础合约(而非库)切分共同代码不会节省gas,因为在solidity中,继承通过复制代码工作。由于库被当做基础合约,库里面带有内部可视化的函数被复制给使用它的合约;否则,库里面带有内部可视性的函数不能被使用这个库的合约调用,因为这需要外 39、部调用,而带有内部可是性的函数不能通过外部调用被调用。此外,库里的structs和enums被复制给使用这个库的合约。
l 库可拥有被数据类型添加成员函数。
如果一个库只包含内部函数和/或structs/enums,则不需要部署库,因为库里面的所有内容都被复制给使用它的合约。
using for
using A for B这条指令可用于连接库函数(从库A到任意类型B)。这些函数将被调用的对象作为它们的第一个参数接收。
using A for *的结果表示来自库A的函数被连接到所有类型。示例如下:
library math
{
struct myStruct1 {
in 40、t a;
}
struct myStruct2{
int a;
}
function addInt(myStruct1 storage s, int b) returns (int c)
{
return s.a + b;
}
function subInt(myStruct2 storage s, int b) returns (int c
{
return s.a + b;
}
}
contract sample
{
using math for *;
math.myStruct1 s1;
math.myStruct2 s2;
funct 41、ion sample()
{
s1 = math.myStruct1(9);
s2 = math.myStruct2(9);
s1.addInt(2);
s2.addInt(1);
}
}
11 返回多值
Solidity允许函数返回多值(multiple values), 示例如下:
contract sample
{
function a() returns( int a, string c)
{
return (1,”ss”);
}
function b()
{
int A;
string memory B;
(A, 42、B) = a();
(A,) = a();
(,B) = a();
}
}
12 导入其他Solidity源文件
Solidity允许一个源文件导入其他源文件,示例如下:
import “filename”;
import * as symbolName from “filename”;
import {symbol1 as alias, symbol2} from “filename”;
import “filename” as symbolName;
13 全局可用变量
有些特殊变量和函数永远在与全局中。
13.1 区块和交易属性
区块和交易属性有如下几项
43、
l block.blockhash (uint blockNumber) returns (bytes32)。给定区块的哈希值,只支持最近256个区块。
l block.coinbase(address)。当前区块矿工的地址。
l block.difficulty(uint)。当前区块的难度值。
l block.gaslimit(uint)。当前区块的gas上限。它定义了整个区块中的所有交易一起最大可以消耗多少gas。其目的是使区块的传播和处理时间保持在较低水平,这样才能够有足够去中心化的网络。矿工有权利将当前区块的gas上限设置为善给一个区块的gas上限-0.0.975%(1/1,0 44、24)以内的数值,所以gas上限的结果应当是矿工偏好的中间值。
l block.number (uint).当前区块的序号。
l block.timestamp(uint)。当前区块的时间戳。
l .msg.data(bytes)。 完整的调用数据里存储的函数及其实参。
l msg.gas(uint) 当前剩余的gas
l msg.sender(address)。当前调用发起人的地址
l msg.sig(bytes4)。调用数据的前四个字节(函数标识符)
l msg.value(uint)。这个消息所附带的货币链,单位为wei.
l now(uit),当前区块的时间戳,等同于b 45、lock.timestamp.
l tx.gasprice(uint)。交易的gas价格。
l tx.origin(address).交易的发起人(完整的调用链)。
13.2 地址类型相关
地址类型相关变量如下:
l






