以太坊智能合约开发(二):Solidity编程基础

以太坊智能合约开发(二):Solidity编程基础,第1张

以太坊智能合约开发(二):Solidity编程基础 1 sol文件结构1.1 编译开关1.2 引用其他源文件1.3 合约1.4 库1.5 接口1.6 代码注释 2 合约文件结构3 变量类型3.1 值类型3.2 引用类型3.2.1 数据位置3.2.2 数组3.2.3 创建内存数组3.2.4 结构体3.2.5 映射 4 表达式和控制语句4.2 输入参数和输出参数4.2 条件语句4.3 循环语句4.3 其他控制结构4.4 函数调用4.5 通过new创建合约 5 类型转换5.1 隐式类型转换5.2 显式转换 6 Solidity中的单位6.1 货币单位6.2 时间单位 7 全局变量7.1 区块和交易属性7.2 ABI编码函数7.3 错误处理7.4 地址相关7.5 合约相关


1 sol文件结构 1.1 编译开关

在使用Solidity编写智能合约前,需要声明所使用的的编译器版本,编译器开关 pragma solidity ^0.6.0;,该编译开关表明编译器版本需要不低于0.6.0且低于于0.5.0才可以编译。也可以指定编译器的版本范围:pragma solidity >= 0.6.0 < 0.7.0;

1.2 引用其他源文件

Solidity支持导入语句引入外部文件。
全局引入:import "filename";,此语句将从“filename”中导入所有的全局符号到当前全局作用域中。
创建新的全局符号代表从文件中引入的所有成员,自定义命名空间引入符号“*”:import * as symbolName from "filename";,这条语句表示创建一个新的全局符号symbolName,其成员全部来自filename中的全局符号。
也可以创建多个全局符号分别表示源文件中的其他符号,分别定义引入语句:import {symbol1 as alias,symbol2} form "filename";,这句话表示创建新的全局符号alisa和symbol2,从filename分别引入symbol1和symbol2。

1.3 合约
pragma solidity ^0.6.0;
contract hello {
    string public name;
    constructor() public {
        name = "hello";
    }
}

这里的contract可以理解为一个类。contact有构造函数、成员函数和成员变量。

1.4 库

库和合约的区别在于库不能有Fallback函数以及payable关键字,同时也不可以定义storage变量,但是库可以修改和他们链接的合约的storage变量。

1.5 接口

与java、C++一样,solidity接口只定义行为,没有实现。

1.6 代码注释

