【一步步一起学DApp开发】(三)Solidity语言讲解 | 用Solidity编写智能合约

2023-10-07 17:27:48 浏览数 (2)

有好几种语言可以用于编写以太坊智能合约,不过Solidity是最热门的语言。在本章中,我们将首先学习Solidity编程语言。然后创建一个DApp,用于证明在特定时间的存在、真实性和所有权,即证明一个文件在一个特定时间属于一个特定所有者。

要点:

  • Solidity源文件的布局
  • 理解Solidity的数据类型
  • 合约的特殊变量和函数
  • 控制结构
  • 合约的结构和功能
  • 编译和部署合约
Solidity源文件

Solidity源文件使用的扩展名为.sol。这里面我们使用的是0.4.2版本。

智能合约的结构

合约就像一个类(class),其中包含:

  • 状态变量(state variable)
  • 函数(function)
  • 函数修改器(function modifier)
  • 事件(event)
  • 结构(structure)
  • 枚举(enum)

同时,合约还支持继承与多态。

示例:

代码语言:javascript复制
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字符串

示例:

代码语言:javascript复制
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

示例

代码语言:javascript复制
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

示例

代码语言:javascript复制
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。
代码语言:javascript复制
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操作符,则会删除与该键相关的值

示例

代码语言:javascript复制
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也支持显式转换,如果编译器不允许在两种数据类型之间隐式转换,则可以进行显式转换。建议尽量避免显式转换,因为可能返回难以预料的结果。

示例:

代码语言:javascript复制
uint32 a = 0x12345678;
uint16 b = uint16(a); // b = 0x5678,将uint32类型显式转换为uint16,也就是说,把较大类型转换为较小类型,因此高位被截掉了

var

使用关键字var声明的变量,其变量类型根据分配给它的第一个值来动态确定。一旦分配了值,类型就固定了,所以如果给它指定另一个类型,将引起类型转换。

代码语言:javascript复制
int256 x= 12;
var y = x;//此时y的类型是int256

uint256 z = 9;
y = z;//此时,报出异常,因为uint256不能转换为int256类型

但要注意的是:

  • 在定义数组array和map时不能使用var。var也不能用于定义函数参数和状态变量

控制结构

  • if-else
  • while
  • for
  • breakcontinue
  • return
  • ?:
  • 等等
代码语言:javascript复制
//结构上和其他语法没有什么差异
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关键字来创建一个新合约。

例如:

代码语言:javascript复制
contract sample1 {
	int a;
	function assign(int b){
		a = b;
	}
}

contract sample2 {
	function sample2(){
		sample1 s = new sample1(); //注意写法
		s.assign(12);
	}
}

异常

异常的抛出分为自动和手动。 若你想手动抛出异常,可以使用throw手动抛出。 注意,异常抛出后,会撤销对状态和余额的所有改变。

代码语言:javascript复制
contract sample {
	function myFunction () {
		throw;
	}
}

函数调用

  • 内部函数调用:一个函数在同一个合约中调用另一个函数
  • 外部函数调用:一个函数调用另一个合约的函数。
外部函数调用–this关键字

合约sample1

代码语言:javascript复制
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

代码语言:javascript复制
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:私有函数和状态变量类似于内部函数,但是继承合约不可以访问它们

示例

代码语言:javascript复制
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();//外部访问
	}
}
函数修改器(较难理解)

先看一个修改器的例子:

代码语言:javascript复制
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;
    }

}

注:

  • 在修改器中,无论下一个修改器体或者函数体二者哪个先到达,都会被插入到“_;”出现的地方。
回退函数

即一个合约中唯一一个未命名函数。

  • 不能有实参
  • 不能有返回值
  • 如果其他函数都不能匹配给定的函数标识符,那么就执行回退函数
  • 如果你想让你的合约接收以太币,就必须实现回退函数
代码语言:javascript复制
contract sample {
	function() payable {
	
	}
}
继承

即使一个合约继承自其他多个合约,在区块链上也只会创建一个合约。 父合约的代码总是会被复制到最终合约里。

  • 关键字 is

示例

代码语言:javascript复制
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
  • 用于引用最终继承链中的下一个合约 示例
代码语言:javascript复制
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();
	}
}
抽象合约
  • 仅包含函数原型而不包含函数实现的合约
  • 抽象合约不能被编译
  • 如果一个合约继承自抽象合约且不重写,那么它自己也是抽象合约

示例

代码语言:javascript复制
contract sample1{
	function a() returns (int b);
}
contract sample2{
	function myFunc(){
		sample1 s = sample();
		s.a();
	}
}

库的目的是在一个特定地址中只部署一次,其其代码可供复用。

示例-使用solidity的math库:

代码语言:javascript复制
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的大小依赖于合约的规模。
返回多个值

示例:

代码语言:javascript复制
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、存储哈希自身,用以证明文件的真实性。如果文件被修改,其哈希也会被改变。更改过的文件的哈希会使得合约无法发现文件,从而证明文件被修改过。

代码如下:

代码语言:javascript复制
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前端。

0 人点赞