solidity_solidity返回数组 - CSDN
精华内容
参与话题
  • solidity教程:solidity语言入门

    千次阅读 2018-10-29 15:02:20
    这篇关于Solidity教程的博客展示了很多Solidity特性。本教程假定你对以太坊虚拟机和编程有一定的了解。 以太坊,“世界计算机”提供了一个非常强大的全球共享基础设施,使用名为Solidity的编程语言构建去中心化应用...

    这篇关于Solidity教程的博客展示了很多Solidity特性。本教程假定你对以太坊虚拟机和编程有一定的了解。

    以太坊,“世界计算机”提供了一个非常强大的全球共享基础设施,使用名为Solidity的编程语言构建去中心化应用程序。

    让我们开始我们的Solidity教程,介绍Solidity。

    什么是Solidity?

    以太坊Solidity是一种面向智能合约的高级语言,其语法与JavaScript类似。solidity是用于生成在EVM上执行的机器级代码的工具。solidity编译器获取高级代码并将其分解为更简单的指令。Solidity代码封装在Contracts中。

    以太坊合约中的solidity

    合约是以太坊去中心化应用程序的基本构建模块。所有变量和函数都是合约的一部分,这是所有项目的起点。一个名为MyFirst的空合约看起来像这样:

    version pragma ^0.4.19;
    contract MyFirst{
    }
    

    11831773-5210aaa8a79bc143.jpg

    image

    盯紧你的屏幕因为接下来在我们的Solidity教程中我们将开始编码......

    Solidity文件的布局

    源文件可以包含任意数量的合约定义,包括指令和pragma指令。

    Version Pragma

    Version Pragma是定义代码使用的Solidity编译器版本的声明。

    version pragma ^0.4.00;
    

    注意:上面显示的源文件不会使用早于版本0.4.0的编译器进行编译,也不能在从版本0.5.0开始的编译器上运行。

    导入其他源文件

    Ethereum Solidity支持与JavaScript中可用的导入语句非常相似的导入语句,尽管Solidity不知道default export的概念。

    在全局级别,可以使用以下形式的import语句:

    import "filename";
    

    上述语句将所有全局符号从filename导入当前全局范围。

    import * as symbolName from "filename";
    

    注释

    就像任何其他语言一样,Solidity可以使用单行和多行注释。

    // This is a single-line comment.
    /*
    This is a
    multi-line comment
    */
    

    现在,在我们进一步深入了解Solidity教程之前,应该知道以太坊有三个可以存储项目的区域。

    • 存储Storage:所有合约状态变量所在的位置。每个合约都有自己的存储,并且在函数调用之间是持久的。
    • 内存Memory:保存临时值并在(外部)函数调用之间擦除,并且使用起来更便宜。
    • 堆栈Stack:保存小的局部变量并且几乎可以免费使用,但只能保存有限数量的值。

    对于几乎所有类型,都无法指定它们应存储的位置,因为它们在每次使用时都会被复制。

    好了,既然你已经知道以太坊Solidity中的存储位置,那么让我告诉你一般的值类型。

    solidity中的值类型

    以下类型也称为值类型,因为这些类型的变量将始终按值传递。

    11831773-db337755867d1e66.png

    image

    布尔

    关键词:bool

    值是常数,即true或false。

    整型

    关键字:int/uint(uint8到uint256,步长为8(无符号,最多为256位),int8为int256)

    各种大小的有符号和无符号整数。

    例:

    contract MySample{
    uint UnsignedInt =50;
    }
    

    在上面的语句中,我们创建了一个名为InsignedInt的uint并将其设置为50。

    地址

    关键字:address

    保存一个20字节的值(以太坊地址的大小)。地址类型也有members,并作为所有合约的基础。

    地址成员:balance与transfer

    可以使用属性balance查询地址的余额,并使用transfer函数将以太网发送到地址。

    address x = 0x123;
    address myAddress = this;
    if  (x.balance < 10 && myAddress.balance > = 10)
    x.transfer(10);
    

    字符串

    String:字符串文字用双引号或单引号如“foo”或'bar'编写。

    用于任意长度的UTF数据。

    string language ="Solidity";
    

    这些值类型可以在包含运算符的表达式中相互交互。接下来,在我们的Solidity教程中,让我告诉你各种运算符。

    运算符

    solidity的运算符与JavaScript相同。Solidity有四种类型的运算符:

    11831773-9dc8c733c3bc5b76.png

    image

    算术运算符

    Solidity具有非常简单的数学运算。以下与大多数编程语言类似:

    • 增加:x + y
    • 减法:x - y
    • 乘法:x * y
    • 除法:x / y
    • 取整/求余:x%y

    Solidity还提供了使用指数运算符的选项,具体如下:

    uint x = 10 **  3; // equal to 10^3 = 1000
    

    增量运算符

    增量运算符的稳定性:a++,a- ,++a,-a,a+=1,a=a+1

    适用于其他编程语言的规则也是类似的。

    按位运算符

    以下是运算符:(按位OR)'|',(按位异或),(按位求反)'~',(按位右移)'>>',(按位左移)'<<'

    逻辑运算符

    Solidity中的逻辑运算符:!(逻辑否定),&&(逻辑和),||(逻辑或),==(相等),!=(不相等)

    例:

    contract operators {
    // Arithmetic Operators
    // +,-,*,/, %, **
    // Incremental Operators
    // a++, a--, a+=1, a=a+1,++a,--a;
    a=10;
    a= a++; //here, output will be 10, because the value is first returned and then then increment is done
    a=++a;
    //Logical Operators
    !, &&, ||, ==, !=
    isOwner = true && false;
    var orValue= 0x02 | 0x01; // output would be 0x03
    //Bitwise Operators~,>>, <<;
    function Operators() {
    // Initialize state variables here}}
    

    现在有时需要更复杂的数据类型。为此,Solidity提供结构。

    solidity数据结构

    Solidity提供三种类型的数据结构:

    11831773-01cea2a550393737.png

    image

    结构Structs

    Solidity提供了一种以Structs形式定义新类型的方法。Structs是自定义类型,可以对多个变量进行分组。

    pragma solidity ^0.4.0;
    contract Ballot {
    struct Voter { // Struct
    uint weight1, weight2, weight3;
    bool voted;
    address delegate1, delegate2, delegate3, delegate4;
    string name;
    uint vote1, vote2, vote3, vote4, vote5;
    uint height1, height2, height3   } }
    

    注意:结构只能有16个成员,超过该成员可能会发生以下错误:Stack too Deep 堆栈太深。

    结构允许创建具有多个属性的更复杂的数据类型。

    现在,如果你需要一些集合,比如说地址,那该怎么办?好吧,就像大多数语言一样,Solidity也有数组。

    数组Arrays

    Solidity中的数组可以具有编译时固定大小,也可以是动态的。

    uint[3] fixed;  //array of fixed length 3
    uint[] dynamic; //a dynamic array has no fixed size, it can keep growing
    

    还可以创建一个结构数组。使用以前创建的Voter结构:

    Voter[] voting;
    

    注意:将数组声明为public将自动为其创建getter方法。

    Voter[] public voting;
    

    映射mappings

    映射可以看作是哈希表,它们被虚拟地初始化,使得每个可能的键都存在并被映射到其字节表示全为零的值:类型的默认值。

    映射声明为:

    Mapping(_Keytype => _ValueType )
    

    注意:_Keytype几乎可以是任何类型,除了动态大小的数组,合约,枚举和结构。

    例:

    contract MappingExample {
    mapping(address => uint) public balances;
    function update(uint newBalance) {
    balances[msg.sender] = newBalance;  }}
    contract MappingUser {
    function f() returns (uint) {
    MappingExample m = new MappingExample();
    m.update(100);
    return m.balances(this);
    }}
    

    控制结构

    除了switch和goto之外,JavaScript中的大多数控制结构都在Solidity中可用。

    所以有:if,else,while,do,for,break,continue,return,? :,使用从C或JavaScript中已知的通常语义。

    注意:没有像C和JavaScript那样从非布尔类型到布尔类型的类型转换。

    现在让我们看看这些控制结构如何在Solidity中使用。

    contract ControlStructure {
    address public a;
    function ControlStructure>){
    // if-else can be used like this
    if(input1==2)
    a=1;
    else
    a=0;
    // while can be used like this
    while(input1>=0){
    if(input1==5)
    continue;
    input1=input1-1;
    a++;}
    // for loop can be used like this
    for(uint i=0;i<=50;i++) { a++; if(a==4) break; } //do while can be used like this do { a--; } (while a>0);
    // Conditional Operator can be used like this
    bool IsTrue = (a == 1)?true: false;
    /*will show an error because
    there is no type conversion from non-boolean to boolean
    */
    if(1)
    {
    }
    

    继续我们的Solidity教程博客,让我们谈谈合约中可执行的代码单元。这些被称为函数。

    函数

    以下是在Solidity中声明函数的方式。

    function sampleFunc(string name, uint amount) {
    }
    

    上面声明的是一个空体函数,它有两个参数:一个字符串和一个uint。

    可以这样调用此函数:

    sampleFunc("Shashank", 10000);
    

    谈到函数,Solidity还提供函数修饰符。

    函数修饰符

    它用于轻松更改函数的行为。甚至在进行函数调用之前也可以检查这些条件,因为它们已在智能合约的函数定义中声明。

    示例:如果要仅通过函数的所有者或创建者调用kill contract函数。

    contract FunctionModifiers{
    address public creator;
    function FunctionModifiers() {
    creator = msg.sender;}
    Modifier onlyCreator() {
    if(msg.sender!=creator){
    throw; }
    _; //resumes the function wherever the access modifier is used
    }
    function killContract() onlyCreator{ //function will not execute if an exception occurs
    self-destruct(creator); }}
    

    继承

    Solidity通过复制包含多态的代码来支持多重继承。

    contract Owned {
    address Owner ;
    function owned() {
    owner = msg.sender;
    }}
    contract Mortal is Owned {  // 'is' keyword is used for inheritance
    function kill(){
    self-destruct(owner);   }}
    contract User is Owned, Mortal //Multiple inheritance
    {
    string public UserName;
    function User(string _name){
    UserName = _name;
    }}
    

    好吧,我觉得上面讨论的概念足以让你开始使用Solidity编程。

    去写代码吧!!

    有了这个,我结束了这个Solidity Tutorial博客。我希望你喜欢阅读这篇博客并发现它内容丰富。到目前为止,必须对Solidity Programming Language的理解有所了解。现在去练习吧。

    如果希望快速进行以太坊开发,那请看我们精心打造的教程:

    以太坊入门教程,主要介绍智能合约与dapp应用开发,适合入门。

    其他区块链教程如下:

    • 以太坊开发进阶教程,主要是介绍使用node.js、mongodb、区块链、ipfs实现去中心化电商DApp实战,适合进阶。
    • java以太坊开发教程,主要是针对java和android程序员进行区块链以太坊开发的web3j详解。
    • python以太坊,主要是针对python工程师使用web3.py进行区块链以太坊开发的详解。
    • php以太坊,主要是介绍使用php进行智能合约开发交互,进行账号创建、交易、转账、代币开发以及过滤器和交易等内容。
    • C#以太坊,主要讲解如何使用C#开发基于.Net的以太坊应用,包括账户管理、状态与交易、智能合约开发与交互、过滤器和交易等。
    • php比特币开发教程,本课程面向初学者,内容即涵盖比特币的核心概念,例如区块链存储、去中心化共识机制、密钥与脚本、交易与UTXO等,同时也详细讲解如何在Php代码中集成比特币支持功能,例如创建地址、管理钱包、构造裸交易等,是Php工程师不可多得的比特币开发学习课程。
    • java比特币开发教程,本课程面向初学者,内容即涵盖比特币的核心概念,例如区块链存储、去中心化共识机制、密钥与脚本、交易与UTXO等,同时也详细讲解如何在Java代码中集成比特币支持功能,例如创建地址、管理钱包、构造裸交易等,是Java工程师不可多得的比特币开发学习课程。
    • EOS入门教程,本课程帮助你快速入门EOS区块链去中心化应用的开发,内容涵盖EOS工具链、账户与钱包、发行代币、智能合约开发与部署、使用代码与智能合约交互等核心知识点,最后综合运用各知识点完成一个便签DApp的开发。

    汇智网原创翻译,转载请标明出处。这里是原文

    展开全文
  • Solidity 官方文档中文版(一)

    千次阅读 2018-03-29 16:55:39
    中文翻译文档:http://www.tryblockchain.org/Solidity-%E8%AF%AD%E8%A8%80%E4%BB%8B%E7%BB%8D.html英文官方文档:http://solidity.readthedocs.io/en/develop/Solidity是一种智能合约高级语言,运行在Ethereum...

    中文翻译文档:http://www.tryblockchain.org/Solidity-%E8%AF%AD%E8%A8%80%E4%BB%8B%E7%BB%8D.html

    英文官方文档:http://solidity.readthedocs.io/en/develop/

    Solidity是一种智能合约高级语言,运行在Ethereum虚拟机(EVM)之上。

    Solidity与其它语言相关的特点?

    它的语法接近于Javascript,是一种面向对象的语言。但作为一种真正意义上运行在网络上的去中心合约,它又有很多的不同,下面列举一些:

    • 以太坊底层是基于帐户,而非UTXO的,所以有一个特殊的Address的类型。用于定位用户,定位合约,定位合约的代码(合约本身也是一个帐户)。
    • 由于语言内嵌框架是支持支付的,所以提供了一些关键字,如payable,可以在语言层面直接支持支付,而且超级简单。
    • 存储是使用网络上的区块链,数据的每一个状态都可以永久存储,所以需要确定变量使用内存,还是区块链。
    • 运行环境是在去中心化的网络上,会比较强调合约或函数执行的调用的方式。因为原来一个简单的函数调用变为了一个网络上的节点中的代码执行,分布式的感觉。
    • 最后一个非常大的不同则是它的异常机制,一旦出现异常,所有的执行都将会被回撤,这主要是为了保证合约执行的原子性,以避免中间状态出现的数据不一致。

    Hello Wolrd!

    听起来高大上,其实入手玩起来也可以很简单:

    pragma solidity ^0.4.0;
    
    contract HelloWorld{
        uint balance;
        function update(uint amount) returns (address, uint){
            balance += amount;
            return (msg.sender, balance);
        }
    }
    

    通过读取参数输入的新值,并将之累加至合约的变量中,返回发送人的地址,和最终的累计值。

    浏览器编译器Remix

    使用无需安装的浏览器编译器Remix可以立即看到效果。打开后,如下图所示:

    输入上述代码,点击Create按钮,就能在浏览器中创建能调用函数的按钮。在update按钮旁输入入参,点击按钮,就能执行函数调用并打印出函数返回的结果了。

    备注

    如果出现错误,可以等待浏览器资源加载完成,或强制刷新后再试。

    Solidity的完整语法:

    入门说明:

    值类型:

    引用类型:

    杂项:

    单位:

    语言内置特性:

    进阶:

    合约详解:

    其它:

    源文件映射

    作为AST输出的一部分,编译器会提供AST某个节点以应的源代码的范围。这可以被用来做基于AST的静态代码错误分析,可以高亮本地变量,和他们对应使用的调试工具。

    此外,编译器也可以生成字节码到生成指令源代码的范围映射。这对静态分析工具来说非常重要,它们在字节码级别分析,可以来在调试工具内显示对应代码位置,或支持断点操作。

    上述的源代码映射都使用整数来引用源代码。


    特殊特性(Esoteric Features)

    在Solidity的类型系统里面有一些类型有一些在其它语言中没有的语法。其中之一就是函数类型。但依然,使用var时,可以把函数类型作为本地变量。

    contract FunctionSelector {
      function select(bool useB, uint x) returns (uint z) {
        var f = a;
        if (useB) f = b;
        return f(x);
      }
    
      function a(uint x) returns (uint z) {
        return x * x;
      }
    
      function b(uint x) returns (uint z) {
        return 2 * x;
      }
    }
    

    可以对var赋值为不同的函数。


    内部机制

    内部机制 - 清理变量(Internals - Cleaning Up Variables)

    当一个值占用的位数小于32字节时,那些没有用到的位必须被清除掉。Solidity编译器设计实现为,在任何可能受到潜在的残存数据带来的副作用之前,清理掉这些脏数据。比如,在向内存写入一个值前,不需要的字节位需要被清除掉,因为没有用到的内存位可能被用来计算哈希,或作为消息调用的发送的数据存储。同样的,在向storage中存储时,未用到的字节位需要被清理掉,否则这些脏数据会带来意想不到的事情。

    另一方面,如果接下来的后述操作不会产生副作用,我们不会主动清理这些字节位。比如,由于任何非0的值被JUMP指令认为是true。在它作用JUMPI指令的条件前,我们在不会清理这个布尔值。

    在上述设计准则之外,Solidity编译器会在输入数据加载到栈上后清理掉它们。

    不同的类型,有不同的无效值的清理规则。

    类型有效值无效值意味着
    有n的成员的枚举类型0到(n - 1)异常(exception)
    布尔0或11
    有符号整数sign-extended word当前静默的包装了结果,以后会以异常的形式抛出来
    无符号整数高位节是0当前静默的包装了结果,以后会以异常的形式抛出来

    内部机制 - 优化(Internals - The Optimizer)

    Solidity是基于汇编优化的,所以它可以,同时也被其它编程语言所使用(译者注:其它语言编译为汇编)。编译器会在JUMPJUMPDEST处拆分基本的指令序列为一个个的基本块。在这些代码块内,所有的指令都被分析。所有的对栈,内存或存储的操作被记录成由指令及其参数组成的一个个表达式,这些表达式又会指向另一个表达式。核心目的是找到一些表达式在任何输入的情况下都恒等,然后将它们组合成一个表达式类。优化器首先尝试在一系列已知的表达式中,找出来一些全新的表达式。如果找不到,表达式通过一些简单的原则进行简化,比如 constant + constant = sum_of_constants 或 X * 1 = X。由于这一切是递归进行的,我们可以在第二项是一个更复杂的表达时,应用上述后续规则。对内存或存储的修改,存储的位置经常会被擦除,由此我们并不知道存的数据有什么不同:如果我们首先写入一个值x,再写入另一个值y,这两个都是输入变量,第二个写入时会覆盖第一个,所以我们实际在写入第二个值时,不知道第一个值是什么了。所以,如果一个简单的表达式x-y指向一个非0的常量,这样我们就能在操作y时知道x内存储的值。

    在流程最后,我们知道哪一个表达式会在栈顶,并且有一系列的对内存或存储的修改。这些信息与基本的块存在一起以方便的用来连接他们。此外,关于栈,存储和内存配置的信息会传递到下一个块。如果我们知道所有JUMP和JUMPI指令的目标,我们可以构建程序的完整的控制流程图。如果有任何一个我们不知道目标的跳转(因为目标是通过输入参数进行计算的,所以原则上可能发生),我们必须擦除块知识的输入,因为他有可能是某个跳转的目的地(译者注:因为可能某个跳转在运行时会指向他,修改他的状态,所以他的推算状态是错误的)。如果某个JUMPI被发现他的条件是常量,它会被转化为一个无状态的跳转。

    在最后一步,每个块中的代码都将重新生成。在某个块结束时,将生成栈上表达式的依赖树,不在这个树上的操作就被丢弃了。在我们原始代码中想要应用的对内存、存储想要的修改顺序的代码就生成出来了(被丢弃的修改被认为是完全不需要的),最终,生成了所有的需要在栈上存在的值。

    这些步骤应用于每个基本的块,如果新生成的代码更小,将会替换现有的代码。如果一个块在分析期间在JUMPI处分裂,条件被证实为一个常量,JUMPI将可以基于常量值被替换掉,比如下述代码:

    var x = 7;
    data[7] = 9;
    if (data[x] != x + 2)
      return 2;
    else
      return 1;
    

    简化的代码可以被编译为:

    data[7] = 9;
    return 1;
    

    尽管上述代码在一开始有一个跳转指令。

    调用数据的布局(Layout of CallData)

    当Solidity合约被部署后,从某个帐户调用这个合约,需要输入的数据是需要符合the ABI specification, 中文翻译见这里: http://me.tryblockchain.org/Solidity-abi-abstraction.htmlABI规范需要参数被填充为多个32字节。内部的函数调用,则使用了不同的约定。


    内存变量的布局(Layout in Memory)

    Solidity预留了3个32字节大小的槽位:

    • 0-64:哈希方法的暂存空间(scratch space)
    • 64-96:当前已分配内存大小(也称空闲内存指针(free memory pointer))

    暂存空间可在语句之间使用(如在内联编译时使用)

    Solidity总是在空闲内存指针所在位置创建一个新对象,且对应的内存永远不会被释放(也许未来会改变这种做法)。

    有一些在Solidity中的操作需要超过64字节的临时空间,这样就会超过预留的暂存空间。他们就将会分配到空闲内存指针所在的地方,但由于他们自身的特点,生命周期相对较短,且指针本身不能更新,内存也许会,也许不会被清零(zerod out)。因此,大家不应该认为空闲的内存一定已经是清零(zeroed out)的。


    状态变量的存储模型(Layout of State Variables in Storage)

    大小固定的变量(除了映射变长数组以外的所有类型)在存储(storage)中是依次连续从位置0开始排列的。如果多个变量占用的大小少于32字节,会尽可能的打包到单个storage槽位里,具体规则如下:

    • 在storage槽中第一项是按低位对齐存储(lower-order aligned)(译者注:意味着是大端序了,因为是按书写顺序。)。
    • 基本类型存储时仅占用其实际需要的字节。
    • 如果基本类型不能放入某个槽位余下的空间,它将被放入下一个槽位。
    • 结构体数组总是使用一个全新的槽位,并占用整个槽(但在结构体内或数组内的每个项仍遵从上述规则)

    优化建议

    当使用的元素占用少于32字节,你的合约的gas使用也许更高。这是因为EVM每次操作32字节。因此,如果元素比这小,EVM需要更多操作来从32字节减少到需要的大小。

    因为编译器会将多个元素打包到一个storage槽位,这样就可以将多次读或写组合进一次操作中,只有在这时,通过缩减变量大小来优化存储结构才有意义。当操作函数参数和memory的变量时,因为编译器不会这样优化,所以没有上述的意义。

    最后,为了方便EVM进行优化,尝试有意识排序storage的变量和结构体的成员,从而让他们能打包得更紧密。比如,按这样的顺序定义,uint128, uint128, uint256,而不是uint128, uint256, uint128。因为后一种会占用三个槽位。

    非固定大小

    结构体和数组里的元素按它们给定的顺序存储。

    由于它们不可预知的大小。映射变长数组类型,使用Keccak-256哈希运算来找真正数据存储的起始位置。这些起始位置往往是完整的堆栈槽。

    映射动态数组根据上述规则在位置p占用一个未满的槽位(对映射里嵌套映射,数组中嵌套数组的情况则递归应用上述规则)。对一个动态数组,位置p这个槽位存储数组的元素个数(字节数组和字符串例外,见下文)。而对于映射,这个槽位没有填充任何数据(但这是必要的,因为两个挨着的映射将会得到不同的哈希值)。数组的原始数据位置是keccak256(p);而映射类型的某个键k,它的数据存储位置则是keccak256(k . p),其中的.表示连接符号。如果定位到的值以是一个非基本类型,则继续运用上述规则,是基于keccak256(k . p)的新的偏移offset

    bytesstring占用的字节大小如果足够小,会把其自身长度和原始数据存在当前的槽位。具体来说,如果数据最多31位长,高位存数据(左对齐),低位存储长度lenght * 2。如果再长一点,主槽位就只存lenght * 2 + 1。原始数据按普通规则存储在keccak256(slot)

    所以对于接下来的代码片段:

    pragma solidity ^0.4.4;
    
    contract C {
        struct s { uint a; uint b; }
        uint x;
        mapping(uint => mapping(uint => s)) data;
    }
    

    按上面的代码来看,结构体从位置0开始,这里定义了一个结构体,但并没有对应的结构体变量,故不占用空间。uint x实际是uint256,单个占32字节,占用槽位0,所以映射data将从槽位1开始。

    data[4][9].b的位置在keccak256(uint256(9) . keccak256(uint256(4) . uint256(1))) + 1

    有人在这里尝试直接读取区块链的存储值,https://github.com/ethereum/solidity/issues/1550


    独立的汇编语言

    上面介绍的在Solidity中嵌入的内联汇编语言也可以单独使用。实际上,它是被计划用来作为编译器的一种中间语言。在这个目的下,它尝试达到下述的目标:

    1. 使用它编写的代码要可读,即使代码是从Solidity编译得到的。
    2. 从汇编语言转为字节码应该尽可能的少坑。
    3. 控制流应该容易检测来帮助进行形式验证与优化。

    为了达到第一条和最后一条的目标,Solidity汇编语言提供了高层级的组件比如,for循环,switch语句和函数调用。这样的话,可以不直接使用SWAP,DUP,JUMP,JUMPI语句,因为前两个有混淆的数据流,后两个有混淆的控制流。此外,函数形式的语句如mul(add(x, y), 7)比纯的指令码的形式7 y x add num更加可读。

    第二个目标是通过引入一个绝对阶段来实现,该阶段只能以非常规则的方式去除较高级别的构造,并且仍允许检查生成的低级汇编代码。Solidity汇编语言提供的非原生的操作是用户定义的标识符的命名查找(函数名,变量名等),这些都遵循简单和常规的作用域规则,会清理栈上的局部变量。

    作用域:一个标识符(标签,变量,函数,汇编)在定义的地方,均只有块级作用域(作用域会延伸到,所在块所嵌套的块)。跨函数边界访问局部变量是不合法的,即使可能在作用域内(译者注:这里可能说的是,函数内定义多个函数的情况,JavaScript有这种语法)。不允许shadowing。局部变量不能在定义前被访问,但标签,函数和汇编可以。汇编是非常特殊的块结构可以用来,如,返回运行时的代码,或创建合约。外部定义的汇编变量在子汇编内不可见。

    如果控制流来到了块的结束,局部变量数匹配的pop指令会插入到栈底(译者注:移除局部变量,因为局部变量失效了)。无论何时引用局部变量,代码生成器需要知道其当前在堆栈中的相对位置,因此需要跟踪当前所谓的堆栈高度。由于所有的局部变量在块结束时会被移除,因此在进入块之前和之后的栈高应该是不变的,如果不是这样的,将会抛出一个警告。

    我们为什么要使用高层级的构造器,比如switchfor和函数。

    使用switchfor和函数,可以在不用jumpjumpi的情况下写出来复杂的代码。这会让分析控制流更加容易,也可以进行更多的形式验证及优化。

    此外,如果手动使用jumps,计算栈高是非常复杂的。栈内所有的局部变量的位置必须是已知的,否则指向本地变量的引用,或者在块结束时自动删除局部变量都不会正常工作。脱机处理机制正确的在块内不可达的地方插入合适的操作以修正栈高来避免出现jump时非连续的控制流带来的栈高计算不准确的问题。

    示例:

    我们从一个例子来看一下Solidity到这种中间的脱机汇编结果。我们可以一起来考虑下下述Soldity程序的字节码:

    contract C {
      function f(uint x) returns (uint y) {
        y = 1;
        for (uint i = 0; i < x; i++)
          y = 2 * y;
      }
    }
    

    它将生成下述的汇编内容:

    {
      mstore(0x40, 0x60) // store the "free memory pointer"
      // function dispatcher
      switch div(calldataload(0), exp(2, 226))
      case 0xb3de648b {
        let (r) = f(calldataload(4))
        let ret := $allocate(0x20)
        mstore(ret, r)
        return(ret, 0x20)
      }
      default { revert(0, 0) }
      // memory allocator
      function $allocate(size) -> pos {
        pos := mload(0x40)
        mstore(0x40, add(pos, size))
      }
      // the contract function
      function f(x) -> y {
        y := 1
        for { let i := 0 } lt(i, x) { i := add(i, 1) } {
          y := mul(2, y)
        }
      }
    }
    

    在经过脱机汇编阶段,它会编译成下述的内容:

    {
      mstore(0x40, 0x60)
      {
        let $0 := div(calldataload(0), exp(2, 226))
        jumpi($case1, eq($0, 0xb3de648b))
        jump($caseDefault)
        $case1:
        {
          // the function call - we put return label and arguments on the stack
          $ret1 calldataload(4) jump(f)
          // This is unreachable code. Opcodes are added that mirror the
          // effect of the function on the stack height: Arguments are
          // removed and return values are introduced.
          pop pop
          let r := 0
          $ret1: // the actual return point
          $ret2 0x20 jump($allocate)
          pop pop let ret := 0
          $ret2:
          mstore(ret, r)
          return(ret, 0x20)
          // although it is useless, the jump is automatically inserted,
          // since the desugaring process is a purely syntactic operation that
          // does not analyze control-flow
          jump($endswitch)
        }
        $caseDefault:
        {
          revert(0, 0)
          jump($endswitch)
        }
        $endswitch:
      }
      jump($afterFunction)
      allocate:
      {
        // we jump over the unreachable code that introduces the function arguments
        jump($start)
        let $retpos := 0 let size := 0
        $start:
        // output variables live in the same scope as the arguments and is
        // actually allocated.
        let pos := 0
        {
          pos := mload(0x40)
          mstore(0x40, add(pos, size))
        }
        // This code replaces the arguments by the return values and jumps back.
        swap1 pop swap1 jump
        // Again unreachable code that corrects stack height.
        0 0
      }
      f:
      {
        jump($start)
        let $retpos := 0 let x := 0
        $start:
        let y := 0
        {
          let i := 0
          $for_begin:
          jumpi($for_end, iszero(lt(i, x)))
          {
            y := mul(2, y)
          }
          $for_continue:
          { i := add(i, 1) }
          jump($for_begin)
          $for_end:
        } // Here, a pop instruction will be inserted for i
        swap1 pop swap1 jump
        0 0
      }
      $afterFunction:
      stop
    }
    

    汇编有下面四个阶段:

    1. 解析
    2. 脱汇编(移除switch,for和函数)
    3. 生成指令流
    4. 生成字节码

    我们将简单的以步骤1到3指定步骤。更加详细的步骤将在后面说明。

    解析、语法

    解析的任务如下:

    • 将字节流转为符号流,去掉其中的C++风格的注释(一种特殊的源代码引用的注释,这里不打算深入讨论)。
    • 将符号流转为下述定义的语法结构的AST。
    • 注册块中定义的标识符,标注从哪里开始(根据AST节点的注解),变量可以被访问。

    组合词典遵循由Solidity本身定义的词组。

    空格用于分隔标记,它由空格,制表符和换行符组成。 注释是常规的JavaScript / C ++注释,并以与Whitespace相同的方式进行解释。

    语法:

    AssemblyBlock = '{' AssemblyItem* '}'
    AssemblyItem =
        Identifier |
        AssemblyBlock |
        FunctionalAssemblyExpression |
        AssemblyLocalDefinition |
        FunctionalAssemblyAssignment |
        AssemblyAssignment |
        LabelDefinition |
        AssemblySwitch |
        AssemblyFunctionDefinition |
        AssemblyFor |
        'break' | 'continue' |
        SubAssembly | 'dataSize' '(' Identifier ')' |
        LinkerSymbol |
        'errorLabel' | 'bytecodeSize' |
        NumberLiteral | StringLiteral | HexLiteral
    Identifier = [a-zA-Z_$] [a-zA-Z_0-9]*
    FunctionalAssemblyExpression = Identifier '(' ( AssemblyItem ( ',' AssemblyItem )* )? ')'
    AssemblyLocalDefinition = 'let' IdentifierOrList ':=' FunctionalAssemblyExpression
    FunctionalAssemblyAssignment = IdentifierOrList ':=' FunctionalAssemblyExpression
    IdentifierOrList = Identifier | '(' IdentifierList ')'
    IdentifierList = Identifier ( ',' Identifier)*
    AssemblyAssignment = '=:' Identifier
    LabelDefinition = Identifier ':'
    AssemblySwitch = 'switch' FunctionalAssemblyExpression AssemblyCase*
        ( 'default' AssemblyBlock )?
    AssemblyCase = 'case' FunctionalAssemblyExpression AssemblyBlock
    AssemblyFunctionDefinition = 'function' Identifier '(' IdentifierList? ')'
        ( '->' '(' IdentifierList ')' )? AssemblyBlock
    AssemblyFor = 'for' ( AssemblyBlock | FunctionalAssemblyExpression)
        FunctionalAssemblyExpression ( AssemblyBlock | FunctionalAssemblyExpression) AssemblyBlock
    SubAssembly = 'assembly' Identifier AssemblyBlock
    LinkerSymbol = 'linkerSymbol' '(' StringLiteral ')'
    NumberLiteral = HexNumber | DecimalNumber
    HexLiteral = 'hex' ('"' ([0-9a-fA-F]{2})* '"' | '\'' ([0-9a-fA-F]{2})* '\'')
    StringLiteral = '"' ([^"\r\n\\] | '\\' .)* '"'
    HexNumber = '0x' [0-9a-fA-F]+
    DecimalNumber = [0-9]+
    

    脱汇编

    一个AST转换,移除其中的forswitch和函数构建。结果仍由同一个解析器,但它不确定使用什么构造。如果添加仅跳转到并且不继续的jumpdests,则添加有关堆栈内容的信息,除非没有局部变量访问到外部作用域或栈高度与上一条指令相同。伪代码如下:

    desugar item: AST -> AST =
    match item {
    AssemblyFunctionDefinition('function' name '(' arg1, ..., argn ')' '->' ( '(' ret1, ..., retm ')' body) ->
      <name>:
      {
        jump($<name>_start)
        let $retPC := 0 let argn := 0 ... let arg1 := 0
        $<name>_start:
        let ret1 := 0 ... let retm := 0
        { desugar(body) }
        swap and pop items so that only ret1, ... retm, $retPC are left on the stack
        jump
        0 (1 + n times) to compensate removal of arg1, ..., argn and $retPC
      }
    AssemblyFor('for' { init } condition post body) ->
      {
        init // cannot be its own block because we want variable scope to extend into the body
        // find I such that there are no labels $forI_*
        $forI_begin:
        jumpi($forI_end, iszero(condition))
        { body }
        $forI_continue:
        { post }
        jump($forI_begin)
        $forI_end:
      }
    'break' ->
      {
        // find nearest enclosing scope with label $forI_end
        pop all local variables that are defined at the current point
        but not at $forI_end
        jump($forI_end)
        0 (as many as variables were removed above)
      }
    'continue' ->
      {
        // find nearest enclosing scope with label $forI_continue
        pop all local variables that are defined at the current point
        but not at $forI_continue
        jump($forI_continue)
        0 (as many as variables were removed above)
      }
    AssemblySwitch(switch condition cases ( default: defaultBlock )? ) ->
      {
        // find I such that there is no $switchI* label or variable
        let $switchI_value := condition
        for each of cases match {
          case val: -> jumpi($switchI_caseJ, eq($switchI_value, val))
        }
        if default block present: ->
          { defaultBlock jump($switchI_end) }
        for each of cases match {
          case val: { body } -> $switchI_caseJ: { body jump($switchI_end) }
        }
        $switchI_end:
      }
    FunctionalAssemblyExpression( identifier(arg1, arg2, ..., argn) ) ->
      {
        if identifier is function <name> with n args and m ret values ->
          {
            // find I such that $funcallI_* does not exist
            $funcallI_return argn  ... arg2 arg1 jump(<name>)
            pop (n + 1 times)
            if the current context is `let (id1, ..., idm) := f(...)` ->
              let id1 := 0 ... let idm := 0
              $funcallI_return:
            else ->
              0 (m times)
              $funcallI_return:
              turn the functional expression that leads to the function call
              into a statement stream
          }
        else -> desugar(children of node)
      }
    default node ->
      desugar(children of node)
    }
    

    生成操作码流

    在操作码流生成期间,我们在一个计数器中跟踪当前的栈高,所以通过名称访问栈的变量是可能的。栈高在会修改栈的操作码后或每一个标签后进行栈调整。当每一个新局部变量被引入时,它都会用当前的栈高进行注册。如果要访问一个变量(或者拷贝其值,或者对其赋值),会根据当前栈高与变量引入时的当时栈高的不同来选择合适的DUPSWAP指令。

    伪代码:

    codegen item: AST -> opcode_stream =
    match item {
    AssemblyBlock({ items }) ->
      join(codegen(item) for item in items)
      if last generated opcode has continuing control flow:
        POP for all local variables registered at the block (including variables
        introduced by labels)
        warn if the stack height at this point is not the same as at the start of the block
    Identifier(id) ->
      lookup id in the syntactic stack of blocks
      match type of id
        Local Variable ->
          DUPi where i = 1 + stack_height - stack_height_of_identifier(id)
        Label ->
          // reference to be resolved during bytecode generation
          PUSH<bytecode position of label>
        SubAssembly ->
          PUSH<bytecode position of subassembly data>
    FunctionalAssemblyExpression(id ( arguments ) ) ->
      join(codegen(arg) for arg in arguments.reversed())
      id (which has to be an opcode, might be a function name later)
    AssemblyLocalDefinition(let (id1, ..., idn) := expr) ->
      register identifiers id1, ..., idn as locals in current block at current stack height
      codegen(expr) - assert that expr returns n items to the stack
    FunctionalAssemblyAssignment((id1, ..., idn) := expr) ->
      lookup id1, ..., idn in the syntactic stack of blocks, assert that they are variables
      codegen(expr)
      for j = n, ..., i:
      SWAPi where i = 1 + stack_height - stack_height_of_identifier(idj)
      POP
    AssemblyAssignment(=: id) ->
      look up id in the syntactic stack of blocks, assert that it is a variable
      SWAPi where i = 1 + stack_height - stack_height_of_identifier(id)
      POP
    LabelDefinition(name:) ->
      JUMPDEST
    NumberLiteral(num) ->
      PUSH<num interpreted as decimal and right-aligned>
    HexLiteral(lit) ->
      PUSH32<lit interpreted as hex and left-aligned>
    StringLiteral(lit) ->
      PUSH32<lit utf-8 encoded and left-aligned>
    SubAssembly(assembly <name> block) ->
      append codegen(block) at the end of the code
    dataSize(<name>) ->
      assert that <name> is a subassembly ->
      PUSH32<size of code generated from subassembly <name>>
    linkerSymbol(<lit>) ->
      PUSH32<zeros> and append position to linker table
    }
    

    感谢您的支持


    展开全文
  • Solidity编程 - 构建以太坊和区块链智能合约的初学者指南
  • 区块链开发技术进阶-深入详解以太坊智能合约语言 solidity视频培训教程:本课程是国内全面介绍智能合约语言的课程,内容包括钱包、以太坊网络、货币单位、交易等区块链核心概念讲解;开发环境搭建、智能合约、...
  • Solidity学习笔记

    2019-04-27 14:27:49
    Solidity是一门静态类型语言,支持继承、库和复杂的用户自定义类型等特性。 二、基础语法 1. 地址(address) 以太坊中的地址为160位,即20个字节大小,所以可以用一个uint160表示。 eip-55:有关验证地址的...
    • 一、基本介绍

      Solidity是一门静态类型语言,支持继承、库和复杂的用户自定义类型等特性。

      二、基础语法

      1. 地址(address)

      • 以太坊中的地址为160位,即20个字节大小,所以可以用一个uint160表示。
      • eip-55:有关验证地址的合法性
      • 在合约内部,this表示当前合约本身,由于合约也是一个address类型(contract是从address继承的),所以this其实也是address类型的值。

      2. 类型转换

      (1) 隐式转换

      (1)无符号整数(uint)可以转换为相同或更大尺寸字节类型(bytes),但是反过来不可以转换。
      (2)任何可以转换为uint160类型的变量都可以转换为地址类型(address)

      pragma solidity ^0.4.17;
      
      contract convert{
          int8  a = 1;
          int16  b = 2;
          // 返回的类型必须是int16,如果返回int8类型会报错,这边运算符已经帮我们隐式转换了类型
          function test() public view returns(int16)
          {
          // a+b 的结果会隐式的转换为int16
              return a + b;
          }
      }
      

      (2)显式转换

      int8  public  c = -2;
      uint8  public  d = uint8(c);
      // 此时的d为254
      uint  public e = uint(c);
      // e = 115792089237316195423570985008687907853269984665640564039457584007913129639934

      3. 发送以太币的两种方法

      (1)transfer

      transfer从合约发起方向某个地址转入以太币,当地址无效或者发起方余额不足时,transfer将抛出异常

      // 向addressA转入一个以太币
      addressA.transfer(1 ether);
      
      // or // 附带 gas
      addressA.transfer.gas(120000)(1 ether);
      

      (2)send

      send是transfer的低级版本。当合约执行失败时,send会返回false。当转账成功,则返回true。

      owner.send(SOME_BALANCE); // 失败时返回false
      
      if(owner.send(SOME_BALANCE)){
          ...
      }
      

      使用send时需注意以下三点:

      • (1)send方法需包裹在if中,因为在调用send函数时,合约可能会有函数执行,这些函数可能会执行失败
      • (2)在用send方法发送以太币之前,请先执行减少账户余额的操作,因为可能会有递归调用消耗完合约余额的风险。
      • (3)用户可以重载send方法。

      总结:
      x.transfer(y)等价于if(!x.send(y)) throw; ,send是transfer的底层实现,建议尽可能使用transfer。

      4. 字节数组

      (1)固定长字节数组

      固定长度字节数组是以bytes加上数字后缀的方式定义的。

      byte a;  // byte 等同于bytes1 a
      bytes2 b;
       ...
      bytes32 c;
      

      索引访问:bytes1~bytes32 支持索引访问,但是这种索引访问是只读的不能使用进行赋值

      bytes10  b
      b[0]   // 获取第1个字节
      b[1]   // 获取第2个字节
      ...
      b[9]   // 获取第9个字节
      
      b[0] = x  // 不能使用索引的形式进行赋值,因为这种索引访问是只读的。
      

      可以将 byte[] 当作字节数组使用,但这种方式非常浪费存储空间,准确来说,是在传入调用时每个元素会浪费 31 字节。 更好地做法是使用 bytes

      (2)动态长度字节数组

      bytes:动态长度字节数组

      一种特殊的数组。bytes类似于byte[],在外部函数作为参数时,会进行压缩打包以便节省空间,所以尽量用bytes。

      bytes m;
      

      string:动态长度字符数串

      • (1)字符串以UTF-8的形式编码
      • (2)类似bytes,但不提供长度和按索引的访问方式。
      // 字符串是双引号
      string n = "hello";
      
      // 不支持长度访问
      n.length
      
      // 不支持按索引访问
      n[0]
      
      • (1) string不支持通过索引访问,但可以通过string类型的值转换为bytes类型的值,然后就可以使用索引访问字符串的特定字节。
      • (2) 由于bytes类型的值是可读写的,所以要修改string类型的值,可以先将string类型的值转换为bytes类型的值,修改完后,再将bytes类型的值转换为string类型的值。
      value2 = typename ( value1 );  //类型转换
      

      其中typename表示类型名,如string、bytes等。

      string类型有一些缺陷:如,不能直接使用加号(+)进行连接,但可以通过bytes类型间接将两个或多个字符串连接起来。

      字符串连接的基本原理

      • (1)创建一个尺寸与所有参与连接的字符串尺寸之和相同的大字符串;
      • (2)将该大字符串转换为bytes类型;
      • (3)依次迭代参与连接的字符串,将字符串中的字符逐一添加到bytes类型的值中;
      • (4)将这个bytes类型的值转换为string类型的值。
          // internal表示函数只能被合约内部调用,函数在合约外不可见
          function strConcat(string memory _str1,string memory _str2) internal pure returns(string memory) {
              // 先将string转化为bytes类型的值
              bytes memory _bytesValue1 = bytes(_str1);
              bytes memory _bytesValue2 = bytes(_str2);
              // 创建一个能容纳_str1和_str2的string对象
              string memory resultStr = new string(_bytesValue1.length + _bytesValue2.length);
              // 创建与_str1和_str2总和同样尺寸的bytes对象
              bytes memory resultBytes = bytes(resultStr);
      
              uint index = 0;
      
              for(uint i=0;i<_bytesValue1.length;i++){
                  resultBytes[index++] = _bytesValue1[i];
              }
      
              for(uint i=0;i<_bytesValue2.length;i++){
                  resultBytes[index++] = _bytesValue2[i];
              }
      
              return string(resultBytes);
          }
      

      5. 枚举类型(enum)

      枚举是Solidity中的自定义数据类型。枚举可以显式转为整型,但是不能与整型隐式转换,枚举在一般程序中可以当作状态机使用。

      // 定义枚举类型(类型名为enumName)
      enum enumName{ value1, value2, ... , valueN}
      

      例如:

       // 定义一个枚举类型名为Country的枚举类型,每一个枚举值都对应一个整数索引,China表示0,America表示1,以此类推。(不用添加分号)
          enum Country {China,America,Japan,Australia,Canada,South_Korea}
          
          // 定义枚举Country类型的变量
          Country country;
          
          // 赋值
          country = Country.China;   //使用枚举值进行赋值
          // or
          country = Country(0);   //使用整数值进行赋值(必须显式类型转换)
          
          // 枚举可以显式转化为整型
          uint currentCountry = uint(Country.China);  // 0
      

      注意:

      • (1)在合约中可以使用枚举值设置枚举变量,也可以使用整数值设置枚举变量,后者必须显式类型转换
      • (2)在remix环境中测试智能合约时,再输入测试数据时不能直接输入Country.China或其他枚举值,而需要输入整数,如0,1,2,3等。

      6. 函数类型

      • (1)函数声明的类型称为函数类型。(注意:强调的是类型,类似我们常见的uint,string类型)
      • (2)函数类型可以作为函数参数类型和函数返回值类型。
      • (3)只要函数的参数个数参数类型函数返回值函数类型一样,该函数就可以赋给一个函数类型变量
      //func是一个函数类型变量
      function (uint,uint) returns(uint) func; 
      function add(uint x, uint y) public returns(uint){
      return x+y;
      }
      function test() public{
      // 将add函数赋给函数类型变量func
      func = add;
      }
      

      7. 函数和状态变量的4种可见性(访问权限)

      (1)访问权限

      (1) 如果不为函数指定访问权限,默认为public
      (2) 合约的状态变量不能用external修饰,否则无法编译通过。
      (3) 如果不为状态变量指定访问权限,默认为internal

      4类可见性(访问权限) 指定:

      • public:智能合约外部和内部都可使用的方法;

      用public声明的状态变量,会自动产生一个getter函数

      • internal智能合约(包括派生合约,子合约)内部才可调用的方法;internal修饰的状态变量只能通过在内部的方式访问;
      • external:可通过其他合约和交易进行调用的方法;

      (1)external函数不能直接调用,前面需要加this, 如this.func( )。
      (2)在接收大量数据时,external函数有时更有效率。

      • private:只有在定义的合约中才可以调用,即使派生的合约也无法调用;用private修饰的状态变量也是如此,只在当前合约中可以访问(子合约也不能访问)。

      (2)getter函数

      如果合约的状态变量使用public修饰,Solidity编译器会自动为状态变量生成一个与状态变量同名getter函数用于获取状态变量的值

      • (1)如果状态变量的数据类型比较复杂,如mapping,生成的getter函数会带一些参数。
      • (2)尽管自动生成的getter函数使用external修饰,但是不能在合约内使用this调用。(尽管不会产生编译错误,但是无法成功部署合约)
      • (3)在合约内部,getter函数会解析为状态变量。

      例如,如果状态变量persons是mapping类型,在合约内部应该使用persons[key], 而不是this.persons(key)形式。
      不过在合约外部必须使用getter函数形式引用persons。(如:my.persons(key), my为创建的合约对象名,persons(key)即为状态变量persons的getter函数形式)

      pragma solidity >=0.4.20 <=0.7.0;
      
      contract MyContract{
          uint public data = 115;
          string public personName;
          uint public personAge;
          struct Person{
              string name;
              uint age;
          }
          mapping(uint=>Person) public persons;
      
          constructor() public{
              data = 200;
              // 创建Person结构体的实例
              Person memory person = Person({
                  name:"Lebron James",
                  age:34
              });
              // 将person添加到persons映射中
              persons[10] = person;
              // 在合约内部不能使用persons的getter函数形式引用persons映射,
              // 所以尽管下面的代码编译不会出错,但无法成功部署在以太坊上。
              // (string memory name,uint age) = this.persons(10);
              
              string memory name = persons[10].name;
              uint age = persons[10].age;
              personName = name;
              personAge = age;
          }
      }
      
      contract GetterContract{
          MyContract my = new MyContract();
          function getData() public view returns(uint){
              // 调用MyContract合约中的data状态变量对应的getter函数(data函数)
              return my.data();
          }
      
          function getPerson(uint id) public view returns(string memory,uint){
              // 调用MyContract合约中persons状态变量对应的getter函数(persons函数)
              // 该函数返回了多个值,这些值都是Person结构体的成员,
              // 如果这个结构体的某个成员的数据类型无法通过函数返回(如mapping),那么系统就会忽略这个结构体成员。
              (string memory name,uint age) = my.persons(id);
              return (name,age);
          }
      }
      

      8. 数组(稍有不同)

      (1)不同之处

      solidity中数组与大多数语言稍有不同。具体如下:

      // 定义一个j行i列的二维数组(注意:定义的时候列在前面,行在后面)
      int[i][j]  arrayName;
      // 为数组arrayName的第m行第n列元素赋值,赋值为20.( 注意:赋值的时候,行在前面,列在后面)
      arrayName[m][n] = 20;
      
      • (1)定义的时候列在前面,行在后面
      • (2)赋值的时候,行在前面,列在后面

      (2)注意事项(一)

      • 对于storage数组,可以保存任意类型的数据,包括另一个数组、映射或结构体。
      • 但对于memory数组,不能存储映射类型的数据
      • 如果作为 public 函数的参数,它只能是 ABI 类型

      (3)注意事项(二)

      • (1) 如果你在一个空数组中使用.length,这将会造成向下溢出(不减小反而增大),导致长度变为2^256-1。
      • (2) 增加一个storage数组的长度花费的gas成本是一个常量值,因为storage变量被当作是zero-initialised(领初始化)的;
        而减少storage数组的长度花费的gas成本至少是线性增长的(但事实上,大多数都要高于线性增长),因为其包含了显式清除要被删除的元素(类似于调用delete方法)。
      • (3) 外部函数中暂时还不支持使用多维数组(但在public函数是支持的)。

      (4)数组成员

      length

      数组的成员变量length表示当前数组的长度。

      • (1)动态数组可以在storage中通过改变成员变量 .length 改变数组大小(在memory中是不可以的)。
      • (2)并不能通过访问超出当前数组长度的方式实现自动扩展数组的长度
      • (3)一经创建,memory数组的大小就是固定的(但却是动态的,也就是说,它依赖于运行时的参数)。
      • (4)如果你尝试改变一个不在storage中的非动态数组的大小,你将会收到一个“Value must be an Ivalue”的错误。

      push

      storage的动态数组以及 bytes类型(字节数组)都有一个叫做 push 的成员函数,它用来添加新的元素到数组末尾。 这个函数将返回新的数组长度

      注意:string即字节数组是没有push方法的

      pop

      storage的动态数组和bytes数组(字节数组)都有一个叫做pop的成员函数,用于从数组的末尾删除元素。
      其在删除元素的时候隐式地调用了delete方法。

      注意:string即字节数组是没有pop方法的

      (5)实例

      pragma solidity >=0.4.16 <0.7.0;
      
      contract ArrayContract {
          uint[2**20] m_aLotOfIntegers; // 数组大小为2的20次方
          // m_pairsOfFlags不是一对动态数组,而是一个数组元素为两个变量的动态数组(说白了就是其每个元素是一个长度为2的数组)
          bool[2][] m_pairsOfFlags; // 列数为2,行数为动态的
      
          // newPairs是一个数组元素为两个bool类型变量的动态数组(其每个元素是一个包含两个bool变量的数组)
          function setAllFlagPairs(bool[2][] memory newPairs) public {
              // 将newPairs数组赋值给storage数组的m_pairsOfFlags,m_pairsOfFlags的值将会被newPairs中的值替换。
              m_pairsOfFlags = newPairs;
          }
      
          struct StructType {
              uint[] contents;
              uint moreInfo;
          }
          StructType s;
      
          function f(uint[] memory c) public {
              // 将类型为StructType结构体变量s的指针(引用)赋值给g
              StructType storage g = s;
              // 改变结构体变量g中的成员属性值,其实也在改变s中的成员属性值(因为s和g指向同一块数据区域)
              g.moreInfo = 2;
              // 将c的值赋值给g.contents(虽然g.contents不是一个局部变量,但它是某个局部变量的一个成员)
              g.contents = c;
          }
      
          function setFlagPair(uint index, bool flagA, bool flagB) public {
              // 访问一个不存在数组下标会抛异常
              m_pairsOfFlags[index][0] = flagA; // 将flagA赋值给第index行第0列的元素
              m_pairsOfFlags[index][1] = flagB; // 将flagB赋值给第index行第1列的元素
          }
      
          function changeFlagArraySize(uint newSize) public {
              // 如果所赋给的新长度值小于原数组长度值,则会把原数组在新长度之外的元素删除。
              m_pairsOfFlags.length = newSize;
          }
      
          function clear() public {
              // 将数组清空
              delete m_pairsOfFlags;
              delete m_aLotOfIntegers;
              // 与上面效果相同(清空数组)
              m_pairsOfFlags.length = 0;
          }
      
          bytes m_byteData;
      
          function byteArrays(bytes memory data) public {
              // 字节数组(bytes)是不一样的,因为它们不是填充式存储,但是它们可以被当作和uint8[]一样对待。
              m_byteData = data;
              m_byteData.length += 7;
              m_byteData[3] = 0x08;
              delete m_byteData[2];
          }
      
          function addFlag(bool[2] memory flag) public returns (uint) {
              return m_pairsOfFlags.push(flag); // 向二维动态数组添加新元素(这里添加的元素是一个长度为2的数组),给二维数组增加一行
          }
      
          function createMemoryArray(uint size) public pure returns (bytes memory) {
              // 使用new关键字进行动态数组的创建
              uint[2][] memory arrayOfPairs = new uint[2][](size);
      
              // 内联数组总是静态大小的,如果你只是使用字面量,则你必须提供至少一种类型。
              arrayOfPairs[0] = [uint(1), 2];
      
              // 创建一个动态数组
              bytes memory b = new bytes(200);
              for (uint i = 0; i < b.length; i++)
                  b[i] = byte(uint8(i));
              return b;
          }
      }
      

      参考:Array

      9. 结构体(struct)

      (1)定义

      结构体用于自定义数据类型,结构体成员可以是任何数据类型,甚至可以是结构体本身。

      • (1)结构体可用于函数返回值,但是要在智能合约内部调用,否则会抛出异常。
      • (2)如果要返回结构体中成员的值,可以使用返回多个值的函数

      (2)实例

      pragma solidity >=0.4.16 <= 0.7.0;
      
      contract StructContract_1{
          // 定义结构体类型
          struct Job{
              uint id;
              string name;
              string company;
          }
      
          struct Person{
              uint id;
              string name;
              uint age;
              Job job; // 结构体类型中引用结构体变量(结构体变量作为结构体类型的成员)
          }
          // Job public job;
          // 声明一个Person类型的变量
          Person  person;
          
          // 初始化结构体 
          // 方法一:按照结构体中命名参数进行初始化
          Person personA = Person({
              id:10002,
              name:"Kobe Bryant",
              age:39,
              job:Job({   //结构体中包含结构体
                  id:102,
                  name:"Basketball Player",
                  company:"NBA"
              })
          });
          // 方法二:按照结构体中定义的顺序初始化
          Job jobA = Job(103,"NBA Retired Players","Home");
          Person personB = Person(10003,"Dwyane Wade",36,Job(104,"LiNing Spokeman","LiNing"));
          Person personC = Person(10004,"Chris Bosh",35,jobA);
          //通过构造函数初始化结构体类型变量
          constructor (uint personId,string memory name,uint age) public{
              // 初始化结构体变量
              Job memory job = Job({
                  id:101,
                  name:"Software Engineer",
                  company:"Google"
              });
      
             person = Person({
                  id:personId,
                  name:name,
                  age:age,
                  job:job
              });
          }
          // 修改工作属性(修改结构体变量的值)
          function setJob(string memory jobName,string memory company) public{
              // job.name = jobName;
              // job.company = company;
              person.job.name = jobName;
              person.job.company = company;
              // 重置为初始值,把struct中的所有变量的值设置为0,除了mapping类型
              // delete person; //也须写在函数内部
          }
      
          // 要用结构体当作返回值,必须将函数定义为internal,即合约内部可见(函数仅在合约内部可调用)
          // 必须在内部调用(需要使用internal声明函数),否则会抛出异常
          function getPerson() internal view returns(Person memory){
              return person; // 返回构造体类型的值
          }
          
          // 获取人员的姓名、年龄、工作等信息(获取结构体的成员值)
          function callGetPerson() public  returns(string memory,uint,string memory,string memory){
              person = getPerson();
              return (person.name,person.age,person.job.name,person.job.company);
          }
      }
      // 1,"Lebron James",34    "BasketBall Player","NBA"
      

      10. 映射(mapping)

      (1)定义

      映射与字典类似,通过key获取对应的value值

      • key:可以是除了映射外的任何数据类型;
      • value:任何数据类型;
      mapping(keyType=>valueType) varName;
      

      (2)实例

      pragma solidity >=0.4.16 <=0.7.0;
      
      contract MappingContract{
          //声明映射类型的变量names
          mapping(uint=>string) public names;
          // 定义Person结构体类型
          struct Person{
              string name;
              uint age;
              string job;
          }
          //声明映射类型的变量persons
          mapping(uint=>Person) public persons;
          // 通过合约的构造函数向映射变量names添加值
          constructor (uint id,string memory name) public{
              names[id] = name; //映射变量的赋值
          }
          // 根据key值从映射类型变量中获取相应的value值
          function getValue(uint id) public view returns(string memory){
              return names[id];
          }
          // 向映射类型变量中添加值
          function addPerson(uint id,string memory name,uint age,string memory job) public{
              // 先初始化结构体
              Person memory person = Person({
                  name:name,
                  age:age,
                  job:job
              });
      
              persons[id] = person; //增加一个person(向映射类型变量中添加值)
          }
          // 根据id(key)从persons映射获取Person对象,并通过返回多值函数返回Person结构体的成员
          
          function getPerson(uint id) public view returns(string memory name,uint age,string memory job){
              // 返回多个值
              // 方法一:多返回值函数可以通过定义具体的函数返回值接收多个返回值,而不使用return关键字
              name = persons[id].name;
              age = persons[id].age;
              job = persons[id].job;
              // 方法二:使用return关键字(多个返回值,需用括号括起来)
              // return (persons[id].name,persons[id].age,persons[id].job);
          }
      }
      // 测试数据
      // 1001,"Lebron James"
      // 1002,"Dwyane Wade",36,"NBA Player"
      // 1003,"Kobe Bryant",39,"World Cup Spokeman"
      

      实例中有提到两种不同的方式返回多个值

      11.函数参数和函数返回值

      (1)函数参数

      在函数中,如果某个参数未使用只需保留参数类型,参数名可以省略。

      (2)函数返回值

      函数返回值可以直接指定返回值类型,也可以为返回值指定变量名,声明返回值类型的方式与声明函数参数的方式相同,所以也可以将函数返回值称为函数输出和参数

      • 返回值类型要使用returns指定,多个返回值类型中间用逗号( , )分隔;
      • 如果为函数返回值指定变量名可以不使用return返回,直接为函数输出参数变量赋值即可。

      返回多个值的两种方法

      • 方法一: 函数可以通过设置多个具体的函数返回值变量接收多个返回值,而不使用return关键字,就可实现多个值的返回。
       function getPerson(uint id) public view returns(string memory name,uint age,string memory job){
              name = persons[id].name;
              age = persons[id].age;
              job = persons[id].job;
          }
      }
      
      • 方法二: 使用return关键字(多个返回值,需用括号括起来)
       function getPerson(uint id) public view returns(string memory, uint ,string memory){
              return (persons[id].name,persons[id].age,persons[id].job);
          }
      }
      

      12. 调用其他合约中的函数

      (1)定义

      当前合约中的函数调用其他合约中的函数的两个前提条件:

      • (1)被调用函数所在的合约必须已经成功部署在以太坊网络上(或在本地的测试环境)。
      • (2)需要知道被调用函数所在的合约的地址

      (2)实例

      // CallOtherContract.sol
      pragma solidity >=0.4.16 <=0.7.0;
      
      /**
      注意:
       (1)在部署FunCallContract之前,必须先部署FactorialContract合约,否则就无法获得FactorialContract的地址。
       (2)部署完FactorialContract合约之后,将FactorialContract合约的地址作为FunCallContract合约的构造参数
             传入FunCallContract合约,然后部署FunCallContract合约。
       */
      
      // 用于计算阶乘的合约
      contract FactorialContract{
          // 计算阶乘的函数
          function getFactorial(uint n) public returns(uint){
              if(n==0 || n==1){
                  return 1;
              }
              else{
                  return getFactorial(n-1)*n;
              }
          }
      }
      // 调用FactorialContract.getFactorial函数计算阶乘
      contract FunCallContract{
          FactorialContract factorial;
          //在构造函数中创建FactorialContract合约的实例,
          // 必须通过FunCallContract构造函数的参数指定FactorialContract合约的地址。
          constructor(address addr) public{
              factorial = FactorialContract(addr);//实例化合约实例的时候需要传入其合约的地址
          }
          // 计算阶乘
          function jiecheng(uint n) public returns(uint){
              return factorial.getFactorial(n);
          }
      }
      

      12. 通过new关键字创建合约对象

      通过new关键字创建合约对象最大的优势
      不需要先部署被调用函数所在的合约,并先获取被调用函数所在合约的地址,然后才能部署调用函数的合约。

      换句话说就是,合约A调用合约B中的函数还需要先部署合约B是比较麻烦的。但是通过new关键字创建合约对象,则不需要部署合约B就可以调用B中的函数。

      相对于上面CallOtherContract.sol的代码,只需将FunCallContract的构造函数

      constructor(address addr) public{
              factorial = FactorialContract(addr);//实例化合约实例的时候需要传入其合约的地址
          }
      

      修改为

      // CallOtherContract_1.sol
      constructor() public{
               // 通过new关键字创建合约对象(此时不需要传入该合约对象的合约地址)
              factorial = new FactorialContract();
          }
      

      其他不用变化。

      这样使用new关键字创建合约对象,就不需要先部署FactorialContract合约,并获取其合约的地址后,然后才能部署FunCallContract合约,在其合约内部调用其FactorialContract合约中的函数。
      这里可以直接部署FunCallContract合约。

      13. 函数的命名参数

      在solidity语言中调用函数时可以指定命名参数,通过命名参数,可以不按被调用函数的参数的定义的顺序传入参数值

      pragma solidity >=0.4.16 <=0.7.0;
      
      // 命名参数的使用
      contract NamedParameter{
          function sub(int n1,int n2) public pure returns(int) {
              return n1-n2;
          }
          function fun() public pure returns(int){
              // 通过函数的命名参数,可以不按被调用函数中的参数的定义顺序进行赋值
              // 命名参数要通过{...}传递,有点类似于javascript中的对象
              return sub({n2:66,n1:32});
          }
      }
      

      15. 函数多返回值解构和元组赋值

      • (1)多返回值解构:如果函数返回多个值,可以支持将多个返回值分别赋给相应数目的变量
      • (2)元组赋值:指赋值运算符(=)左侧和右侧都有n个变量。
      pragma solidity >=0.4.24 <=0.7.0; //注意:只有0.4.24及以上版本才支持多返回值解构和元组赋值
      
      contract AssignmentContract{
          uint[] data;
          function mulValueFun() public pure returns(uint,bool,uint){
              return (2018,true,2019);
          }
      
          function assignment() public returns(uint xx,uint yy,bool bb,uint length){
              // 多返回值解构赋值,x、b和y分别等于mulValueFun函数的3个返回值
              (uint x,bool b,uint y) = mulValueFun();
              // 交换x和y的值
              (x,y)=(y,x);  //元组赋值
              // 这里只指定了一个变量(data.length),所以mulValueFun函数的其他返回值会被忽略
              (data.length,,) = mulValueFun(); //未指定的变量,通过逗号(,)将位置留着
              // 重新设置y变量的值
              y = 123;
              // 设置返回值
              xx = x;
              yy = y;
              bb = b;
              length = data.length;
          }
      }
      

      16. 变量声明和作用域

      (1)0.5.0版本之前

      在Solidity 0.5.0之前,Solidity语言的作用域规则继承自JavaScript。
      在if、while、for循环中定义的变量仍然作用于{...}外面,也就是说 {...}中声明的变量,在 {...}外仍然可以使用
      换句话说,就是无论{..}内还是{...}外,都不能有同名的变量。

      (2)0.5.0版本之后

      在Solidity 0.5.0之后, 开始支持声明块({...})变量,也就是在 {...}中声明的变量只在{...}中有效,这就意味着在多个{...}中可以声明多个同名的变量。

      17. 错误处理

      Solidity语言有3种与错误处理相关的函数:

      • (1)require:用于校检外部输入,如函数的参数、调用外部函数的返回值等。
      • (2)assert:用于校检合约的内部错误
      • (3)revert抛出错误

      Solidity语言的错误处理与数据库中的事务回滚类似,一旦发生错误以前做的所有操作都将回滚,因为合约很可能涉及到转账等敏感操作,所以一旦有任何异常,必须全部恢复到最初的状态,以避免数据不一致的情况发生。

      18. 全局变量

      (1)block变量

      pragma solidity >=0.4.20 <=0.7.0;
      
      contract BlockContract{
          function getBlockInfo() public view returns(address coinbase,uint difficulty,
          uint gaslimit,uint number,uint timestamp){
              coinbase = block.coinbase; //获取挖出当前区块的矿工的地址;
              difficulty = block.difficulty; //获取当前区块的挖矿难度;
              gaslimit = block.gaslimit; //获取当前区块的gas限制;
              number = block.number; //获取当前区块的编号
              timestamp = block.timestamp; //获取当前区块的时间戳(从Unix epoch即Unix纪元,从1970年1月1日开始)
          }
      }
      

      (2)msg变量

      • (1)执行函数包含参数:
      pragma solidity >=0.4.20 <=0.7.0;
      
      contract MsgContract{
          // 获取相关的系统信息
          function getMsgInfo(uint x) public payable returns(bytes memory data,uint gas,address sender,bytes4 sig,uint value){
              data = msg.data; //获取当前执行函数的调用数据(包含函数标识,即sha3散列值的前8位,若执行函数有参数,则还包含参数值)
              // gas = msg.gas; // msg.gas已经被gasleft()函数取代
              gas = gasleft(); // 获取剩余的gas
              sender = msg.sender; // 获取当前执行函数的调用地址
              sig = msg.sig; // 获取当前执行函数的标识(sha3散列值的前8位)
              value = msg.value; // 当前被发送的wei的数量(使用该属性的函数要使用payable关键字修饰)
          }
      }
      

      结果:

      • (2)执行函数不包含参数:

      把上述合约函数中的getMsgInfo(uint x)修改为getMsgInfo( ), 即去掉函数的参数。
      结果:

      • msg.data表示当前执行函数的调用数据,包含函数标识(即sha3散列值的前8位)。如果执行函数包含参数,则其还包含参数值
      • msg.sig表示当前执行函数的标识(即sha3散列值的前8位)。
      • 换句话说,如果执行函数不包含参数,则msg.data(只包含函数标识)与msg.sig(函数标识)是一样的

      例如,若当前执行的函数是getMsgInfo( ),那么可以使用下面的Node.js代码获取该函数sha3散列值的前8位。该值与msg.data属性返回的值相同(即都是只包含函数标识)。

      var Web3 = require('Web3');
      web3 = new Web3( );
      // 由于sha3函数返回的值前两位是表示十六进制的0x,所以从第3个字符开始截取,截取的长度为8位
      sign = web3.sha3("getMsgInfo( )").substr(2,8);
      console.log(sign); //输出 4c668374

      (3)其他全局变量

      pragma solidity >=0.4.22 <=0.7.0;
      
      // 其他全局变量
      contract OtherGlobalContract{
          // 获取其他全局变量的值
          function getOtherGlobal() public view returns(bytes32 hash,uint nowTime,uint gasPrice,address origin){
              // 获取指定区块的哈希值(要传入区块号)
              hash = blockhash(1001);
              // 获取当前区块的时间戳(与block.timestamp属性返回的值相同)
              nowTime = now;
              // 获取交易的gas价格
              gasPrice = tx.gasprice;
              // 获取发送交易的地址
              origin = tx.origin; 
          }
      }
      

      19. 自定义修饰符(modifier)

      modifier常用于在函数执行前检查某种前置条件是否满足,modifier是一种合约属性,可以被继承(子合约可以使用父合约中定义的modifier),同时还可被派生的合约重写(override)。

      modifier  modiferName{
          //校检代码
          _;
      }
      

      校检代码用于校检使用自定义修饰符的函数,后面必须跟一个下划线(_),而且下划线后面跟分号( ; )。如果通过校检,将使用该定义修饰符的函数的函数体插入到下划线的位置。也可以认为自定义修饰符其实就是多个函数相同代码的抽象,除了校检代码。

      pragma solidity >=0.4.20 <0.7.0;
      
      contract OwnerContract{
          address owner;
          // 保存部署合约的账号
          constructor() public{
              owner = msg.sender;
          }
          // 定义用于检测msg.sender是否为部署合约的账号,如果不是,终止执行函数
          modifier onlyOwner{
              require(msg.sender == owner,"Only owner can call this function.");
              _;   // 如果校检通过,会将使用onlyOwner函数的函数体插到这个位置。
          }
          // 校检地址是否可以为空
          // 当输入的_address为0x0000000000000000000000000000000000000000(0x后40个0),会抛出“_address can not be 0!”
          modifier notNull(address _address){
              require(_address != address(0),"_address can not be 0!");
              _;
          }
          // 一个函数可以有多个修饰符,多个修饰符之间用空格或回车分隔,修饰符的生效顺序与定义顺序是一样的
          // 修改合约所有者
          function changeOwner(address newOwner) notNull(newOwner) onlyOwner() public{
              owner = newOwner;
          }
      }
      
      //从OwnerContract继承 
      contract AddContract is OwnerContract{
          // 使用onlyOwner修饰函数
          function add(uint m,uint n)  public view onlyOwner() returns(uint){
              return m+n;
          }
      }
      
      contract RestrictContract{
          uint public mm;
          uint public nn;
          // 用于校检 m是否大于或等于n,如果不满足条件,相当于将使用restrict1函数的函数体删除
          modifier restrict1(uint m,uint n){
              if(m>=n){ //如果不满足条件,相当于将使用restrict1函数的函数体删除
                  _;
              }
          }
          // 除了校检m是否大于n外,还将m和n分别保存在mm和nn变量中
          modifier restrict2(uint m,uint n){
              require(m>=n,"m can not less than n");
              mm = m;
              nn = n;
              _;
          }
      }
      
      // 从RestrictContract合约继承
      contract SubContract is RestrictContract{
          // 使用restrict1修饰sub1函数
          function sub1(uint m,uint n) public pure restrict1(m,n) returns(uint){
              return m-n;
          }
          // 使用restrict2修饰sub2函数
          function sub2(uint m,uint n) public restrict2(m,n) returns(uint){
              return m-n;
          }
      }
      

      20. pure和view

      (1)pure

      使用pure关键字修饰的函数不允许读写 状态变量,否则会编译出错。
      下面几种情况会被认为是读写状态变量,在这些情况下,用pure关键字修饰函数就会编译错误:

      • (1)直接读取状态变量;
      • (2)访问 this.balance<address>.balance
      • (3)访问任何block、tx、msg变量中的成员,但msg.sig和msg.data除外
      • (4)调用任何没有使用pure修饰的函数,哪怕是这个函数中确实没有读写任何状态变量。
      • (5)内嵌用于操作状态变量的汇编代码的函数。

      (2)view

      使用view关键字修饰函数时,表示该函数不会修改状态变量
      下面几种情况表明函数会修改合约的状态变:

      • (1)只写修改状态变量;
      • (2)触发事件
      • (3)创建其他合约的实例
      • (4)调用selfdestruct函数销毁合约;
      • (5)通过call函数方发送以太币
      • (6)调用任何未标记view或pure函数
      • (7)使用底层的call函数;
      • (8)内嵌用于操作状态变量的汇编代码的函数;

      需要注意的是:用view修饰的函数并不会阻止函数中修改状态变量只是在用view修饰的函数中修改状态变量会出现警告。(不报错,只出现警告)

      21. fallback函数(回调函数)*

      fallback函数:一个没有函数名、参数和返回值的函数。必须用external进行修饰。
      在下面两种情况下会调用fallback函数:

      • (1) 合约中没有匹配的函数标识

      换句话说,就是

      1. 该合约没有其他函数;
      2. 调用合约时,如果没有匹配上该合约中的任何一个函数,就会调用回调函数。
      • (2) 合约接收到以太币(交易中没有附带任何其他数据),也会调用回调函数。

      注意:

      1. 这种情况下,fallback函数要使用payable关键字修饰,否则给包含fallback函数的合约发送以太币时出现编译错误
      2. 即使 fallback 函数不能有参数仍然可以使用 msg.data 来获取随调用提供的任何有效数据

      另外,还需注意以下几点:

      • (1) 如果调用者想调用一个不存在的函数,fallback函数将会被执行。
      • (2) 如果你只想为了接收以太币而实现fallback函数,你需要增加一些校检(如 require(msg.data.length == 0 ) )去避免一些无效的调用。
      • (3) 一个没有定义fallback函数(回调函数)的合约直接接收以太币(没有函数调用,如使用send或transfer),则会抛出一个异常,并返还以太币(有些行为在Solidity V0.4.0之前有些不同)。因此如果你要使你的合约接收以太币,你就必须实现一个被payable修饰的fallback函数。

      一个没有 payable fallback 函数的合约,可以作为 coinbase transaction (又名 miner block reward )的接收者或者作为 selfdestruct 的目标来接收以太币。

      pragma solidity >=0.5.0 <=0.7.0;
      
      contract Test{
          uint x;
          // (1)给这个合约发送任何消息都会调用这个函数(因为合约没有其他函数)
          // 定义一个fallback函数,在该函数中设置了状态变量x。
          // (2)向这个合约发送以太币将会抛出一个异常,因为这个回调函数没有用“payable”修饰符修饰。
          function() external{ x=101; }
          
      }
      
      contract Sink{
          // 定义了一个fallback函数,该函数使用payable修饰,表明可以接受其他地址发过来的以太币。
          function() external payable{ }
      }
      
      contract Caller{
          function callTest(Test test) public returns(bool){
              // 这里调用一个不存在的函数,由于匹配不到函数,所以将调用Test合约中的回调函数。
              (bool success,) = address(test).call(abi.encodeWithSignature("nonExitingFunction()"));
              require(success);
              // address(test)不允许直接调用“send”方法,因为“test”没有被“payable”修饰的回调函数。
              // 其必须通过“uint160”进行一个中间转换,然后再转换为“address payable”类型才能调用“send”方法。
              address payable testPayable = address(uint160(address(test)));
              // 如果某人发送以太币给那个合约,这笔交易将会失败(例如,这里将会返回false)
              return testPayable.send(2 ether);
          }
      
          function callSink(address payable sinkAddress) public returns(bool){
              Sink sink = Sink(sinkAddress);
              // 如果向Sink合约发送以太币时发送成功,Sink中的fallback函数会被调用
              return address(sink).send(5 ether);
          }
      }
      

      参考:Fallback Function

      22. 函数重载

      (1)定义

      函数重载是指一个合约中定义了多个函数名相同,但参数个数和类型不同的函数。(不考虑返回值)

      需要注意的是
      如果函数参数类型是可以转换的,例如合约和address,Solidity编译器就会认为它们是同一个数据类型,因此会产生编译错误

      (2)实例

      pragma solidity >=0.4.20 <=0.7.0;
      
      // 拥有4个同名的重载函数
      contract OverloadContract1{
          // 拥有2个uint类型的参数
          function add(uint m,uint n) public pure returns(uint){
              return m+n;
          }
          // 没有参数
          function add() public pure returns(uint){
              return 11+22;
          }
          // 有一个bool类型参数
          function add(bool b) public pure returns(bool){
              return b;
          }
          // 有3个uint类型的参数
          function add(uint l,uint m,uint n) public pure returns(uint){
              return l+m+n;
          }
      }
      
      contract A{
      
      }
      
      // 从表面上看第一个和第二个test函数的参数不一样,其实是一样的。因为合约A本身就是一个address类型
      // 所以OverloadContract2合约编译会失败,因为前两个test函数无法实现函数重载
      contract OverloadContract2{
          // 函数重载失败
          function test(address addr) public view returns(uint){
              return addr.balance;
          }
          // 函数重载失败,具体报错:Function overload clash during conversion to external types for arguments.
          // function test(A a) public view returns(uint){
          //     return address(a).balance;
          // }
      
          // 函数重载成功
          function test(A a,uint b) public view returns(uint,uint){
              return (address(a).balance,b);
          }
         
      }
      

      23. 事件(event)

      (1)定义

      如果将合约部署在TestRPC环境或者以太坊网络上,在执行以太坊函数时无法直接获得函数的返回值的,但是可以通过事件将计算结果返回给客户端

      event EventName( typeName  parameter,... );
      

      (2)实例

      pragma solidity >=0.4.20 <=0.7.0;
      
      contract EventContract{
          // 定义MyEvent事件
          event MyEvent(
              uint m,
              uint n,
              uint results
          );
      
          function add(uint m,uint n) public returns(uint){
              uint results = m+n;
              // 使用emit指令触发MyEvent事件,并通过事件参数传递m、n和m+n的计算结果(传递到客户端)
              emit MyEvent(m,n,results);
              return results;
          }
      }
      

      24. 合约继承

      合约继承,使用is关键字指定父合约。

      • (1) Solidity合约支持多继承,如果要指定多个合约,合约之间用逗号( , )分隔
      • (2) 尽管可以指定多个父合约,但是只会创建一个合约实例,将其他父合约中的代码复制到这个合约实例中。
      • (3) 如果多个父合约实现了同样的函数,那么以最后一个父合约的函数为准

      25. 合约构造函数

      • (1)老版本的solidity语言中,合约的构造函数与普通函数类似,只是函数名与合约名相同
      • (2)新版本的solidity语言中,使用constructor作为构造函数的名字

      这样做的好处是,一旦改变了合约的名字,也不用修改其构造函数的名字。

      • (3)合约构造函数允许使用publicinternal修饰。
      pragma solidity >=0.4.20 <=0.7.0;
      
      contract Contract1{
          uint public a;
          // 带参数的构造函数,假设用internal修饰
          constructor(uint _a) internal{
              a = _a; //用来初始化状态变量
          }
      }
      
      // 从Contract1继承,并将构造函数重新用public修饰,变成外部可访问的构造函数。
      // 由于Contract1合约的构造函数有一个参数,所以在继承时需要指定Contract1合约构造函数的参数值。
      contract Contract2 is Contract1(100){
          constructor() public{
      
          }
      }
      
      contract Contract3 is Contract1{
          uint aa;
          uint bb;
          // 如果构造参数的参数需要用某些变量设置,如构造函数的参数,可以在构造函数后面指定父合约构造函数的参数值
          constructor(uint _a,uint _b) Contract1(_a*_b) public{
              aa = _a;
              bb = _b;
          }
      }
      

      26. 抽象合约

      抽象合约: 至少有一个函数没有实现的合约。

      如果合约从一个抽象合约继承,而且没有全部实现抽象合约中的函数,那么这个合约就会继承这些未实现的函数,所以这个合约也是抽象合约。(说白了,就是这个合约继承了一个抽象合约,但是还有些继承自抽象合约的函数没有实现,于是这个合约也就有了一些函数没有实现,所以这个合约也就是抽象合约了。)

      抽象合约通常来实现多态,也就是用抽象合约的多个子合约创建多个实例,将这些实例赋给抽象合约类型变量
      由于这些子合约都实现了抽象合约中的函数,所以调用抽象合约中的函数会根据抽象合约类型变量的值不同,调用结果也不同,这就是称为多态。(调用同一个函数,会有多种不同表现形态)

      pragma solidity >=0.5.0 <=0.7.0;
      
      /**
          在MyContract合约中的test1和test2函数中分别创建了 MyContract1和MyContract2的实例,
          且将这两个合约的实例都赋值给了AbstractContract类型(抽象合约类型)的变量。
          在test1和test2函数中都调用了AbstractContract合约(父合约)中的add函数,且输入相同的实参值,
          不过返回结果却不一样,这就是多态。
          实际上,本质上调用的是MyContract1(子合约)和MyContract2合约(子合约)中的add函数。
       */
      
      contract AbstractContract{
          // add函数没有实现
          function add(uint m,uint n) public returns(uint);
          // 完整实现了sub函数
          function sub(int m,int n) public pure returns(int){
              return m-n;
          }
      }
      
      // 该合约从AbstractContract继承(即MyContract1是AbstractContract的一个子合约)
      contract MyContract1 is AbstractContract{
          // 实现了抽象合约中的add函数
          function add(uint m,uint n) public returns(uint){
              return m+n;
          }
      }
      
      // 该合约从AbstractContract继承(即MyContract2是AbstractContract的另一个子合约)
      contract MyContract2 is AbstractContract{
          // 实现了抽象合约中的add函数
          function add(uint m,uint n) public returns(uint){
              return 4*(m+n);  //不同于MyContract1中add函数的实现
          }
      } 
      
      // 该合约从MyContract1 继承,即继承了add函数和sub函数
      contract MyContract is MyContract1{
          function test1(uint m,uint n) public returns(uint){
              // 创建MyContract1 合约的实例
              AbstractContract abstractContract = new MyContract1();
              // 实际是调用了MyContract1 合约中的add函数
              return abstractContract.add(m,n);
          }
      
          function test2(uint m,uint n) public returns(uint){
              // 创建MyContract2 合约的实例
              AbstractContract abstractContract = new MyContract2();
              // 实际是调用了MyContract1 合约中的add函数
              return abstractContract.add(m,n);
          }
      }
      

      27. 接口

      接口与抽象合约类似,但是不能实现任何函数。(即所有接口中的方法都是未实现的)
      此外,接口还有如下限制:

      • (1)不能继承其他合约或接口
      • (2)不能定义构造函数;
      • (3)不能定义变量;
      • (4)不能定义结构体;
      • (5)不能定义枚举类型。
      interface interfaceName{
          //抽象方法(未被实现的方法)
      }
      

      注意:
      (1)接口应该定义在合约的外部(与合约是同一等级);
      (2)接口中定义的方法必须被external修饰;

      合约实现接口的方法与继承合约或抽象合约的方法类似, 使用is关键字.

      pragma solidity >=0.5.0 <=0.7.0;
      
      // 定义接口(定义在合约外面)
      interface MyInterface{
          function add(uint m,uint n) external returns(uint);
          function sub(int m,int n) external returns(int);
      }
      // InterfaceContract实现了MyInterface
      contract InterfaceContract is MyInterface{
          function add(uint m,uint n) public returns(uint){
              return m+n;
          }
          function sub(int m,int n) public returns(int){
              return m-n;
          }
      }
      

      28. gas limit和gas price

      (1)gas limit

      • (1) gas limit 表示完成转账交易最大消耗的gas数,如果超过这个gas数,交易就会失败,整个交易过程都会回滚。
      • (2) gas limit 主要是为了防止由于发布交易消耗过多的gas

      (2)gas price

      • 表示你愿意为单位gas支付的费用,以gwei为单位表示。
        1 gwei = 10^9 wei

      (3)两者的作用

      • (1)在交易中gasPrice是由发起交易人来决定的,每个矿工接收到交易请求之后,会根据gasPrice的高低来决定是否要打包进区块。
      • (2)每个交易中必须包含gas limit和gas price的值。gas limit代表了这个交易执行过程中最多被允许消耗的gas数量。
      • (3)gas limit和gas price 代表着交易发送者愿意为执行交易支付的wei的最大值。
        付款金额(单位 wei)= Gas数量 × GasPrice
      • (4)交易执行完成后,如果实际消耗的gas小于gaslimit,那么剩余的gas会以Ether的方式返回给交易发起者。
      • (5)如果在交易过程中,实际消耗的gas大于gas limit,那么就会出现“gas不足”的错误,这种情况下交易会被终止,交易之前的所有修改的状态会被回滚,同时在交易执行过程中所消耗的gas不会回退给交易发起者的
    展开全文
  • Solidity】7. 部件 - 深入理解Solidity

    万次阅读 2017-09-18 14:33:45
    Solidity 部件Solidity定义了一种也可以在没有Solidity的情况下使用的汇编语言。 此汇编语言也可以用作Solidity源代码中的“内联汇编”。 我们从描述如何使用内联汇编以及它与独立程序集的区别开始,然后指定程序集...

    索引

    Solidity 部件

    Solidity定义了一种也可以在没有Solidity的情况下使用的汇编语言。 此汇编语言也可以用作Solidity源代码中的“内联汇编”。 我们从描述如何使用内联汇编以及它与独立程序集的区别开始,然后指定程序集本身。

    TODO:编写内联汇编的范围规则有点不一样,例如使用库的内部函数时出现的复杂情况。 此外,请编写由编译器定义的符号。

    内联汇编

    为了更精细的控制,特别是为了通过编写库来增强语言,可以使用接近虚拟机的语言将Solidity语句与内联汇编交错。 由于EVM是一个堆栈机器,因此通常难以解决正确的堆栈槽位,并为堆栈上正确点的操作码提供参数。 Solidity的内联组件试图通过以下功能来促进编写手动装配时出现的其他问题:

    • 函数式操作码:mul(1, add(2, 3)) 替代 push1 3 push1 2 add push1 1 mul
    • 汇编局部变量:let x := add(2, 3) let y := mload(0x40) x := add(x, y)
    • 访问外部变量:function f(uint x) { assembly { x := sub(x, 1) } }
    • 标签:let x := 10 repeat: x := sub(x, 1) jumpi(repeat, eq(x, 0))
    • 循环:for { let i := 0 } lt(i, x) { i := add(i, 1) } { y := mul(2, y) }
    • switch语句:switch x case 0 { y := mul(x, 2) } default { y := 0 }
    • 函数调用:function f(x) -> y { switch x case 0 { y := 1 } default { y := mul(x, f(sub(x, 1))) } }

    我们现在想详细描述内联汇编语言。

    内联汇编是在低级别访问复仇虚拟机的方式。 这丢弃了Solidity的几个重要安全功能。

    例子

    以下示例提供了用于访问另一个合约的代码并将其加载到字节变量中的库代码。 这是不可能的,在所有”plain Solidity”和想法是,组件库将用于提高这种方式的语言。

    pragma solidity ^0.4.0;
    
    library GetCode {
        function at(address _addr) returns (bytes o_code) {
            assembly {
                // 检索代码的大小,这需要汇编
                let size := extcodesize(_addr)
                // 分配输出字节数组 - 这也可以在没有汇编的情况下完成
                // by using o_code = new bytes(size)
                o_code := mload(0x40)
                // 新的“内存端”包括填充
                mstore(0x40, add(o_code, and(add(add(size, 0x20), 0x1f), not(0x1f))))
                // 存储长度在内存中
                mstore(o_code, size)
                // 实际上检索代码,这需要汇编
                extcodecopy(_addr, add(o_code, 0x20), 0, size)
            }
        }
    }

    在优化器无法生成有效代码的情况下,内联汇编也可能是有益的。 请注意,汇编更难编写,因为编译器不执行检查,所以只有当你真的知道你在做什么时,才应该使用它来处理复杂的事情。

    pragma solidity ^0.4.12;
    
    library VectorSum {
        // 此功能效率较低,因为优化程序当前无法删除阵列访问中的边界检查。
        function sumSolidity(uint[] _data) returns (uint o_sum) {
            for (uint i = 0; i < _data.length; ++i)
                o_sum += _data[i];
        }
    
        // 我们知道我们只能在边界访问数组,所以我们可以避免检查。 0x20需要添加到数组,因为第一个插槽包含数组长度。
        function sumAsm(uint[] _data) returns (uint o_sum) {
            for (uint i = 0; i < _data.length; ++i) {
                assembly {
                    o_sum := add(o_sum, mload(add(add(_data, 0x20), mul(i, 0x20))))
                }
            }
        }
    
        // 与上述相同,但在内联汇编中完成整个代码。
        function sumPureAsm(uint[] _data) returns (uint o_sum) {
            assembly {
               // 加载长度(前32个字节)
               let len := mload(_data)
    
               // 跳过长度字段。
               //
               // 保持临时变量,使其可以增加到位。
               //
               // 注意:增加_data将导致此程序集后不可用的_data变量
               let data := add(_data, 0x20)
    
               // 迭代,直到绑定不满足。
               for
                   { let end := add(data, len) }
                   lt(data, end)
                   { data := add(data, 0x20) }
               {
                   o_sum := add(o_sum, mload(data))
               }
            }
        }
    }

    语法

    汇编将注释,文字和标识符完全解析为Solidity,因此您可以使用通常的/// * * /注释。 内联装配体由装配{...}标记,并且在这些花括号内,可以使用以下内容(有关详细信息,请参阅后面的部分)

    • 文字,即0x123的,42"ABC"(字符串最多32个字符)
    • 操作码(在“指令式”),例如 mload sload dup1存储,列表见下文
    • 函数式的操作码,例如 add(1,mlod(0))
    • 标签,例如 name:
    • 可变声明,例如 让x:= 7,让x:= add(y,3)或让x(empty (0)的初始值分配)
    • 标识符(标签或汇编局部变量和外部作为内联汇编使用),比如jump(name), 3 x add
    • 分配(在“指令式”),例如 3 =:x
    • 函数式分配,例如 x:= add(y,3)
    • 块,其中局部变量内作用域,例如 {let x:= 3 {let y:= add(x,1)}}

    操作码

    本文档不希望完整描述Ethereum虚拟机,但以下列表可用作其操作码的参考。

    如果操作码接受参数(总是从堆栈的顶部),它们将以括号给出。 请注意,参数的顺序可以看作是在非功能性风格(如下所述)中颠倒。 标记的操作码 - 不要将一个项目推入堆栈,标有*的项目是特殊的,而其他所有其他项目都将一个项目推入堆栈。

    在下文中,mem [a ... b]表示从位置a开始到(不包括)位置b的存储器的字节,存储器[p]表示位置p处的存储内容。

    操作码pushijumpdest不能直接使用。

    在语法中,操作码被表示为预定义的标识符。

    stop - stop execution, identical to return(0,0)
    add(x, y) x + y
    sub(x, y) x - y
    mul(x, y) x * y
    div(x, y) x / y
    sdiv(x, y) x / y, for signed numbers in two’s complement
    mod(x, y) x % y
    smod(x, y) x % y, for signed numbers in two’s complement
    exp(x, y) x to the power of y
    not(x) ~x, every bit of x is negated
    lt(x, y) 1 if x < y, 0 otherwise
    gt(x, y) 1 if x > y, 0 otherwise
    slt(x, y) 1 if x < y, 0 otherwise, for signed numbers in two’s complement
    sgt(x, y) 1 if x > y, 0 otherwise, for signed numbers in two’s complement
    eq(x, y) 1 if x == y, 0 otherwise
    iszero(x) 1 if x == 0, 0 otherwise
    and(x, y) bitwise and of x and y
    or(x, y) bitwise or of x and y
    xor(x, y) bitwise xor of x and y
    byte(n, x) nth byte of x, where the most significant byte is the 0th byte
    addmod(x, y, m) (x + y) % m with arbitrary precision arithmetics
    mulmod(x, y, m) (x * y) % m with arbitrary precision arithmetics
    signextend(i, x) sign extend from (i*8+7)th bit counting from least significant
    keccak256(p, n) keccak(mem[p…(p+n)))
    sha3(p, n) keccak(mem[p…(p+n)))
    jump(label) - jump to label / code position
    jumpi(label, cond) - jump to label if cond is nonzero
    pc current position in code
    pop(x) - remove the element pushed by x
    dup1 … dup16 copy ith stack slot to the top (counting from top)
    swap1 … swap16 * swap topmost and ith stack slot below it
    mload(p) mem[p..(p+32))
    mstore(p, v) - mem[p..(p+32)) := v
    mstore8(p, v) - mem[p] := v & 0xff - only modifies a single byte
    sload(p) storage[p]
    sstore(p, v) - storage[p] := v
    msize size of memory, i.e. largest accessed memory index
    gas gas still available to execution
    address address of the current contract / execution context
    balance(a) wei balance at address a
    caller call sender (excluding delegatecall)
    callvalue wei sent together with the current call
    calldataload(p) call data starting from position p (32 bytes)
    calldatasize size of call data in bytes
    calldatacopy(t, f, s) - copy s bytes from calldata at position f to mem at position t
    codesize size of the code of the current contract / execution context
    codecopy(t, f, s) - copy s bytes from code at position f to mem at position t
    extcodesize(a) size of the code at address a
    extcodecopy(a, t, f, s) - like codecopy(t, f, s) but take code at address a
    returndatasize size of the last returndata
    returndatacopy(t, f, s) - copy s bytes from returndata at position f to mem at position t
    create(v, p, s) create new contract with code mem[p..(p+s)) and send v wei and return the new address
    create2(v, n, p, s) create new contract with code mem[p..(p+s)) at address keccak256(

    . n . keccak256(mem[p..(p+s))) and send v wei and return the new address
    call(g, a, v, in, insize, out, outsize) call contract at address a with input mem[in..(in+insize)) providing g gas and v wei and output area mem[out..(out+outsize)) returning 0 on error (eg. out of gas) and 1 on success
    callcode(g, a, v, in, insize, out, outsize) identical to call but only use the code from a and stay in the context of the current contract otherwise
    delegatecall(g, a, in, insize, out, outsize) identical to callcode but also keep caller and callvalue
    staticcall(g, a, in, insize, out, outsize) identical to call(g, a, 0, in, insize, out, outsize) but do not allow state modifications
    return(p, s) - end execution, return data mem[p..(p+s))
    revert(p, s) - end execution, revert state changes, return data mem[p..(p+s))
    selfdestruct(a) - end execution, destroy current contract and send funds to a
    invalid - end execution with invalid instruction
    log0(p, s) - log without topics and data mem[p..(p+s))
    log1(p, s, t1) - log with topic t1 and data mem[p..(p+s))
    log2(p, s, t1, t2) - log with topics t1, t2 and data mem[p..(p+s))
    log3(p, s, t1, t2, t3) - log with topics t1, t2, t3 and data mem[p..(p+s))
    log4(p, s, t1, t2, t3, t4) - log with topics t1, t2, t3, t4 and data mem[p..(p+s))
    origin transaction sender
    gasprice gas price of the transaction
    blockhash(b) hash of block nr b - only for last 256 blocks excluding current
    coinbase current mining beneficiary
    timestamp timestamp of the current block in seconds since the epoch
    number current block number
    difficulty difficulty of the current block
    gaslimit block gas limit of the current block

    文本

    您可以使用整数常数,以十进制或十六进制表示法键入,并且将自动生成适当的PUSHi指令。 以下创建代码添加2和3导致5,然后计算按位和字符串“abc”。 字符串左对齐存储,不能超过32个字节。

    assembly { 2 3 add "abc" and }

    函数式

    您可以在操作码之后输入操作码,方式与字节码最终相同。 例如,在0x80的内存中添加3将是

    3 0x80 mload add 0x80 mstore
    

    由于通常很难看到某些操作码的实际参数是什么,Solidity inline assembly也提供了一个“函数式”符号,其中相同的代码将被写入如下

    mstore(0x80, add(mload(0x80), 3))

    函数式表达式不能在内部使用教学风格,即1 2 mstore(0x80,add)是无效的程序集,它必须写成mstore(0x80,add(2,1))。 对于不带参数的操作码,可以省略括号。

    请注意,在函数式方面,参数的顺序与指令式的方式相反。 如果您使用功能样式,则第一个参数将结束于堆栈顶部。

    访问外部变量和函数

    可以通过简单地使用其名称来访问Solidity变量和其他标识符。 对于内存变量,这将推送地址,而不是将值推送到堆栈上。 存储变量不同:存储中的值可能不占用完整存储槽,因此它们的“地址”由该槽内的一个槽位和一个字节偏移量组成。 要检索由变量x指向的插槽,您使用x_slot并检索您使用x_offset的字节偏移量。

    在分配(见下文)中,我们甚至可以使用本地Solidity变量来分配。

    还可以访问内联汇编的外部函数:程序集将推入其入口标签(应用虚拟函数分辨率)。 在Solidity的调用语义是:

    • 调用者推送返回标签,arg1arg2,…,argn
    • 该调用返回与ret1ret2,…,retm

    这个功能使用起来还是有点麻烦,因为堆栈偏移量在调用过程中基本上有变化,因此对局部变量的引用将是错误的。

    pragma solidity ^0.4.11;
    
    contract C {
        uint b;
        function f(uint x) returns (uint r) {
            assembly {
                r := mul(x, sload(b_slot)) // 忽略偏移量,我们知道它是零
            }
        }
    }

    标签

    EVM组装中的另一个问题是jump和jumpi使用可以轻松更改的绝对地址。 Solidity inline assembly提供标签,使跳转更容易使用。 请注意,标签是低级功能,可以使用汇编功能,循环和开关指令(见下文)编写无标签的高效组装。 以下代码计算Fibonacci系列中的一个元素。

    {
        let n := calldataload(4)
        let a := 1
        let b := a
    loop:
        jumpi(loopend, eq(n, 0))
        a add swap1
        n := sub(n, 1)
        jump(loop)
    loopend:
        mstore(0, a)
        return(0, 0x20)
    }
    

    请注意,自动访问堆栈变量只有在汇编器知道当前堆栈高度时才能工作。 如果跳转源和目标具有不同的堆栈高度,则无法正常工作。 使用这种跳转仍然很好,但是在这种情况下,您应该不会访问任何堆栈变量(甚至汇编变量)。

    此外,堆栈高度分析器通过操作码(而不是根据控制流程)执行代码操作码,因此在以下情况下,汇编程序将对标签二的堆栈高度产生错误的印象:

    {
        let x := 8
        jump(two)
        one:
            // 这里的堆栈高度是2(因为我们推x和7),但汇编器认为它是1,因为它读取从上到下。 这里访问堆栈变量x会导致错误。
            x := 9
            jump(three)
        two:
            7 // 把东西推到堆栈上
            jump(one)
        three:
    }

    可以通过手动调整汇编程序的堆栈高度来修复此问题 - 您可以提供在标签之前添加到堆栈高度的堆栈高度增量。 请注意,如果您只是使用循环和程序集级别的函数,则不需要关心这些事情。

    作为一个例子,如何在极端情况下做到这一点,请参阅以下内容。

    {
        let x := 8
        jump(two)
        0 // 此代码无法访问,但会正确调整堆栈高度
        one:
            x := 9 // 现在可以正确访问x。
            jump(three)
            pop // 类似的负面矫正。
        two:
            7 // 把东西推到堆栈上
            jump(one)
        three:
        pop // 我们要在这里再次弹出手动推值。
    }

    声明汇编局部变量

    您可以使用let关键字声明仅在内联程序集中可见的变量,实际上仅在当前的{…} - 块中声明。 发生什么事情是,let指令将创建一个新的堆栈槽,为该变量保留,并在块的结尾达到时再次自动删除。 需要提供一种用于可以只是0的变量的初始值,但它也可以是一个复杂的函数式表达。

    pragma solidity ^0.4.0;
    
    contract C {
        function f(uint x) returns (uint b) {
            assembly {
                let v := add(x, 1)
                mstore(0x80, v)
                {
                    let y := add(sload(v), 1)
                    b := y
                } // y在这里被“释放”了
                b := add(b, v)
            } // v在这里被“释放”了
        }
    }

    分配

    组件局部变量和函数局部变量都可以进行分配。 注意当您分配指向内存或存储的变量时,只会更改指针而不是数据。

    有两种分配:函数式和命令式的。 对函数式的赋值(variable := value),您需要在一个函数式表达式中提供一个值,该表达式将产生一个堆栈值和指令样式(=:variable),该值仅从堆栈中获取 最佳。 对于这两种方式,冒号指向变量的名称。 通过将堆栈中的变量的值替换为新值来执行分配。

    {
        let v := 0 // 函数式赋值作为变量声明的一部分
        let g := add(v, 2)
        sload(10)
        =: v // 指令样式分配,将`sload(10)`的结果放入v中
    }

    Switch

    您可以使用switch语句作为“if / else”的非常基本的版本。 它需要一个表达式的值,并将其与几个常量进行比较。 对应于匹配常数的分支。 与某些编程语言的容易出错的行为相反,控制流程从一种情况到下一种情况都不会继续。 可能会有一个备用或默认情况称为默认。

    {
        let x := 0
        switch calldataload(4)
        case 0 {
            x := calldataload(0x24)
        }
        default {
            x := calldataload(0x44)
        }
        sstore(0, div(x, 2))
    }

    案件清单不需要花括号,但案件确实需要它们。

    循环

    装配支持简单的for-style循环。 For-style循环有一个包含初始化部分,条件和后迭代部分的标题。 条件必须是功能性表达式,而另外两个是块。 如果初始化部分声明任何变量,这些变量的范围将扩展到正文(包括条件和后期部分)。

    以下示例计算内存区域的总和。

    {
        let x := 0
        for { let i := 0 } lt(i, 0x100) { i := add(i, 0x20) } {
            x := add(x, mload(i))
        }
    }

    For循环也可以写成它们的行为就像while循环:简单地将初始化和后处理部分留空。

    {
        let x := 0
        let i := 0
        for { } lt(i, 0x100) { } {     // while(i < 0x100)
            x := add(x, mload(i))
            i := add(i, 0x20)
        }
    }

    函数

    装配允许定义低级功能。 这些从堆栈中获取参数(和返回PC),并将结果放在堆栈中。 调用函数看起来与执行功能式操作码相同。

    函数可以在任何位置定义,并在它们声明的块中可见。在函数内部,不能访问在该函数之外定义的局部变量。 没有明确的return语句。

    如果调用返回多个值的函数,则必须使用a,b:= f(x)或a,b:= f(x)将它们分配给元组。

    以下示例通过平方和乘法来实现功率函数。

    {
        function power(base, exponent) -> result {
            switch exponent
            case 0 { result := 1 }
            case 1 { result := base }
            default {
                result := power(mul(base, base), div(exponent, 2))
                switch mod(exponent, 2)
                    case 1 { result := mul(base, result) }
            }
        }
    }

    要避免的事情

    内联汇编可能有一个相当高的水平看,但它实际上是非常低的水平。 函数调用,循环和开关由简单的重写规则转换,之后,唯一汇编为您做的是重新排列函数式操作码,跳的管理标签,用于组件 - 计数堆高度为变量访问和移除堆叠插槽 局部变量,当它们的块结束到达时。 特别是对于最后两种情况,重要的是要知道汇编程序只能从上到下计算堆栈高度,而不一定跟随控制流程。 此外,像swap这样的操作只会交换堆栈的内容,而不是变量的位置。

    粘性合约

    与EVM组装相反,Solidity知道比256位更窄的类型,例如uint24。为了使它们更有效率,大多数算术运算只将它们视为256位数,高阶位仅在需要的位置进行清除,即在写入存储器之前不久或执行比较之前。这意味着如果您从内联汇编中访问这样的变量,那么您可能必须首先手动清除较高位。

    Solidity以非常简单的方式管理内存:内存中位置0x40有一个“可用内存指针”。如果要分配内存,只需从该位置使用内存并相应地更新指针。

    Solidity中的内存数组中的元素总是占用32个字节的倍数(是的,这对于byte []甚至是正确的,但不是字节和字符串)。多维存储器阵列是指向存储器阵列的指针。动态数组的长度存储在数组的第一个时隙,然后只有数组元素。

    静态大小的内存数组没有长度字段,但是它将很快添加,以便在静态和动态大小的数组之间实现更好的可转换性,所以请不要依赖这些数组。

    独立装配

    上面描述为内联汇编的汇编语言也可以单独使用,实际上该计划是将其用作Solidity编译器的中间语言。 在这种形式下,它试图实现几个目标:

    1. 编写的程序应该是可读的,即使代码是由Solidity的编译器生成的。
    2. 从汇编到字母代码的翻译应尽可能少的“惊喜”。
    3. 控制流程应易于检测,有助于形式验证和优化。

    为了实现第一个和最后一个目标,程序集提供了高级结构,如循环,切换语句和函数调用。 应该可以编写不使用显式SWAP,DUP,JUMP和JUMPI语句的汇编程序,因为前两个模糊数据流和最后两个混淆控制流。 此外,mul(add(x,y),7)的函数语句优于纯操作码语句,如7 y x add mul,因为在第一种形式下,看起来哪个操作数用于哪个操作码要容易得多。

    第二个目标是通过引入一个绝对的阶段来实现,该阶段只能以非常规则的方式去除较高级别的构造,并且仍允许检查生成的低级汇编代码。汇编器执行的唯一的非本地操作是用户定义的标识符(函数,变量…)的名称查找,它遵循非常简单和规则的范围规则,并从堆栈中清除局部变量。

    范围设定:声明的标识符(标签,变量,函数,程序集)仅在声明的块中(包括当前块中的嵌套块)可见。访问函数边界的局部变量是不合法的,即使它们在范围内。不允许阴影。局部变量在声明之前无法访问,但标签,函数和程序集可以访问。组件是用于例如组件的特殊块。返回运行时代码或创建合同。子组件中没有外部组件的标识符可见。

    如果控制流经过块的末尾,则插入与该块中声明的局部变量数相匹配的弹出指令。无论何时引用局部变量,代码生成器需要知道其当前在堆栈中的相对位置,因此需要跟踪当前所谓的堆栈高度。由于所有局部变量在块的末尾被移除,所以块之前和之后的堆栈高度应该相同。如果不是这种情况,则会发出警告。

    为什么我们使用更高级的结构,如switch,for和函数:

    使用switch,for和函数,应该可以手动编写复杂的代码而不使用jump或jumpi。 这使得分析控制流程变得更加容易,这允许改进形式验证和优化。

    此外,如果允许手动跳转,则计算堆栈高度相当复杂。 堆栈上的所有局部变量的位置需要是已知的,否则,不会引用局部变量,也不会在块结束时自动从堆栈中删除局部变量,这将正常工作。 脱机机制正确地将操作插入到不可达到的块中,以在没有持续控制流的跳跃的情况下适当地调整堆栈高度。

    例:

    我们将按照Solidity的一个示例汇编到脱机装配。 我们考虑以下Solidity程序的运行时字节码:

    pragma solidity ^0.4.0;
    
    contract C {
      function f(uint x) returns (uint y) {
        y = 1;
        for (uint i = 0; i < x; i++)
          y = 2 * y;
      }
    }

    将生成以下程序集:

    {
      mstore(0x40, 0x60) // 存储“可用内存指针”
      // 函数调度程序
      switch div(calldataload(0), exp(2, 226))
      case 0xb3de648b {
        let (r) = f(calldataload(4))
        let ret := $allocate(0x20)
        mstore(ret, r)
        return(ret, 0x20)
      }
      default { revert(0, 0) }
      // 内存分配器
      function $allocate(size) -> pos {
        pos := mload(0x40)
        mstore(0x40, add(pos, size))
      }
      // 契约功能
      function f(x) -> y {
        y := 1
        for { let i := 0 } lt(i, x) { i := add(i, 1) } {
          y := mul(2, y)
        }
      }
    }

    在desug阶段之后,它看起来如下:

    {
      mstore(0x40, 0x60)
      {
        let $0 := div(calldataload(0), exp(2, 226))
        jumpi($case1, eq($0, 0xb3de648b))
        jump($caseDefault)
        $case1:
        {
          // the function call - we put return label and arguments on the stack
          $ret1 calldataload(4) jump(f)
          // This is unreachable code. Opcodes are added that mirror the
          // effect of the function on the stack height: Arguments are
          // removed and return values are introduced.
          pop pop
          let r := 0
          $ret1: // the actual return point
          $ret2 0x20 jump($allocate)
          pop pop let ret := 0
          $ret2:
          mstore(ret, r)
          return(ret, 0x20)
          // although it is useless, the jump is automatically inserted,
          // since the desugaring process is a purely syntactic operation that
          // does not analyze control-flow
          jump($endswitch)
        }
        $caseDefault:
        {
          revert(0, 0)
          jump($endswitch)
        }
        $endswitch:
      }
      jump($afterFunction)
      allocate:
      {
        // we jump over the unreachable code that introduces the function arguments
        jump($start)
        let $retpos := 0 let size := 0
        $start:
        // output variables live in the same scope as the arguments and is
        // actually allocated.
        let pos := 0
        {
          pos := mload(0x40)
          mstore(0x40, add(pos, size))
        }
        // This code replaces the arguments by the return values and jumps back.
        swap1 pop swap1 jump
        // Again unreachable code that corrects stack height.
        0 0
      }
      f:
      {
        jump($start)
        let $retpos := 0 let x := 0
        $start:
        let y := 0
        {
          let i := 0
          $for_begin:
          jumpi($for_end, iszero(lt(i, x)))
          {
            y := mul(2, y)
          }
          $for_continue:
          { i := add(i, 1) }
          jump($for_begin)
          $for_end:
        } // Here, a pop instruction will be inserted for i
        swap1 pop swap1 jump
        0 0
      }
      $afterFunction:
      stop
    }

    大会发生在四个阶段:

    1. 解析
    2. 脱机(删除开关,用于和功能)
    3. 操作码流生成
    4. 字节代码生成

    我们将以伪形式的方式指定步骤1到3。 更正式的规格将遵循。

    解析/语法

    解析器的任务如下:

    • 将字节流转换成令牌流,丢弃C ++风格的注释(对源引用有一个特殊的注释,但是我们不会在这里解释)。
    • 根据下面的语法将令牌流转换为AST
    • 使用它们定义的块(AST节点的注释)来注册标识符,并注意可以访问变量的哪个点。

    组合词典遵循由Solidity本身定义的词组。

    空格用于分隔标记,它由空格,制表符和换行符组成。 注释是常规的JavaScript / C ++注释,并以与Whitespace相同的方式进行解释。

    语法:

    AssemblyBlock = '{' AssemblyItem* '}'
    AssemblyItem =
        Identifier |
        AssemblyBlock |
        FunctionalAssemblyExpression |
        AssemblyLocalDefinition |
        FunctionalAssemblyAssignment |
        AssemblyAssignment |
        LabelDefinition |
        AssemblySwitch |
        AssemblyFunctionDefinition |
        AssemblyFor |
        'break' | 'continue' |
        SubAssembly | 'dataSize' '(' Identifier ')' |
        LinkerSymbol |
        'errorLabel' | 'bytecodeSize' |
        NumberLiteral | StringLiteral | HexLiteral
    Identifier = [a-zA-Z_$] [a-zA-Z_0-9]*
    FunctionalAssemblyExpression = Identifier '(' ( AssemblyItem ( ',' AssemblyItem )* )? ')'
    AssemblyLocalDefinition = 'let' IdentifierOrList ':=' FunctionalAssemblyExpression
    FunctionalAssemblyAssignment = IdentifierOrList ':=' FunctionalAssemblyExpression
    IdentifierOrList = Identifier | '(' IdentifierList ')'
    IdentifierList = Identifier ( ',' Identifier)*
    AssemblyAssignment = '=:' Identifier
    LabelDefinition = Identifier ':'
    AssemblySwitch = 'switch' FunctionalAssemblyExpression AssemblyCase*
        ( 'default' AssemblyBlock )?
    AssemblyCase = 'case' FunctionalAssemblyExpression AssemblyBlock
    AssemblyFunctionDefinition = 'function' Identifier '(' IdentifierList? ')'
        ( '->' '(' IdentifierList ')' )? AssemblyBlock
    AssemblyFor = 'for' ( AssemblyBlock | FunctionalAssemblyExpression)
        FunctionalAssemblyExpression ( AssemblyBlock | FunctionalAssemblyExpression) AssemblyBlock
    SubAssembly = 'assembly' Identifier AssemblyBlock
    LinkerSymbol = 'linkerSymbol' '(' StringLiteral ')'
    NumberLiteral = HexNumber | DecimalNumber
    HexLiteral = 'hex' ('"' ([0-9a-fA-F]{2})* '"' | '\'' ([0-9a-fA-F]{2})* '\'')
    StringLiteral = '"' ([^"\r\n\\] | '\\' .)* '"'
    HexNumber = '0x' [0-9a-fA-F]+
    DecimalNumber = [0-9]+

    脱糖

    AST转换消除了交换和功能结构。 结果仍然可以解析为同一个解析器,但它不会使用某些结构。 如果添加仅跳转到并且不继续的jumpdests,则添加有关堆栈内容的信息,除非没有访问外部范围的本地变量或堆栈高度与上一条指令相同。

    伪代码:

    desugar item: AST -> AST =
    match item {
    AssemblyFunctionDefinition('function' name '(' arg1, ..., argn ')' '->' ( '(' ret1, ..., retm ')' body) ->
      <name>:
      {
        jump($<name>_start)
        let $retPC := 0 let argn := 0 ... let arg1 := 0
        $<name>_start:
        let ret1 := 0 ... let retm := 0
        { desugar(body) }
        swap and pop items so that only ret1, ... retm, $retPC are left on the stack
        jump
        0 (1 + n times) to compensate removal of arg1, ..., argn and $retPC
      }
    AssemblyFor('for' { init } condition post body) ->
      {
        init // cannot be its own block because we want variable scope to extend into the body
        // find I such that there are no labels $forI_*
        $forI_begin:
        jumpi($forI_end, iszero(condition))
        { body }
        $forI_continue:
        { post }
        jump($forI_begin)
        $forI_end:
      }
    'break' ->
      {
        // find nearest enclosing scope with label $forI_end
        pop all local variables that are defined at the current point
        but not at $forI_end
        jump($forI_end)
        0 (as many as variables were removed above)
      }
    'continue' ->
      {
        // find nearest enclosing scope with label $forI_continue
        pop all local variables that are defined at the current point
        but not at $forI_continue
        jump($forI_continue)
        0 (as many as variables were removed above)
      }
    AssemblySwitch(switch condition cases ( default: defaultBlock )? ) ->
      {
        // find I such that there is no $switchI* label or variable
        let $switchI_value := condition
        for each of cases match {
          case val: -> jumpi($switchI_caseJ, eq($switchI_value, val))
        }
        if default block present: ->
          { defaultBlock jump($switchI_end) }
        for each of cases match {
          case val: { body } -> $switchI_caseJ: { body jump($switchI_end) }
        }
        $switchI_end:
      }
    FunctionalAssemblyExpression( identifier(arg1, arg2, ..., argn) ) ->
      {
        if identifier is function <name> with n args and m ret values ->
          {
            // find I such that $funcallI_* does not exist
            $funcallI_return argn  ... arg2 arg1 jump(<name>)
            pop (n + 1 times)
            if the current context is `let (id1, ..., idm) := f(...)` ->
              let id1 := 0 ... let idm := 0
              $funcallI_return:
            else ->
              0 (m times)
              $funcallI_return:
              turn the functional expression that leads to the function call
              into a statement stream
          }
        else -> desugar(children of node)
      }
    default node ->
      desugar(children of node)
    }

    操作码流生成

    在操作码流生成期间,我们跟踪计数器中的当前堆栈高度,以便可以通过名称访问堆栈变量。 使用修改堆栈的每个操作码和使用堆栈调整注释的每个标签修改堆栈高度。 每当引入新的局部变量时,它将与当前堆栈高度一起注册。 如果访问变量(用于复制其值或用于分配),则根据当前堆栈高度和引入变量点的堆栈高度之间的差异来选择适当的DUP或SWAP指令。

    伪代码:

    codegen item: AST -> opcode_stream =
    match item {
    AssemblyBlock({ items }) ->
      join(codegen(item) for item in items)
      if last generated opcode has continuing control flow:
        POP for all local variables registered at the block (including variables
        introduced by labels)
        warn if the stack height at this point is not the same as at the start of the block
    Identifier(id) ->
      lookup id in the syntactic stack of blocks
      match type of id
        Local Variable ->
          DUPi where i = 1 + stack_height - stack_height_of_identifier(id)
        Label ->
          // reference to be resolved during bytecode generation
          PUSH<bytecode position of label>
        SubAssembly ->
          PUSH<bytecode position of subassembly data>
    FunctionalAssemblyExpression(id ( arguments ) ) ->
      join(codegen(arg) for arg in arguments.reversed())
      id (which has to be an opcode, might be a function name later)
    AssemblyLocalDefinition(let (id1, ..., idn) := expr) ->
      register identifiers id1, ..., idn as locals in current block at current stack height
      codegen(expr) - assert that expr returns n items to the stack
    FunctionalAssemblyAssignment((id1, ..., idn) := expr) ->
      lookup id1, ..., idn in the syntactic stack of blocks, assert that they are variables
      codegen(expr)
      for j = n, ..., i:
      SWAPi where i = 1 + stack_height - stack_height_of_identifier(idj)
      POP
    AssemblyAssignment(=: id) ->
      look up id in the syntactic stack of blocks, assert that it is a variable
      SWAPi where i = 1 + stack_height - stack_height_of_identifier(id)
      POP
    LabelDefinition(name:) ->
      JUMPDEST
    NumberLiteral(num) ->
      PUSH<num interpreted as decimal and right-aligned>
    HexLiteral(lit) ->
      PUSH32<lit interpreted as hex and left-aligned>
    StringLiteral(lit) ->
      PUSH32<lit utf-8 encoded and left-aligned>
    SubAssembly(assembly <name> block) ->
      append codegen(block) at the end of the code
    dataSize(<name>) ->
      assert that <name> is a subassembly ->
      PUSH32<size of code generated from subassembly <name>>
    linkerSymbol(<lit>) ->
      PUSH32<zeros> and append position to linker table
    }
    
    展开全文
  • Solidity - 介绍

    千次阅读 2019-11-20 10:33:02
    章节Solidity – 介绍 Solidity – 环境搭建 Solidity – 基础语法 Solidity – 第一个程序 Solidity – 代码注释 Solidity – 数据类型 Solidity – 变量 Solidity – 变量作用域 Solidity – 运算符 Solidity –...
  • Solidity v0.5.0 重大更新 这部分Solidity v0.5.0 版本一写主要的不兼容的更新,了解更新背后的原因,以及影响了那些代码,可以查看更新日志. 注解 Contracts compiled with Solidity v0.5.0 can still interface...
  • solidity

    2020-01-31 01:53:51
    1、string ...pragma solidity ^0.4.0; contract StringTest{ string name = "xms"; function getName() public view returns(string){ return name; } function setName(string _name) publi...
  • Solidity介绍

    千次阅读 2018-11-10 10:57:53
    Solidity 是一个面向合约的高级语言,其语法类似于JavaScript 。是运行在以太坊虚拟机中的代码。 Solidity 是静态类型的编程语言,编译期间会检查其数据类型。支持继承、类和复杂的用户定义类型。 在线体验: ...
  • 第一集:Solidity语法讲解

    千次阅读 2018-10-19 19:57:41
    注释 1.代码注释 单行是// 多行是/**/  2.... 三斜杠/// ...1.值传递:简单来说就是拷贝,不会对原数据造成影响 ...2.引用传递:不是拷贝,会对原有的数据产生影响 ...4.引用类型(在某些情况下,复杂类型占用的空间比较大.....
  • Solidity

    2019-06-02 13:37:00
    1、Solidity的变量类型: 值类型 :这类变量在赋值或传参时,总是进行值拷贝。 布尔类型(Booleans) 整型(Integers) 定长浮点型(Fixed Point Numbers) 定长字节数组(Fixed-size byte arrays) 有理数和整型常量...
  • Solidity】6. 合约 - 深入理解Solidity

    千次阅读 2017-09-18 14:33:20
    合约Solidity的合约类似于面向对象语言的类。 它们包含可以修改这些变量的状态变量和函数中的持久性数据。 在不同的合同(实例)上调用函数将执行EVM函数调用,从而切换上下文以使状态变量无法访问。创建合约合同...
  • Solidity】3.类型 - 深入理解Solidity

    千次阅读 2017-09-18 14:31:57
    类型Solidity是一种静态类型的语言,这意味着每个变量(州和地方)的类型需要被指定的(或至少已知的 - 见下文型扣)在编译时。 Solidity提供了几种可以组合形成复杂类型的基本类型。另外,类型可以在含有运算符的...
  • 本地环境xiaoyu@LIXIAOYUdeMBP.com➤ npm -v 5.3.0 ❖ ~ [17:45:03] xiaoyu@LIXIAOYUdeMBP.com➤ node -v v8.4.0 安装我们选择最简单的npm安装的方式,在这里加上-g进行全局安装。...npm ERR! code EINTEGRI
  • 表达式和控制结构输入参数和输出参数与Javascript一样,函数可以将参数作为输入;... 例如,假设我们希望我们的合约接受一种具有两个整数的外部调用,我们会写下如下:pragma solidity ^0.4.0;contract Simple {
  • 以太坊:深入理解Solidity-Solidity汇编

    万次阅读 2019-05-08 13:02:54
    Solidity汇编 Solidity 定义了一种汇编语言,在没有 Solidity 的情况下也可以使用。这种汇编语言也可以嵌入到 Solidity 源代码中当作“内联汇编”使用。 我们从如何使用内联汇编开始,介绍它如何区别于独立汇编语言...
  • solidity定义一个组件语言,这个语言可以在没有Solidity下使用。该组件语言也能在Solidity源代码中被用作“在线组件”。我们从这样使用在线组件以及怎样区分其与脱机组件开始介绍,然后接下来介绍详细介绍组件。 ...
  • 深入理解Solidity——Solidity汇编

    千次阅读 2018-07-30 15:48:52
    Solidity汇编(Solidity Assembly) Solidity定义了一个汇编语言,可以不同Solidity一起使用。这个汇编语言还可以嵌入到Solidity源码中,以内联汇编的方式使用。下面我们将从内联汇编如何使用着手,介绍其与独立使用...
  • Solidity】8. 杂项 - 深入理解Solidity

    千次阅读 2017-09-18 14:30:36
    Solidity】1.一个Solidity源文件的布局 【Solidity】2.合约的结构体 【Solidity】3.类型 【Solidity】4.单位和全局可变量 【Solidity】5.表达式和控制结构 【Solidity】6. 合约 【Solidity】7. 部件 ...
1 2 3 4 5 ... 20
收藏数 12,455
精华内容 4,982
关键字:

solidity