Solidity可以使用单行注释(//)和多行注释(/···/),此外还有一种natepec注释,其文档尚未编写完成。它用三个反斜杠(///)和双星号开头的块编写(/**···*/),注释还可以使用Doxygen央视,以支持生成对文档的说明,参数验证的注解或者是在用户调用这个函数时d出的确认内容。

pragma solidity ^0.4.0;
/** @title  形状计算器 */
contract shapeCalculator {
    /** @dev  求面积和周长
    * @param w
    * @param h
    * @return s
    * @return p 
    */
    function rectangle(uint w, uint h) returns (uint s, uint p ){
        s = w * h ;
        p = 2 * (w + h );
    }
}

2 合约文件结构

一个合约一般包括以下组成部分:

状态变量(State variables)结构定义(Structure definitions)修饰符定义(Modifier definitions)事件声明(Event declaration)枚举定义(Enumeration definitions)函数定义(Function definitions) 3 变量类型 3.1 值类型

(1)布尔型:值为true或者false。
(2)整型:int或者uint。
(3)地址类型:以太坊的地址(address)的长度大小为20B,160bit(以太坊地址的大小)。地址类型也有成员变量,是所有合约的基础。balanc属性,用来查询一个地址的余额;transfer函数,用来向一个地址发送以太币。
(4)定长字节数组:固定大小的字节数组。
(5)有理数和整型字面量。
(6)枚举类型:solidity中的一种用户自定义类型,它可以显式地与整型进行转化,但不能进行隐式转换。
(7)函数:

函数可以将函数值给一个变量,该变量即为一个函数类型的变量。还可以将一个函数作为参数进行传递。也可以在函数调用中返回一个函数。
完整的函数定义为:
function <name>(<parameter types>){ internal | external } [ pure | constant | view | payable] [returns(<return type>)]

若不说明函数类型,默认函数类型为internal。如果函数没有返回值,则省略return关键字。
函数可以分为内部函数(internal)和外部函数(exinternal)。**内部函数只能在当前合约内被使用,**不能在当前合约的上下文环境以外的地方执行。如在当前的代码块内,包括内部库函数和继承的函数中,函数的默认类型就是internal,与之相反,合约中的函数默认是public,只有被当做类型名的时候,默认才是内部函数。 外部函数由地址和函数方法签名两部分组成。可作为外部函数调用的参数或者外部函数调用的返回。
注意,当前合约的public函数即可以当做内部函数使用也可以当做外部函数使用。如果想将一个函数当做内部函数使用,就用f调用;如果想当做外部函数调用就用this.f调用。此外public函数也有一个特殊的成员变量,称作selector,可以返回ABI函数选择器。

pragma solidity ^0.4.0;
contract Selector {
	function f () public view returns (bytes4) {
	 return this.f. selector;
	}
}
3.2 引用类型

相比值类型,处理复杂类型时因为其占用空间超过256位,需要更加谨慎,由于复制这些类型变量开销相当大,因此不得不考虑他们的存储位置,考虑他们是是保存在内存中(并不是永久存储)还是存储(保存状态变量的地方)。

3.2.1 数据位置

所有复杂类型(即数组和结构类型)都有一个额外属性,即数据位置,说明数据是保存在内存中还是存储中。大多数时候数据有默认位置,但也可以通过类型名后增加关键字storage和memory进行修改。函数参数的数据默认是memory,局部变量的数据位置默认storage,状态变量数据位置默认为storage。

3.2.2 数组

数组可以在声明时指定长度,也可以动态调整大小。对于存储位置是存储(storage)的数组来说,元素类型可以是任意的(即可以是数组类型,映射类型或结构体)。对于存储位置是内存(memory)的数组来说,元素类型不能是映射类型,如果作为public函数的参数,它只能是ABI类型。
一个元素类型为uint,固定长度为K的数组可以声明为uint[K],动态数组可以声明为uint[]。bytes和string类型的变量是特殊的数组。

3.2.3 创建内存数组

可使用new关键字创建一个memory的数组。与storage的数组不同,不能用.length的长度来修改数组的大小属性。

pragma solidity ^0.4.0;
contract test1{
    function fun (){
        uint [] memory A = new uint[](7);    //创建一个memory的数组
    }
    uint[] B;
    function fun2 (){
        B = new uint[](7);
        B.length = 10;
        B[9]=100;
    }
}

(1)不定长字节数组:不定长字节数组是一个动态数组,能够容纳任意长度的字节。Bytes可以声明为一个设定长度的状态变量:bytes localBytes = new bytes(0);,Bytes也可直接赋值:localBytes = "this is a test";,元素可以被压缩入字节数组:localBytes.push(byte(10));
(2)字符串:在C语言中,字符串以“\0”结尾,在solidity中,字符串并不包含结束符。

3.2.4 结构体

结构体是用来实现用户定义的数据类型。结构是一个组合数据类型,包含多个不同数据类型的变量。但结构体里没有任何代码,仅仅由变量构成。struct funder {address addr;uint amount;}。结构体目前仅支持在合约内使用,如果在参数和返回值中使用结构体,函数必须声明internal。

pragma solidity ^0.4.11;

contract CrowFunding {
    struct Funder {
        address addr;
        uint amount;
    }
    struct Campaign {
        address beneficiary;
        uint fundingGoal;
        uint numFunders;
        uint amount;
        mapping (uint => Funder) funders;
    }
    uint numCampaingns;
    mapping (uint => Campaign) campaigns;
    function newCampaign(address beneficiary,uint goal) public returns (uint campaignID){
        campaignID = numCampaingns ++;
        campaigns [campaignID] = Campaign(beneficiary,goal,0,0);
    }
    function contribute (uint campaignID) publice payable{
        Campaign storage c = campaigns[campaignID];
        c.funders[c.numFunders++] = Funder({addr:msg.sender, amount:msg.value});
        a.amount += msg.value;
    }
    function checkGoalReached(uint campaignID) public returns (bool reached){
        Campaign storage c = campaigns[campaignID];
        if (c.amount < c.fundingGoal){
            return false;
        }
        uint amount = c.amount;
        c.amount = 0;
        c.beneficiary.transfer(amount);
        return true;
    }
}

这是一个简化版的众筹合约。结构体类型可以作为元素用在映射和数组中,其自身也可以包含映射和数组作为成员变量。

3.2.5 映射

映射类型的声明形式为mapping(_KeyType => _ValueType)。_KayType可以是除映射、变长数组、合约、枚举以及结构体之外的几乎所有类型,_ValueType可以是包括映射类型在内的任何类型。
可以将映射声明为public,然后让solidity创建一个getter。_KeyType将成为getter的必须函数,并且getter会返回_ValueType。_ValueType也可以是一个映射。在使用getter时将需要递归地传入每个_Keytype参数。

pragma solidity ^0.4.0;
contract MappingExample{
    mapping (address => uint ) public balance;
    function update(uint newBalance) public{
        balance [msg.sender] = newBalance;
    }
}
contract mappingUser{
    function f() public returns(uint){
        MappingExample.m = new MappingExample();
        m.upadate(100);
        return m.balance(this);
    }
}
4 表达式和控制语句

solidity不支持switch和go…to语句。

4.2 输入参数和输出参数

Solidity和JavaScript一样,函数可能需要参数作为输入,与之不同的是,它们可以返回任意数量的参数作为输出。
(1)输入参数
输入参数的声明和变量相同,未使用的参数可以省略参数名。例如如果希望合约接受有两个整数形参的函数的外部调用,可以

pragma solidity ^0.4.0;

contract Simple {
	function taker(uint _a,uint _b) public pure {
		//用_a和_b实现相关功能
	}
}

(2)输出参数
输出参数的声明方式在关键词returns之后,与输入参数的声明方式相同。例如,如果需要返回两个结果,两个给定整数的和与积。

pragma solidity ^0.4.0;

contract Simple {
	function arithmetics(uint _a,uint _b) public pure returns (uint sum,uint product){
		sum = _a + _b;
		product = _a * _b;
	}
}

输出参数名可以被省略。输出值也可使用return语句指定。return语句也可以返回多值。返回的输出参数被初始化为0。

4.2 条件语句

与传统语句一样,solidity同样支持if/else语句。需要注意if(1){…}在solidity中是无效的,因为solidity中非布尔类型不能转换成布尔类型。

4.3 循环语句

循环语句while和for语句。

uint insertIndex = stack.length;
while(insertIndex > 0 && bid.limit <=stack[insertIndex-1].limit){
	insertIndex--;
}
4.3 其他控制结构

(1)break:用来跳出现有的循环。
(2)continue:用来退出当前的循环。
(3)return:用来从函数/方法中返回。
(4)?::三元 *** 作符。如a>b? a:b,如果a>b返回a,否则返回b。

4.4 函数调用

(1)内部函数调用
当前合约中的函数可以直接从内部调用,也可以递归调用,例如,

pragma solidity ^0.4.0;

contract Test {
	function test1 (uint a) public pure returns (uint ret){
		return test2();
	}
	function test2 internal public pure returns (uint ret){
		return test1(7) + test2();
	}
}

这些函数调用在EVM中被解释为简单的跳转。这样做的效果就是当前内存不会被清除,也就是说,通过内部调用在函数之间传递内存引用是非常有效的。
(2)外部函数调用
表达式“this.g(8)”和“c.g(2)”(其中c是合约实例)都是有效的函数调用,这种情况下,函数将会通过一个消息调用来被外部调用,而不是直接跳转。注意,不可以在构造函数中通过this来调用函数,此时真实的合约实例还没有建立。
如果想要调用其他合约的函数,需要外部调用。对于一个外部调用,所有的函数参数都需要被复制到内存。当调用其他合约的函数时,随函数调用发送的wei和gas的数量可以分别由特定选型.value和.gas指定。
(3)具名调用和匿名函数调用
函数调用参数也可以按照任意顺序由名称给出,如果他们被包含在{}中,如以下实例中所示。参数列表必须按名称与函数声明中的参数列表相符,但可以任意顺序排列。

pragma solidity ^0.4.0;
constant C {
	function f (uint key,uint value) public {
		//.....
	}
	function g () public {
		f({value : 2,key : 4});
	}
}
4.5 通过new创建合约

使用关键字new可以创建一个新合约。由于创建合约的完整代码必须事先知道,因此递归地创建智能合约是不可能的。

pragma solidity ^0.4.0;

contract T{
    uint x;
    function T (uint a) public payable{
        x = a;
    }
}

contract T2 {
    T D = new T(4);   //作为合约T2构造函数的一部分执行
    function creatT (uint arg) public {
        T newT = new T(arg);
    }
    
    function creatAndEndowT(uint arg, uint amount) public payable{
    //随合约的创建发送ether。
        T newT = (new T).value(amount)(arg);
    }
}

使用.value选项创建T的实例时可以转发ether,但是不能限制gas的数量。如果创建失败(栈溢出,没有足够的余额等问题),就会引发异常。

5 类型转换

Solidity同样支持类型转换,例如,将一个数字字符串转换为整型或浮点数。转换被分为隐式类型转换和显式类型转换。

5.1 隐式类型转换

如果运算符支持两种不同的类型,那么编译器会尝试隐式类型转换,同理赋值时也是类似。通常,隐式类型转换要保证不丢失数据且语义通顺。例如uin8可以转换为uint256,int8不能转为uint256,因为uint不支持-1。任何无符号整数都可以转换为相同或更大长度的字节数组,任何可以转换为uint16的类型,也可以转换为address类型。

function add() public pure returns (uint){
	uint8 i = 10;
	uint16 j = 20;
	uint16 k = i + j;
	return k;
}

在运行uint16 k= i+j运算时,i会隐式转换为uint16,在return k时,k会隐式转换为uint256。

5.2 显式转换

如果编译器不允许隐式的自动转换,但你知道转换没有问题时,可以进行显式类型转换。例如,将一个int8类型转换成uint类型。

int8 y = 8;
uint x = uint(y);
6 Solidity中的单位 6.1 货币单位

一个数字常量后面跟随一个后缀 wei, funney,szabo或ether,这个后缀就是货币单位。

6.2 时间单位

一个数字常量后面跟随一个后缀seconds,minutes,hours,days,weeks,years,这个后缀就是时间单位。

7 全局变量

在全局命名空间中已经存在了一些特殊的变量和函数,他们主要用来提供关于区块的信息或一些通用的工具函数。

7.1 区块和交易属性 block.coinbase(address):挖出当前区块的矿工地址。block.difficulty(uint):当前区块难度。block.gaslimit:当前区块gas限额。block.number:当前区块号。gasleft () returns(uint256):剩余的gas。msg.date(bytes):完整的calldate。msg.sender(address):当前发送者(当前调用)。msg.value(uint):随消息发送wei的数量。tx.gasprice(uint):交易gas的价格。tx.origin(address):交易发起者。 7.2 ABI编码函数

这里给出部分ABI编码可以使用的全局函数,后面会详细介绍ABI的相关概念和 *** 作。

abi.encode(…) returns (bytes):对给定参数进行编码。abi.encodePacked(…) returns (bytes):对给定参数进行紧打包编码。
这些编码函数可以用来构造函数调用数据,而不用实际进行调用。 7.3 错误处理 assert(bool condition):如果条件不满足,则当前交易没有效果,主要用于检查内部错误。require(bool condition):如果条件不满足,则撤销状态更改,主要用于检查由输入或者外部组件引起的错误。require(bool condition,string message):如果条件不满足,则撤销更改状态,用于检查由输入或者外部组件引起的错误,可以同时提供一个错误消息。revert():中止运行并撤销状态更改。revert(string reason):中止运行并撤销状态更改,同时提供一个解释性的字符串。
solidity使用“状态恢复”异常的处理方式来解决程序运行中出现的错误。这种异常处理方法是将撤销对当前调用中的状态所做的所有更改,并且还向调用者标记错误发生的位置。函数asser和require可用于检查条件并在条件不满足的时抛出异常。**assert函数只用于测试内部错误,并检查非变量。require函数用于确定条件的有效性,例如输入变量,或合约状态变量是否满足条件,或验证外部合约调用返回的值。**如果使用得当,分析工具可以评估用户的合约,并标出那些会使用assert失败的条件和函数调用。正常的语句不会导致assert语句的失败,如果出现则表示出现了一个需要修复的的bug。
还有另外两种触发异常的方法:一种是revert函数标记错误并恢复当前的调用,revert调用中包含有关错误的详细信息是可能的,这个消息会被返回给调用者;另一种第throw代替revert,但无法返回错误信息。从0.4.13版本开始throw已被弃用。
下面的例子中,可以看到如何使用require检查输入条件,以及如何使用assert检查内部错误。注意可以给require提供一个消息字符串,而assert不行。
pragma solidity ^0.4.22;

contract Sharer{
    function sendHalf (address addr) public payable returns (uint balance){
        require (msg.value % 2 == 0,"Even value require.");
        uint balanceBeforTransfer = this.balance;
        addr.transfer(msg.value / 2);
        assert (this.balance == balanceBeforTransfer-msg.value/2);
        return this.balance;
    }
}

下列情况将产生一个assert异常:

如果访问数组的索引太大或为负

如果访问固定长度bytesN的索引太大或为负

如果用零当除数做除法或模运算

如果移位负数位

如果将一个太大或者负值转换为一个枚举类型

调用内部函数类型的零初始化变量

调用assert的参数最终结算为false。
下列情况将会产生一个require式异常:

调用throw

调用require的参数最终结算为false

如果通过消息调用某个函数,但该函数没有正确结束(耗尽gas,没有匹配函数,或者本身抛出一个异常),上述函数不包括call、send、callcode等低级 *** 作,低级 *** 作不会抛出异常,而通过返回false来指示失败。

使用new关键字创建合约,但合约没有被正确创建。

对不包含代码的合约执行外部函数调用。

如果合约通过一个没有payable修饰符的共有函数接收Ether

合约通过公有的getter函数接收Ether

如果.transfer失败。
在内部,solidity对一个require式的异常执行回退 *** 作并执行一个无效的 *** 作来引发assert式异常。在这两种情况下,都会导致EVM回退对状态所做的所有更改。回退的原因不能继续安全的执行,没有实现预期效果。因为我们想保留交易的原子性,所以最安全的做法是回退所有更改并使整个交易不产生效果。assert异常不会消耗任何gas。
下面的例子展示了require和revert中使用错误字符串:

pragma solidity ^0.4.22;

contract Sharer{
    function buy (uint amount) payable {
        if (amount > msg.value / 2 ether)
            revert ("no enough Ether");
        require (amount <= msg.value / 2 ether,"no enough Ether");
    }
}
7.4 地址相关

针地址 *** 作的全局函数如下。

< address >.balance(uint256):以wei为单位的地址类型余额。< address >.tranfer(uint256 amount):向地址类型发送数量为amount的wei,失败时抛出异常。< address >.send(uint256 amount) returns(bool):向地址类型发送数量为amount的wei,失败时返回false。< address >.call(…) returns (bool):发出低级函数call,失败时返回false,发送所有可用gas,失败时可调节。< address >.callcode(…) return (bool):发送低级函数callcode,失败时返回false,发送所有可用gas,失败时可调节。< address >.delegatecall(…) returns (bool):发送低级函数delegatecall,失败时返回false,发送所有可用gas,失败时可调节。
在使用send时,如果栈的深度已经达到1024(可以由调用者强制指定),转账就会失败;如果接受者用光了gas,转账也会失败。为了保证以太币转账安全,应该总是检查send的返回值,利用transfer或者更好的方式,例如使用接收者取回钱的方式。 7.5 合约相关 this(current contract’s type):当前合约可以显示转换为地址类型。selfdestruct (address recipient):销毁合约,并把余额发送到指定地址类型。
此外,当前合约内的所有函数都可被直接调用,包括当前函数。

欢迎分享,转载请注明来源:内存溢出

原文地址: http://outofmemory.cn/zaji/1298375.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-06-10
下一篇 2022-06-10

发表评论

登录后才能评论

评论列表(0条)

保存