💡
💡
💡
💡
Solidity智能合约开发
Search…
💡
💡
💡
💡
Solidity智能合约开发
前言
1.1 智能合约简介
1.2 以太坊虚拟机简介
1.3 智能合约的编写与调试
1.4 智能合约的部署与执行
1.5 智能合约如何与其他IT系统交互
1.6 Solidity语言教程
1.6.1 典型Solidity源文件包含的组成部分
1.6.2 智能合约的组成部分
1.6.3 类型介绍
1.6.4 单位及全局变量
1.6.5 表达式及控制结构
1.6.6 Solidity中的智能合约
1.6.7 Solidity汇编语言
1.6.8 Solidity编码风格
1.6.9 Solidity V0.5.0版本与之前版本的显著区别
1.6.10 Solidity V0.6.0版本与之前版本的显著区别
1.6.11 课外参考
Powered By
GitBook
1.6.5 表达式及控制结构
1.6.5.1 输入参数和输出参数
和Javascript一样,函数有输入参数,但和Javascrip以及C语言不同的是,Solidity函数可以返回任意数量的返回值。
1)输入参数
输入参数的定义和变量的定义一样,如果某参数不使用,可以省略其变量名。我们看下面的例子。
pragma solidity ^0.4.16;
contract Simple {
function taker(uint _a, uint _b) public pure {
// 输入参数 _a 和 _b.
}
}
2)返回值
返回值的定义紧跟在关键字returns之后。如下例所示。
pragma solidity ^0.4.16;
contract Simple {
function arithmetics(uint _a, uint _b)
public
pure
returns (uint o_sum, uint o_product)
{
o_sum = _a + _b;
o_product = _a * _b;
}
}
返回值的名字可省略。返回值可用return指定。return可返回多个值。返回值都会被初始化为0,如果它们没有被显式地赋值,将一直为0。
1.6.5.2 控制结构
Javascript中的控制结构除了switch和goto,其它的在Solidity中也都存在。Solidity的控制语句有:if, else, while, do, for, break, continue, return, ? : 。它们的用法和定义与Javascript或C语言一样。
在条件语句中,括号不能省略,但在单一语句中,花括号可省略。
注意:不像C语言和Javascript,它们可以把非布尔值转换为布尔值,Solidity不支持这样的转换,因此if (1) { ... }在Solidity中是错误的用法。
1.6.5.3 函数调用
1)内部函数调用(Internal Function Calls)
当前合约的函数可以被直接(内部)调用,甚至是递归调用,见下例。
pragma solidity ^0.4.16;
contract C {
function g(uint a) public pure returns (uint ret) { return f(); }
function f() internal pure returns (uint ret) { return g(7) + f(); }
}
上例中的函数调用在以太坊虚拟机中会被翻译成简单的跳转语句。调用完后当前内存不被清零,在内存中传递引用,这种方式非常高效。只有同一个合约中的函数才能进行内部调用。
2)外部函数调用(External Function Calls)
表达式this.g(8);和c.g(2);(c是个合约实例)也是函数调用,但这种函数调用是外部函数调用,它通过消息而不是跳转语句进行调用。注意,不能在合约的构造函数中使用this调用,因为在构造函数调用时合约还未真正实例化。
在一个合约中想调用其它合约的函数只能是外部函数调用。在外部函数调用时,函数的参数都会被拷贝到内存。当调用其它合约的函数时,要发送的wei和gas可用.value()和.gas()定义,如下例所示。
pragma solidity ^0.4.0;
contract InfoFeed {
function info() public payable returns (uint ret) { return 42; }
}
contract Consumer {
InfoFeed feed;
function setFeed(address addr) public { feed = InfoFeed(addr); }
function callFeed() public { feed.info.value(10).gas(800)(); }
}
在上例中修饰符payable必须被用于info函数,否则.value()将无法使用。
值得注意的是表达式InfoFeed(addr)进行了显式转换,告诉系统,地址addr的合约类型是InfoFeed,并且不会调用构造函数。显式类型转换在使用时必须非常小心,在没有把握时,不要使用。
我们也可以直接使用函数setFeed(InfoFeed feed){feed = feed;}。注意feed.info.value(10).gas(800)仅仅只在本地局部设置函数调用的gas值,gas(800)才是真正的函数调用。
如果被调用的合约不存在,合约抛出异常或gas耗尽,函数调用就会抛出异常。
注意:任何调用其它合约的行为都有风险,尤其是当被调用的合约源代码未知时。调用另外一个合约就意味着当前合约把控制权给了另外一个合约,那么另外那个合约做什么就完全超出了控制。特别要提醒的是,尽量在当前合约的状态已经进行了明确的改变之后,再调用其它的合约,这样操作能在最大程度上防止重入攻击(reentrancy exploit)。
3)命名调用和匿名函数参数(Named Calls and Anonymous Function Parameters)
函数的参数可用{}表示,参数列表必须符合函数定义中对参数的定义,顺序可任意,如下例所示。
pragma solidity ^0.4.0;
contract C {
function f(uint key, uint value) public {
// ...
}
function g() public {
// 命名参数
f({value: 2, key: 3});
}
}
4)省略函数参数名
函数中不用的参数(尤其是返回值)可被省略,虽然这些参数仍然在堆栈中,但无法使用。
pragma solidity ^0.4.16;
contract C {
// 省略函数参数名
function func(uint k, uint) public pure returns(uint) {
return k;
}
}
1.6.5.4 用new创建智能合约
用户可以在智能合约中用关键字new创建新的合约实例。待创建的合约必须已经定义。
pragma solidity ^0.4.0;
contract D {
uint x;
function D(uint a) public payable {
x = a;
}
}
contract C {
D d = new D(4); // 会在合约C的构造函数中执行
function createD(uint arg) public {
D newD = new D(arg);
}
function createAndEndowD(uint arg, uint amount) public payable {
// 创建的同时发送以太币
D newD = (new D).value(amount)(arg);
}
}
在上例中,可以用.value()选项创建合约实例D并发送以太币,但不能定义gas值。如果创建失败,会抛出异常。
1.6.5.5 条件判断表达式的执行顺序
在Solidity中并没有明确定义条件判断表达式的执行顺序,但会保证条件判断表达式都会执行。条件判断表达式在做布尔运算时会做短路运算。
1.6.5.6 赋值运算
1.6.5.6.1 赋值运算及返回多个值
Solidity允许定义记录类型(tuple),所谓记录类型就是包含一系列不同类型对象的数据结构,每个记录类型的长度固定。记录可被用于返回多个返回值。如下例所示。
pragma solidity >0.4.23 <0.5.0;
contract C {
uint[] data;
function f() public pure returns (uint, bool, uint) {
return (7, true, 2);
}
function g() public {
// 把函数返回的记录值赋值给多个变量
(uint x, bool b, uint y) = f();
// 数值互换-但不能用于非数值的storage存储类型变量.
(x, y) = (y, x);
//变量可被略去
(data.length,,) = f(); // Sets the length to 7
// 只有赋值语句左边才能略去变量
// 一种特例意外:
(x,) = (1,);
// (1,) 是仅有的能用来定义只有一个元素的记录,因为 (1) 等于 1.
}
}
1.6.5.6.2 数组和结构体的赋值运算
对于非值类型的变量比如数组和结构体来说,赋值运算的定义就要复杂一些。给一个状态变量赋值通常会把这个值进行拷贝。给一个局部变量赋值则会复制一个基本类型的值,也即长为32个字节的静态类型。如果把一个结构体和数组(包括bytes和string)由一个状态变量赋值给一个局部变量,这时局部变量只是引用了状态变量。当对此局部变量再赋值时,只会改变局部变量的引用,而不改变状态变量的值。如果对该局部变量的成员进行赋值,则状态变量对应的成员值也会被改变。
1.6.5.7 变量声明和作用域
每一个被声明的变量都有个初始值,通常初始值为0。比如bool变量的初始值为false。uint和int的初始值为0。对定长数组以及bytes1到bytes32的类型,其初始值会被设为其元素所对应类型的初始值。对变长数组以及bytes和string,其初始值为空。
通常函数中定义的变量其作用域为整个函数,与其具体在函数的哪个位置定义无关。关于作用域的规则,Solidity完全承袭了Javascript。注意:下例中会出现编译错误。
// 无法通过编译
pragma solidity ^0.4.16;
contract ScopingErrors {
function scoping() public {
uint i = 0;
while (i++ < 1) {
uint same1 = 0;
}
while (i++ < 2) {
uint same1 = 0;// 非法,重复定义same1。
}
}
function minimalScoping() public {
{
uint same2 = 0;
}
{
uint same2 = 0;// 非法,重复定义same2。
}
}
function forLoopScoping() public {
for (uint same3 = 0; same3 < 1; same3++) {
}
for (uint same3 = 0; same3 < 1; same3++) {//非法,重复定义same3。
}
}
}
此外,函数中的一个变量一旦被定义,就会在函数的起始部分被初始化。因此,下例中,尽管代码风格很差,但却是合法可以通过编译的。
pragma solidity ^0.4.0;
contract C {
function foo() public pure returns (uint) {
//在这里,baz被隐式初始化为0
uint bar = 5;
if (true) {
bar += baz;
} else {
uint baz = 10;//这条语句永远不会执行
}
return bar;// 返回值为 5
}
}
1.6.5.8 错误处理:Assert,Require,Revert和Exceptions
Solidity是通过回退状态的方式来处理错误。发生异常时会撤消当前的调用(及其所有的子调用)所改变的状态,同时给调用者返回一个错误标识。
Solidity提供了两个函数assert和require来进行条件检查,如果条件不满足则抛出异常。assert函数通常只用来检查内部错误,require函数可用来检查输入变量或合约的状态变量是否满足条件,还可以验证合约调用的返回值是否有效。正确使用assert,可以帮我们发现智能合约及函数调用中的错误。
除了assert和require,另外还有两种方式来触发异常:revert和throw。revert函数可以用来标记错误并回退当前调用。还可以用string定义出错时的消息并把错误信息返回给调用者。使用throw关键字也可以抛出异常(从0.4.13版本开始,throw关键字已被弃用,将来会被淘汰。),但无法返回错误信息。
当子调用(sub-call)中发生异常时,异常会自动向上“冒泡”(异常会再次抛出)。 不过也有一些例外:send和底层的函数调用call, delegatecall,callcode发生异常时,这些函数返回false,不抛出异常。
注意:如果从一个不存在的地址调用底层函数call,delegatecall和callcode, 它们也会返回成功,所以我们在进行调用时,应该要优先检查函数是否存在。在Solidity中,异常可以抛出但无法捕捉。
下面这个示例说明了如何使用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 required.");
uint balanceBeforeTransfer = this.balance;
addr.transfer(msg.value / 2);
// transfer调用会在调用失败时抛出异常而无法回调,我们将无法取回一半的金额。
assert(this.balance == balanceBeforeTransfer - msg.value / 2);
return this.balance;
}
}
下列场景会产生assert类型的异常:
1) 越界,或用负值下标访问数组,如i >= x.length 或 i < 0时访问x[i]。
2) 序号越界,或用负值下标访问一个定长的bytesN。
3) 被除数为0, 如5/0 或 23 % 0。
4) 移位运算时所移位数为负值,如:5<<i; i为-1。
5)将一个过大值或负值转为枚举类型。
6) 调用一个初始化为0的内部函数类型变量。
7) 调用assert的参数为false。
下列场景会产生require类型的异常:
1) 调用throw
2) 调用require的参数为false
3)用户通过消息调用一个函数,在调用的过程中,函数没有正确结束(gas不足,没有匹配到对应的函数,或被调用的函数抛出异常)。调用底层操作如call,send,delegatecall或callcode时除外,它们不会抛出异常,它们会通过返回false来表示失败。
4) 使用关键字new创建一个新合约时没有正常完成。
5) 通过外部函数调用合约时,合约未实现。
6) 合约用来接收以太币的public函数没有用payable修饰符(包括构造函数,和回退函数)定义。
7) 合约通过一个public的getter函数(public getter 函数)接收以太币。
8) .transfer()执行失败。
当发生require类型异常时,Solidity会执行一个回退操作(指令0xfd)。当发生assert类型异常时,Solidity会执行一个无效操作(指令0xfe)。在上述的两种情况下,EVM都会回撤所有状态的改变,这样设计是期望如果函数一旦执行,要么就正确的执行,一旦执行可能会出问题,就不再继续安全地执行,必须保证交易的原子性(一致性,要么全部执行,要么一点改变都没有,不能只改变一部分),所以一旦出现异常,就需要撤销所有的操作,让整个系统状态不受此失败交易的影响。
注意:assert类型的异常会消耗掉所有的gas, 而require从大都会版本(Metropolis, 即目前主网所在的版本)起不会消耗gas。
下例显示了如何与revert和require一起使用error字符串:
pragma solidity ^0.4.22;
contract VendingMachine {
function buy(uint amount) payable {
if (amount > msg.value / 2 ether)
revert("Not enough Ether provided.");
// 另一种方式:
require(
amount <= msg.value / 2 ether,
"Not enough Ether provided."
);
// Perform the purchase.
}
}
上例中如果错误是通过Error(string)函数调用产生的,则信息将会是ABI编码。revert("Not enough Ether provided.")将会产生如下数据作为错误返回信息。
0x08c379a0 // Function selector for Error(string)
0x0000000000000000000000000000000000000000000000000000000000000020 // Data offset
0x000000000000000000000000000000000000000000000000000000000000001a // String length
0x4e6f7420656e6f7567682045746865722070726f76696465642e000000000000 // String data
1.6 Solidity语言教程 - Previous
1.6.4 单位及全局变量
Next - 1.6 Solidity语言教程
1.6.6 Solidity中的智能合约
Last modified
2yr ago
Copy link
Contents
1.6.5.1 输入参数和输出参数
1.6.5.2 控制结构
1.6.5.3 函数调用
1.6.5.4 用new创建智能合约
1.6.5.5 条件判断表达式的执行顺序
1.6.5.6 赋值运算
1.6.5.6.1 赋值运算及返回多个值
1.6.5.6.2 数组和结构体的赋值运算
1.6.5.7 变量声明和作用域
1.6.5.8 错误处理:Assert,Require,Revert和Exceptions