有好几种语言可以用于编写以太坊智能合约,不过Solidity是最热门的语言。在本章中,我们将首先学习Solidity编程语言。然后创建一个DApp,用于证明在特定时间的存在、真实性和所有权,即证明一个文件在一个特定时间属于一个特定所有者。
要点:
- Solidity源文件的布局
- 理解Solidity的数据类型
- 合约的特殊变量和函数
- 控制结构
- 合约的结构和功能
- 编译和部署合约
Solidity源文件
Solidity源文件使用的扩展名为.sol。这里面我们使用的是0.4.2版本。
智能合约的结构
合约就像一个类(class),其中包含:
- 状态变量(state variable)
- 函数(function)
- 函数修改器(function modifier)
- 事件(event)
- 结构(structure)
- 枚举(enum)
同时,合约还支持继承与多态。
示例:
contract Sample
{
//状态变量
uint256 data;
address owner;
//定义事件
event logData(uint256 dataToLog);
//函数修改器
modifier onlyOwner() {
if(msg.sender!=owner) throw;
}
//构造器,名字与合约名一致
function Sample(uint256 initData,address initOwner) {
data = initData;
owner = initOwner;
}
//函数
function getData() returns (uint256 returnedData) {
return data;
}
function setData() returns (uint256 newData) onlyOwner {
logData(newData);
data = newData;
}
}
代码注释:
-
contract 关键字:用于声明一个合约
-
data和owner:是两个状态变量。data包含一些数据,owner包含所有者的以太坊钱包地址,即部署合约者的以太坊地址
-
event logData 定义事件logData,用于通知客户端:一旦data发生变化,将触发这个事件。所有事件都保存在区块链中。
-
函数修改器:onlyOwner。修改器用于在执行一个函数之前自动检测文件。这里的修改器用于检测合约所有者是否在调用函数。如果没有,则会抛出异常。
-
合约函数构造器constructor:在部署合约时,构造器用于初始化状态变量。
-
function,getData()用于得到data状态变量的值,setData()用于改变data的值。
数据位置(较难理解)
通常,变量会存储在内存中。但是,在Solidity中,会根据不同的情况,变量可能会不存储在内存和文件系统中。
通常,在Solidity中,数据有一个默认位置。通常,存储有一个storage位置和一个memory位置,即本地存储与内存存储。
函数参数,包括其返还参数,默认用memory,本地变量默认用storage,例如状态变量,其数据位置强制使用storage。
注意:
- 不能把memory中存储的复杂类型分配给storage;
###什么是不同的数据类型
首先明白3点;
- Solidity是一种静态类型语言,变量存储的数据类型需要预先定义。
- 所有变量默认值都是0。
- 在Solidity中,变量是有函数作用范围的,也就是说,在函数中任何地方声明的变量将对整个函数存在适用范围。
那么Solidity提供了哪些数据类型——》
基本类型
除了数组类型、字符串类型、结构类型、枚举类型和map类型外,
其他类型均称为基本类型。
-
无符号型:例如uint8,uint16,uint24,…,uint256分别用于存储无符号的8位,16
位,24位,…,256位整数 - 有符号型:例如,int8,int16,…,int256分别用于存储8位,16位,24位,…,256位整数
-
address类型:用于存储以太坊地址,用16进制表示。address类型有两个属性:balance和send。balance用于检测地址余额,send用于向地址发送以太币。send方法拿出需要转账那
些数量的wei,并根据转账是否成功返回true或者false。
注意:
-
uint和int是uint256和int256的别名。
-
如果一个数字超过256位,则使用256位数据类型存储该数字的近似值。
-
数组:Solidity支持generic和byte两种数组类型。
数组有length属性,用于发现数组的长度。
注意:不可以在内存中改变数组大小,也不可以改变非动态数组大小。
字符串类型
有两种方法创建字符串:使用bytes和string。
bytes用于创建原始字符串,而string用于创建UTF-8字符串
示例:
contract sample {
string myString = "";// string
bytes myRawString;
function sample(string initString,bytes rawStringInit) {
myString = initString;
string storage myString2 = myString;
string memory myString3 = "ABCDE";
myString3 = "imaginecode";
myRawString = rawStringInit;
myRawString.length++;
}
}
结构类型struct
示例
contract sample {
struct myStruct {
bool myBool;
string myString;
}
myStruct s1;
myStruct s2 = myStruct{true,""};
function sample(bool initBool,string initString){
s1 = myStruct(initBool,initString);
myStruct memory s3 = myStruct(initBool,initString);
}
}
注意:函数参数不可以是结构类型,且函数不可以返回结构类型。
枚举类型 enum
示例
contract sample {
enum OS {OSX, Linux,Unix,windows }
OS choice;
function sample(OS chosen) {
choice = chosen;
}
function setLinux() {
choice = OS.Linux;
}
function getChoice return (OS chosenOS) {
return choice;
}
}
mapping 类型
-
mapping类型只可以存在于storage中,不存在于memory中,因此它们是作为状态变量声明的。
-
mapping类型包含key/value对,不是实际存储key,而是存储key的keccak256哈希,用于查询value。
-
mapping不可以被分配给另一个mapping。
constract sample {
mapping (int => string) myMap;
function sample(){
myMap[key] = value;
mapping (int => string) myMap2 = myMap;
}
}
注意:如果想访问mapping中不存在的key,返回的value为0。
delete 操作符
可用于操作任何类型的变量。
-
对动态数组使用delete操作符,则删除所有元素,其长度变为0。
-
对静态数组使用delete操作符,则重置所有索引
-
对map类型使用delete操作符,什么都不会发生,但是,对map类型的一个键使用delete操作符,则会删除与该键相关的值
示例
contract 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 = Struct(number);
myStruct.myMap[key] = value;//对某个键赋值
myArray = array;
}
function reset() {
delete myArray;//myArray数组长度为0
delete myStruct;//myNumber为0,myMap不变
}
function deleteKey(int key) {
delete myStruct.myMap[key];//删除myMap中的某个键的值
}
}
基本类型之间的转换
- 隐式转换:常用。通常来说,如果没有语义信息丢失,值和类型之间可以进行隐式转换:uint8可转换为uint16,int128可转换为int256,但是int8不可转换为uint256(因为uint256不能存储,例如-1)
- Solidity也支持显式转换,如果编译器不允许在两种数据类型之间隐式转换,则可以进行显式转换。建议尽量避免显式转换,因为可能返回难以预料的结果。
示例:
uint32 a = 0x12345678;
uint16 b = uint16(a); // b = 0x5678,将uint32类型显式转换为uint16,也就是说,把较大类型转换为较小类型,因此高位被截掉了
var
使用关键字var声明的变量,其变量类型根据分配给它的第一个值来动态确定。一旦分配了值,类型就固定了,所以如果给它指定另一个类型,将引起类型转换。
int256 x= 12;
var y = x;//此时y的类型是int256
uint256 z = 9;
y = z;//此时,报出异常,因为uint256不能转换为int256类型
但要注意的是:
- 在定义数组array和map时不能使用var。var也不能用于定义函数参数和状态变量
控制结构
- if-else
- while
- for
- break\continue
- return
- ?:
- 等等
//结构上和其他语法没有什么差异
contract sample {
int a = 12;
int[] b;
function sample() {
if(a == 12) {}
else if(a==34){}
else {}
var temp = 10;
while(temp<20)
{
if(temp==17){break;}
else {continue;}
}
temp++;
}
for(var m=0;m<b.length;m++){
}
}
用new 操作符创建合约
一个合约可以使用new关键字来创建一个新合约。
例如:
contract sample1 {
int a;
function assign(int b){
a = b;
}
}
contract sample2 {
function sample2(){
sample1 s = new sample1(); //注意写法
s.assign(12);
}
}
异常
异常的抛出分为自动和手动。
若你想手动抛出异常,可以使用throw手动抛出。
注意,异常抛出后,会撤销对状态和余额的所有改变。
contract sample {
function myFunction () {
throw;
}
}
函数调用
- 内部函数调用:一个函数在同一个合约中调用另一个函数
- 外部函数调用:一个函数调用另一个合约的函数。
外部函数调用–this关键字
合约sample1
contract sample1 {
int a;
function sample1(int b) payable {
a = b;
}
function assign(int c){
a = c;
}
function makePayment(int d) payable {
a = d;
}
}
合约sample2
contract sample2 {
function hello() {}
function sample2(address addressOfContract){
sample1 s = (new sample1).value(12)(23);
s.makePayment(22);
s.makePayment.value(45)(12);
s.makePayment.value(4).gas(900)(12);
this.hello(); //利用this调用外部合约函数
sample1 s2 = sample1(addressOfContract);
s2.makePayment(112);
}
}
注意:使用this关键字进行的调用称为外部调用。在函数中,this关键字代表当前合约实例
合约功能——深入理解合约
可见性
可见性定义了谁可以看到它,
函数和状态变量有四种可见性:external、public、internal和private
-
函数可见性,默认为 public
-
状态变量可见性,默认为 internal
-
external:外部函数只能由其他合约调用,或者通过交易调用——this.f()
-
public:公共函数和状态变量可以用所有可行办法访问
-
internal:内部函数和状态变量只可以内部访问,即从当前合约内和继承它的合约访问。不可以使用this访问它
-
private:私有函数和状态变量类似于内部函数,但是继承合约不可以访问它们
示例
contract sample1 {
int public b = 78;
int internal c = 90;
function sample1() {
this.a();//外部访问
b = 21;//内部访问
}
function a() external {}
}
contract sample2 {
int internal d = 9;
int private e = 90;
}
//sample3 继承 sample2
contract sample3 is sample2 {
sample1 s;
function sample3() {
s = new sample1();
s.a();//外部访问
}
}
函数修改器(较难理解)
先看一个修改器的例子:
contract sample {
int a = 90;
modifier myModifier1(int b) {
int c = b;
_;
c = a;
a = 1;
}
modifier myModifier2 {
int c = a;
_;
}
modifier myModifier3 {
a = 96;
return;
_;
a = 99;
}
modifier myModifier4 {
int c = a;
_;
}
function myFunction() myModifier1(a) myModifier2 myModifier3 returns (int d) {
a = 2;
return a;
}
}
注:
- 在修改器中,无论下一个修改器体或者函数体二者哪个先到达,都会被插入到“_;”出现的地方。
回退函数
即一个合约中唯一一个未命名函数。
- 不能有实参
- 不能有返回值
- 如果其他函数都不能匹配给定的函数标识符,那么就执行回退函数
- 如果你想让你的合约接收以太币,就必须实现回退函数
contract sample {
function() payable {
}
}
继承
即使一个合约继承自其他多个合约,在区块链上也只会创建一个合约。
父合约的代码总是会被复制到最终合约里。
- 关键字 is
示例
contract sample1 {
function a(){}
function b() {}
}
//合约2继承自合约1
contract sample2 is sample1{
function b() {}
}
contract sample3{
function sample3(int b){
}
}
//合约4继承自合约1与合约2
contract sample4 is sample1,sample2 {
function a(){}
function c() {
a();
//执行合约1中的a方法
sample1.a();
//执行合约2中的b方法
b();
}
}
关键字super
-
用于引用最终继承链中的下一个合约
示例
contract sample1{
}
contract sample2{
}
contract sample3 is sample2{
}
contract sample4 is sample2{
}
contract sample5 is sample4{
function myFunc(){}
}
contract sample6 is sample1,sample2,sample3,sample4,sample5 {
function myFunc() {
//执行sample5中的myFunc方法
super.myFunc();
}
}
抽象合约
- 仅包含函数原型而不包含函数实现的合约
- 抽象合约不能被编译
- 如果一个合约继承自抽象合约且不重写,那么它自己也是抽象合约
示例
contract sample1{
function a() returns (int b);
}
contract sample2{
function myFunc(){
sample1 s = sample();
s.a();
}
}
库
库的目的是在一个特定地址中只部署一次,其其代码可供复用。
示例-使用solidity的math库:
library math
{
function addInt(int a,int b) return (int c){
return a+b;
}
}
contract sample {
function data() returns (int d){
return math.addInt(1,2);//调用math库中的addInt方法
}
}
使用场景
- 如果有许多合约,这些合约有一些共同的代码,那么可以把它们共同的代码部署成一个库。这么做的好处是这样能节省gas。因为gas的大小依赖于合约的规模。
返回多个值
示例:
contract sample{
function a() returns (int a,string c){
return (1,"m");
}
function b(){
int A;
string memory B;
(A,B) = a();// A =1,B = "m"
(A,) = a();// A =1
(,B) = a(); //B = "m"
}
}
全局变量
- 特殊变量
- 特殊函数
1、区块和交易属性
- block.blockhash(uint blockNumber) returns (bytes32) //区块哈希值
- block.coinbase(address) //当前区块矿工的地址
- block.difficulty(uint) //当前区块的难度值
- block.gaslimit(uint) //当前区块的gas上限,定义了整个区块中的所有交易最多能消耗多少gas
- block.number(uint) //当前区块的序号
- block.timestamp(uint) //当前区块的时间戳
- msg.gas(uint) //当前剩余的gas
- msg.sender(address) //当前调用发起人的地址
- msg.sig(bytes4) //调用数据的前四个字节
- msg.value(uint) //这个消息所附带的货币量,单位为wei
- now(uint) //当前区块的时间戳,等同于block.timestamp
- tx.gasprice(uint) //交易的gas价格
- tx.origin(address) //交易的发起人
2、地址类型相关
-
.balance(uint256) //地址余额,单位为wei
-
.send(uint256 amount) returns(bool) //发送指定数量的wei到地址
3、合约相关
- this //当前合约
- selfdestruct(address recipient) //销毁当前合约,把其中的资金发送到指定地址
以太币单位
一个数字可以用wei、finney、szabo、Ether等单位转换为不同面值的以太币。默认使用wei为单位。
存在、真实性和所有权合约的证明
下面我们要实现一个“证明文件所有权”的合约。
分以下几步来进行:
- 1、成对存储文件的哈希和文件所有者的名字,用以实现所有权证明(PoO)
- 2、成对存储文件的哈希和区块的时间戳,用以实现文件在某个特定时间存在的证明(PoE)
- 3、存储哈希自身,用以证明文件的真实性。如果文件被修改,其哈希也会被改变。更改过的文件的哈希会使得合约无法发现文件,从而证明文件被修改过。
代码如下:
contract Proof{
struct FileDetails {
uint timestamp;
string owner;
}
mapping (string => FileDetails) files;
event logFileAddedStatus(bool status,uint timestamp,string owner,string fileHash);
//存储文件所有者
function set(string owner,string fileHash){
if(files[fileHash].timestamp==0){
files[fileHash] = FileDetails(block.timestamp,owner);
//触发一个事件以至于前端应用知道文件的存在
logFileAddedStatus(true,block.timestamp,owner,fileHash);
}else {
//告诉前端文件存在,但是不能存储
logFileAddedStatus(false,block.timestamp,owner,fileHash);
}
}
//获取文件信息
function get(string fileHash) returns (uint timestamp,string owner){
return (files[fileHash].timestamp,files[fileHash].owner);
}
}
编译和部署合约
-
sol编译器 之 安装指南
查看sol编辑器查看链接
-
sol编译器 之 使用指南
查看sol编辑器使用指南链接
这里,我们使用solcjs和Browser Solidity ,其中solcjs允许在node.js中以编程方式编译Solidity,而Browser Solidity
是一个适用于小型合约的IDE。
至此,我们将Solidity语言进行了基本的讲解,下一节中我们将介绍如何使用web3.js开发DApp前端。