为您推荐:
精华内容
最热下载
问答
  • 1.54MB weixin_43405220 2019-09-03 21:03:32
  • 1.4MB u010940941 2019-08-06 17:26:27
  • 1.27MB totoz 2018-04-03 10:02:38
  • 1.6MB xiaoqiangc 2018-01-11 12:45:06
  • Solidity是一个静态类型的语言,在编译时需要明确指定变量的类型,主要分类值类型,引用类型。 值类型包括: 布尔 整形 地址 定长字节数组 有理数和整型 枚举类型 函数 值类型在传值时,会临时拷贝一份内容...

    1. 值类型和引用类型

    • Solidity是一个静态类型的语言,在编译时需要明确指定变量的类型,主要分类值类型,引用类型。

    • 值类型包括:

      1. 布尔
      2. 整形
      3. 地址
      4. 定长字节数组
      5. 有理数和整型
      6. 枚举类型
      7. 函数
    • 值类型在传值时,会临时拷贝一份内容出来,而不是拷贝指针,当你修改新的变量时候,原来值不会发生改变。

    • 引用类型包括:

      1. 不定长字节数组
      2. 字符串
      3. 数组
      4. 结构体
    • 引用类型,赋值时,我们可以值传递,也可以引用即地址传递,如果是值传递,和上面的案例一样,修改新变量时,不会影响原来的变量值,如果是引用传递,那么当你修改新变量时,原来变量的值会跟着变化,这是因为新就变量同时指向同一个地址的原因。

    • 智能合约说白了就是跑在区块链上的一段程序。

      pragma solidity ^0.4.4;
      contract Counter {
          uint count = 0;
          address owner;
          function Counter() {
             owner = msg.sender;
          } 
          function increment() public {
             uint step = 10;
             if (owner == msg.sender) {
                count = count + step;
             }
          }
          function getCount() constant returns (uint) {
             return count;
          }
          function kill() {
             if (owner == msg.sender) { 
                selfdestruct(owner);
             }
          }
      }
      
    • 版本声明 :pragma solidity ^0.4.4;

    • 状态变量:uint count = 0;address owner;

    • 属性变量

    • 构造函数(Contructor):function Counter()函数名和合约名相同时,此函数是合约的构造函数,当合约对象创建时,会先调用构造函数对相关数据进行初始化处理

    • 成员函数:Counter合约里面的成员函数,成员函数在iOS里面叫做方法、行为,合约实例可以调用成员函数处理相关操作。

    • 本地变量:increment()方法中声明的step就是局部变量。局部变量只在离它最近的{}内容使用

    • 析构函数:(selfdestruct):析构函数和构造函数对应,构造函数是初始化数据,而析构函数是销毁数据。在counter合约中,当我们手动调用kill函数时,就会调用selfdestruct(owner)销毁当前合约

    2. memory,storange

    • 一段代码认识状态变量和局部变量

      pragma solidity ^0.4.4;
      contract Person {
          int public _age;
          string public _name;
          function Person(int age,string name) {
                _age = age;
                _name = name;
          }
          function f(string name) {
                var name1 = name;
          }
      }
      
    • 在这段代码中,_age,_name就属于状态变量,Person(int age,string name)中的age和name,还有f(string name)中的name以及f()函数中声明的name1都默认属于本地/局部变量。

    • 代码演示(值类型)

      pragma solidity ^0.4.4;
      contract Person {
          int public _age;
          function Person(int age) {
            _age = age;
          }
          function f() {
            midifyAge(_age);
          }
          function midifyAge(int age) {
            age = 100;
          }
      }
      
    • 代码演示(引用类型)-引用类型的变量有两种类型,分别是memory和storage

      • memory(值传递)
      • 引用类型作为函数参数,类型默认为memory,函数参数为memory类型的变量给一个变量赋值时,这个变量的类型必须和函数参数类型一致。可以写成 string memory name1 = name,或者var name1=name,var声明一个变量时,这个变量的类型最终由赋值给他的值的类型决定。
      • 任何函数参数当它的类型为引用类型时,这个函数参数都默认为memory类型,memory类型的变量会临时拷贝一份值存储到内存中,当我们将这个参数值赋给一个新的变量,并尝试去修改这个新的变量的值时,最原始的变量的值并不会发生变化
      • 当创建合约时,_name的值为liyuechun,当我们调用f()函数时,f()函数中会将_name的值赋给临时的memory变name,换句话说,因为name的类型为memory,所以name和_name会分别指向不同的对象,当我们尝试去修改name指针指向的值时,_name所指向的内容不会发生变化
      pragma solidity ^0.4.4;
      contract Person {
          string public  _name;
          function Person() {
              _name = "liyuechun";
          }
          function f() {
              modifyName(_name);
          }
          function modifyName(string name)  {
              var name1 = name;
              bytes(name1)[0] = 'L';
          }
      }
      
    • storage(指针传递)

    • 当函数参数为memory类型时,相当于值传递,而storage类型的函数参数将是指针传递如果想要在modifyName函数中通过传递过来的指针修改_name的值,那么必须将函数参数的类型显示设置为storage类型,storage类型拷贝的不是值,而是_name指针,当调用modifyName(_name)函数时,相当于同时有_name,name,name1三个指针同时指向同一个对象,我们可以通过三个指针中的任何一个指针修改他们共同指向的内容的值。

      pragma solidity ^0.4.4;
      contract Person {
          string public  _name;
          function Person() {
              _name = "liyuechun";
          }
          function f() {
              modifyName(_name);
          }
          function modifyName(string storage name)  {
              var name1 = name;
              bytes(name1)[0] = 'L';
          }
      }
      
    • 函数默认是public类型

    • 函数参数如果是storage类型时,函数的类型必须是internal或者private

    3. Solidity Types-布尔(Booleans)

    • bool:可能取值为true和false

    • 支持的运算符

      • !逻辑非
      • &&逻辑与
      • ||逻辑或
      • ==等于
      • !=不等于
      • 运算符&&和||是短路运算符,如f(x)||g(y),当f(x)为真时,则不会继续执行g(y)在f(x)&&g(y)表达式中,当f(x)为false时,则不会执行g(y)
    • 案例

      bool a = true;
      bool b = !a;
      // a == b -> false
      // a != b -> true
      // a || b -> true
      // a && b -> false
      //
      pragma solidity ^0.4.4;
      contract Test {
          uint _a;
          uint _b;
          bool _c;
          function Test() {
              _a = 100;
              _b = 200;
              _c = true;
          }
          // ! 逻辑非
          function fei() constant returns (bool) {
              return (!_c);
          }
          // && 逻辑与  
          // == 等于
          function luojiyu() constant returns (bool) {
              return _a == _b && _c; 
          }
          // || 逻辑或
          // != 不等于
          function luojihuo() constant returns (bool) {
              return _a != _b || _c; 
          }
      }
      

    4. Solidity Types - 地址(Address)

    • 以太坊钱包地址位数验证:以太坊中的地址的长度有20字节,一字节等于8位,一共160位,所以address可以用uint160来声明。

    • 一个以太坊钱包的地址是0xF055775eBD516e7419ae486C1d50C682d4170645,0x代表十六进制,我们将F055775eBD516e7419ae486C1d50C682d4170645拷贝,如下图所示,将其进行二进制转换,不难发现,它的二进制刚好160位。

    • mes.sender:就是当前调用方法的发起人,一个合约部署后,通过钱包地址操作合约的人很多,但是如何正确判断谁是合约的拥有者呢?判断方法很简单,就是第一次部署合约时候,谁出的gas,谁就对合约具有拥有权。

      pragma solidity ^0.4.4;
      contract Test {
          address public _owner;
          uint public _number;
          function Test() {
              _owner = msg.sender;
              _number = 100;
          }
          function msgSenderAddress() constant returns (address) {
              return msg.sender;
          }
          function setNumberAdd1() {
              _number = _number + 5;
          }
          function setNumberAdd2() {
              if (_owner == msg.sender) {
                  _number = _number + 10;
              }
          }
      }
      
    • 合约地址:一个合约部署后,会有一个合约地址,这个合约地址就是代表合约自己。

      pragma solidity ^0.4.4;
      // 0x903ad08970c70d10e5fb5b3c26f7b714830afcf6
      // 0x62e40877f4747e06197aa1a2b9ac06dd9bb244a3
      // 0xf055775ebd516e7419ae486c1d50c682d4170645
      // 0xe7795e05d15f7406baf411cafe766fc28eccf35f
      // 0xe7795e05d15f7406baf411cafe766fc28eccf35f
      contract Test {
          address public _owner;
          uint public _number;
          function Test() {
              _owner = msg.sender;
              _number = 100;
          }
          function msgSenderAddress() constant returns (address) {
              return msg.sender;
          }
          function setNumberAdd1() {
              _number = _number + 5;
          }
          function setNumberAdd2() {
              if (_owner == msg.sender) {
                  _number = _number + 10;
              }
          }
          function returnContractAddress() constant returns (address) {
              return this;
          }
      }在这里插入代码片
      
    • 支持的运算符

      pragma solidity ^0.4.4;
      contract Test {
          address address1;
          address address2;
          // <=,<,==,!=,>=和>
          function Test() {
              address1 = 0xF055775eBD516e7419ae486C1d50C682d4170645;
              address2 = 0xEAEC9B481c60e8cDc3cdF2D342082C349E5D6318;
          }
          // <=
          function test1() constant returns (bool) {
              return address1 <= address2;
          }
          // <
          function test2() constant returns (bool) {
              return address1 < address2;
          }
          //  != 
          function test3() constant returns (bool) {
              return address1 != address2;
          }
          // >=
          function test4() constant returns (bool) {
              return address1 >= address2;
          }
          // >
          function test5() constant returns (bool) {
              return address1 > address2;
          }
      }
      
    • 成员变量和函数

    • balance:如果我们需要查看一个地址的余额,可以用balance属性查看

      pragma solidity ^0.4.4;
      contract addressBalance{
          function getBalance(address addr) constant returns (uint){
              return addr.balance;
          }
      }
      
    • this查看当前合约地址的余额

      pragma solidity ^0.4.4;
      contract addressBalance{
          function getBalance(address addr) constant returns (uint){
              return this.balance;
          }
      }
      
    • transfer:从合约发起方向某个地址转入亿台币(单位是:wei),地址无效或者合约发起方余额不足时,代码抛出异常并停止转账

    • send:send相对transger方法较为底层,不过使用方法和transger相同,都是从合约发起方向某个地址转入以太币(单位时wei),地址无效或者合约发起方余额不足时,send不会抛出异常,而是直接返回false.两者不同,

    • send()方法执行时有一些风险

      • 调用递归深度不能超过1024
      • 如果gas不够,执行会失败
      • 所以使用这个方法要检查成功与否
      • transger相对send较安全

    5. Solidity Types - 字符串(String Literals)

    • 字符串可以通过“”或者来表示字符串的值。

      pragma solidity ^0.4.4;
      contract StringLiterals{ 
          string  _name; // 状态变量
          //构造函数
          function StringLiterals() {
              // 微信号初始化
              _name = "boss2967";
          }
          // set方法
          function setString(string name) {
              _name = name;
          }
          // get方法
          function name() constant returns (string) {
              return _name;
          }
      }
      
    • string字符串不能通过length方法获取其长度

    06.Solidity Types - 固定大小字节数组(Fixed-size byte arrays)

    • 固定大小字节数组可以通过 bytes1, bytes2, bytes3, …, bytes32来进行声明。
      • byte的别名就是 byte1
      • bytes1只能存储一个字节,也就是二进制8位的内容。
      • bytes2只能存储两个字节,也就是二进制16位的内容。
      • bytes3只能存储三个字节,也就是二进制24位的内容。
        ……
      • bytes32能存储三十二个字节,也就是二进制32 * 8 = 256位的内容
      pragma solidity ^0.4.4;
      contract C {
          // 0x6c697975656368756e
          byte public a = 0x6c; // 0110 1100
          bytes1 public b = 0x6c; // 0110 1100
          bytes2 public c = 0x6c69; // 0110 1100 0110 1001
          bytes3 public d = 0x6c6979; // 0110 1100 0110 1001 0111 1001
          bytes4 public e = 0x6c697975; // 0110 1100 0110 1001 0111 1001 0111 0101
          // ...
          bytes8 public f = 0x6c69797565636875; // 0110 1100 0110 1001 0111 1001 0111 0101 0110 0101 0110 0011 0110 1000 0111 0101
          bytes9 public g = 0x6c697975656368756e; // // 0110 1100 0110 1001 0111 1001 0111 0101 0110 0101 0110 0011 0110 1000 0111 0101 0110 1110
      }
      
    • byte和bytes1等价,只能存一个字节,当超过他的存储范围就会报错
    • 操作运算符:
      • 比较运算符:<=, <, ==, !=, >=, >
      pragma solidity ^0.4.24;
      contract C{  
          // 1 2 3 4 5 6 7 8 9 A B C D E F 
          bytes1 b10 = 0x6c; // l -> 0110 1100  -> 12 * 1 + 6 * 16  = 108
          bytes1 b11 = 0x69; // i -> 0110 1001  -> 9 * 1 + 6 * 16 = 105
          // <=, <, ==, !=, >=, >
          function test1() constant returns (bool) {
              return b10 <= b11; // false
          }
          function test2() constant returns (bool) {
              return b10 < b11; // false
          }
          function test3() constant returns (bool) {
              return b10 == b11; // false
          }
          function test4() constant returns (bool) {
              return b10 >= b11; // true
          }
          function test5() constant returns (bool) {
              return b10 > b11; // true
          }  
      }
      
    • 位操作符:&, |, ^(异或), ~ (取反), << (左移), >> (右移)
      pragma solidity ^0.4.4;
      contract C{ 
          bytes1 b10 = 0x6c; // l -> 0110 1100  -> 12 * 1 + 6 * 16  = 108
          bytes1 b11 = 0x69; // i -> 0110 1001  -> 9 * 1 + 6 * 16 = 105
          //   &, |, ^(异或), ~ (取反), << (左移), >> (右移)
          function test1() constant returns (bytes1) {
              return b10 & b11;
              // 0110 1100 -> 0x6c
              // 0110 1001 -> 0x69
              // 0110 1000 -> 0x68
          }
          function test2() constant returns (bytes1) {
              return b10 | b11;
              // 0110 1100 -> 0x6c
              // 0110 1001 -> 0x69
              // 0111 1101 -> 0x6d
          }
          function test3() constant returns (bytes1) {
              return ~b10;
              // 0110 1100 -> 0x6c
              // 1001 0011 -> 0x93
          }
          function test4() constant returns (bytes1) {
              return b10 << 1;
              // 0110 1100 -> 0x6c
              // 1101 1000 -> 0xd8
          }
           function test5() constant returns (bytes1) {
              return b10 >> 1;
              // 0110 1100 -> 0x6c
              // 0011 0110 -> 0x36
          } 
      }
      
    • 索引访问:如果x是一个bytesI,那么可以通过x[k](0 < k < I)获取对应索引的字节,**PS:**x[k]是只读,不可写
    • 成员函数:.length返回字节的个数
      pragma solidity ^0.4.4;
      contract C {
          bytes9 public g = 0x6c697975656368756e;
          function gByteLength() constant returns (uint) {
              return g.length;
          }
      }
      
    • 不可变深度解析:长度不可变,内部字节不可修改
      • 长度不可变
      pragma solidity ^0.4.4;
      contract C {
          bytes9  name = 0x6c697975656368756e;
          function setNameLength(uint length) {
              // 报错
              name.length = length;
          }
      }
      
      • 内部字节不可修改
      pragma solidity ^0.4.4;
      contract C {
          bytes9  name = 0x6c697975656368756e;
          function setNameFirstByte(byte b) {
              name[0] = b;
          }
      }
      

    07. Solidity Types - 动态大小字节数组(Dynamically-sized byte array)

    • string是一个动态尺寸的UTF-8编码字符串,它其实是特殊的可变字节数组,string是引用类型,而非值类型。

    • bytes动态字节数组,是引用类型。

    • 在不确定字节数据大小的情况下,可以使用string后者bytes,如果确定知道或者能够将字节数量控制在bytes1-bytes32,那么我们就使用bytes1-bytes32,为什么要这样选择?因为后者可以降低储存成本。

    • 常规字符串string转换为bytes

    • string字符串中没有提供length方法获取字符串长度,也没有提供方法修改某个索引的字节码,不过我们可以将string转换为bytes,再调用length方法获取字节长度,当然可以修改某个索引的字节码。

      pragma solidity ^0.4.4;
      contract C {
          bytes9 public g = 0x6c697975656368756e;
          string public name = "abcdefght";
          function gByteLength() constant returns (uint) {
              return g.length;
          }
          function nameBytes() constant returns (bytes) {
              return bytes(name);
          }
          function nameLength() constant returns (uint) {
              return bytes(name).length;
          }
          function setNameFirstByteForL(bytes1 z) {
              // 0x4c => "L"
              bytes(name)[0] = z;
          }
      
    • 说明

      function nameBytes() constant returns (bytes) {
          return bytes(name);
      }
      
    • nameBytes这个函数的功能是将字符串name转换为bytes,并且返回的结果为0x6c697975656368756e。0x6c697975656368756e一共为9字节,也就是一个英文字母对应一个字节。

      function nameLength() constant returns (uint) {
          return bytes(name).length;
      }
      
    • string字符串它并不提供length方法帮助我们返回字符串的长度,所以在nameLength方法中,我们将name转换为bytes,然后再调用length方法来返回字节数,因为一个字节对应一个英文字母,所以返回的字节数量刚好等于字符串的长度。

      function setNameFirstByteForL(bytes1 z) {
          // 0x4c => "L"
          bytes(name)[0] = z;
      }
      
    • 将name字符串中的某个字母进行修改,那么我们直接通过x[k] = z的形式进行修改即可。

    • 汉字字符串或特殊字符的字符串转换为bytes:特殊字符,中文字符串

      • 特殊字符
      pragma solidity ^0.4.24;
      contract C {
          string public name = "a!+&52230";
          function nameBytes() constant returns (bytes) {
              return bytes(name);
          }
          function nameLength() constant returns (uint) {
              return bytes(name).length;
          }
      }
      //声明了一个name字符串,值为a!+&520,根据nameBytes和nameLength返回的结果中,我们不难看出,不管是字母、数字还是特殊符号,每个字母对应一个byte(字节)
      
      • 中文字符串
      pragma solidity ^0.4.4;
      contract C {
          string public name = "张三丰";
          function nameBytes() constant returns (bytes) {
              return bytes(name);
          }
          function nameLength() constant returns (uint) {
             return bytes(name).length;
          }
      }
      //在上面的代码中,我们不难看出,张三丰转换为bytes以后的内容为0xe9bb8ee8b783e698a5,一共9个字节。也就是一个汉字需要通过3个字节来进行存储。那么问题来了,以后我们取字符串时,字符串中最好不要带汉字,否则计算字符串长度时还得特殊处理
      
    • 字符串中最好不要带汉字,否则计算字符串长度时还得特殊处理。

    • 创建bytes字节数组

      pragma solidity ^0.4.24;
      contract C {
          bytes public name = new bytes(1);
          function setNameLength(uint length) {
              name.length = length;
          }
          function nameLength() constant returns (uint) {
              return name.length;
          }
      }
      
    • bytes可变数组length和push两个函数的使用案例

      pragma solidity ^0.4.4;
      contract C {
          // 0x6c697975656368756e
          // 初始化一个两个字节空间的字节数组
          bytes public name = new bytes(2);
          // 设置字节数组的长度
          function setNameLength(uint len) {
              name.length = len;
          }
          // 返回字节数组的长度
          function nameLength() constant returns (uint) {
              return name.length;
          }
          // 往字节数组中添加字节
          function pushAByte(byte b) {
              name.push(b);
          }
      }
      

      08.Solidity Types - (Dynamically-sized byte array)、(Fixed-size byte arrays)、string之间的转换关系

    • 固定大小字节通过bytes0,bytes32进行声明,固定大小字节数组的长度不可变,内容不可修改。

      pragma solidity ^0.4.4;
      contract C {
         bytes9 name9 = 0x6c697975656368756e;
         function bytes9ToBytes1() constant returns (bytes1) {
             return bytes1(name9);
         }
         function bytes9ToBytes2() constant returns (bytes2) {
             return bytes2(name9);
         }
         function bytes9ToBytes32() constant returns (bytes32) {
             return bytes32(name9);
         }
      }
      

    09.Solidity Types - Solidity 枚举(Enums)

    • ActionChoices就是一个自定义的整型,当枚举数不够多时,它默认的类型为uint8,当枚举数足够多时,它会自动变成uint16,下面的GoLeft == 0,GoRight == 1, GoStraight == 2, SitStill == 3。在setGoStraight方法中,我们传入的参数的值可以是0 - 3当传入的值超出这个范围时,就会中断报错

      pragma solidity ^0.4.4;
      contract test {
          enum ActionChoices { GoLeft, GoRight, GoStraight, SitStill }
          ActionChoices _choice;
          ActionChoices constant defaultChoice = ActionChoices.GoStraight;
          function setGoStraight(ActionChoices choice) public {
              _choice = choice;
          }
          function getChoice() constant public returns (ActionChoices) {
              return _choice;
          }
          function getDefaultChoice() pure public returns (uint) {
              return uint(defaultChoice);
          }
      }
      

    10.Solidity Types - Solidity 结构体(Structs)

    • 自定义结构体

      pragma solidity ^0.4.4;
      contract Students {
          struct Person {
              uint age;
              uint stuID;
              string name;
          }
      }
      
    • Person就是我们自定义的一个新的结构体类型,结构体里面可以存放任意类型的值。

    • 初始化一个结构体:初始化一个storage类型的状态变量

      pragma solidity ^0.4.4;
      contract Students {
          struct Person {
              uint age;
              uint stuID;
              string name;
          }
          Person _person = Person(18,101,"zhangsanfeng");
      }
      
      pragma solidity ^0.4.4;
      contract Students {
          struct Person {
              uint age;
              uint stuID;
              string name;
          }
          Person _person = Person({age:18,stuID:101,name:"zhangsanfeng"});
      }
      
    • 初始化一个memory类型的变量

      pragma solidity ^0.4.4;
      contract Students {
          struct Person {
              uint age;
              uint stuID;
              string name;
          }
          function personInit() {
              Person memory person = Person({age:18,stuID:101,name:"zhangsanfeng"});
          }
      }
      
    展开全文
    boss2967 2018-12-18 11:35:41
  • 中文翻译文档: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
    }
    

    感谢您的支持


    展开全文
    xiatiancc 2017-12-27 11:27:44
  • 原文地址:http://solidity.readthedocs.io/en/latest/solidity-in-depth.html本节提供您需要了解的有关Solidity的所有信息。 如果这里缺少某些东西,请与Gitter联系或者在Github上发出pull请求。一、纯文件的布局...

    原文地址:http://solidity.readthedocs.io/en/latest/solidity-in-depth.html

    本节提供您需要了解的有关Solidity的所有信息。 如果这里缺少某些东西,请与Gitter联系或者在Github上发出pull请求。

    一、纯文件的布局

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

    pragma版本

    源文件可以使用所谓的pragma版本进行注释,以防将来可能引入不兼容更改的编译器版本进行编译。 我们试图将这种变化保持在绝对最小值,特别是以语义上的变化也需要改变语法的方式引入变化,但这当然不能完全保证。 因此,对于包含中断更改的版本,通读更新日志是一个好办法,这些版本将始终具有0.x.0或x.0.0格式的版本。

    pragma版本使用如下:
    pragma solidity ^0.4.0;

    这样的源文件将不会使用早于版本0.4.0的编译器进行编译,并且它也不能在编译器开始版本0.5.0(第二个条件通过使用^添加)上工作。 这个想法是,在版本0.5.0之前不会有任何突变,所以我们可以始终确保我们的代码将按照我们的意图编译。 我们不修复编译器的确切版本,所以仍然可能是bug修正版本。

    可以为编译器版本指定更复杂的规则,表达式遵循npm使用的表达式。

    导入其他源文件

    1.语法和语义

    Solidity支持与JavaScript(从ES6开始)中可用的import语句非常相似的import语句,尽管Solidity不知道“default export”的概念。

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

    import "filename";

    该语句将“filename”(和导入的符号)的所有全局符号导入当前全局范围(与ES6中的不同,但对于Solidity向后兼容)。

    import * as symbolName from "filename";

    …创建一个新的全局符号symbolName,其成员都是来自“filename”的全局符号。

    import {symbol1 as alias, symbol2} from "filename";

    …创建新的全局符号alias和symbol2,它们分别从“filename”引用symbol1和symbol2。

    另一种语法不是ES6的一部分,但可能很方便:

    import "filename" as symbolName;

    相当于import * as symbolName from "filename";

    2.路径

    文件名总是作为一个路径用/作为目录分隔符, ..作为父目录。当 . 或 .. 后跟一个字符而不是/,它不被认为是当前或父目录。 所有路径名称都被视为绝对路径,除非它们以当前路径 . 或父目录 … 开始。

    要从当前文件所在的目录导入文件x,请使用import“./x”作为x。 如果使用import“x”作为x,可能引用的是不同的文件(在全局“包括目录”中)。

    它取决于编译器(见下文)如何实际解析路径。 一般来说,目录层次结构不需要严格映射到您的本地文件系统,它也可以映射到通过例如 ipfs,http或git。

    3.在实际编译器中使用

    当编译器被调用时,不仅可以指定如何发现路径的第一个元素,而且可以指定路径前缀重映射,例如:github.com/ethereum/dapp-bin/library重映射到/ usr/local/dapp-bin/library,编译器将从那里读取文件。 如果可以应用多个重新映射,则首先尝试使用最长键。 这允许使用例如图2的“回退重新映射”。“”映射到“/usr/local/include/solidity”。 此外,这些重新映射可以取决于上下文,这允许您配置要导入的包。 例如不同版本的同名库。

    solc:
    对于solc(命令行编译器),这些重映射作为上下文提供:prefix = target参数,其中上下文:和=目标部分都是可选的(在这种情况下,目标默认为prefix)。 编译所有重新映射值是常规文件(包括它们的依赖关系)。 这种机制是完全向后兼容的(只要没有文件名包含=或:),因此不是破坏性的更改。 在导入以前缀开头的文件的目录上下文中的文件中的所有导入都通过以目标替换前缀来重定向。

    例如,如果您将github.com/ethereum/dapp-bin/本地克隆到/usr/local/ dapp-bin,则可以在源文件中使用以下内容:

    import "github.com/ethereum/dapp-bin/library/iterable_mapping.sol" as it_mapping;

    然后运行编译器

    solc github.com/ethereum/dapp-bin/=/usr/local/dapp-bin/ source.sol

    一个更复杂的例子,假设你依赖一些使用非常老版本的dapp-bin的模块。 旧版本的dapp-bin被检出在/usr/local/dapp-bin_old,那么你可以使用

    solc module1:github.com/ethereum/dapp-bin/=/usr/local/dapp-bin/ \
         module2:github.com/ethereum/dapp-bin/=/usr/local/dapp-bin_old/ \
         source.sol

    以便module2中的所有导入都指向旧版本,但在module1中导入获得新版本。

    请注意,solc只允许包含来自某些目录的文件:它们必须位于某个明确指定的源文件的目录(或子目录)中,或者位于重新映射目标的目录(或子目录)中。 如果要允许直接绝对包含,只需添加remapping = /。

    如果存在导致有效文件的多个重新映射,则选择具有最长公共前缀的重新映射。

    browser-solidity:
    基于浏览器的编译器提供了github的自动重映射,并且还将通过网络自动检索文件:您可以导入可迭代映射通过:

    import "github.com/ethereum/dapp-bin/library/iterable_mapping.sol" as it_mapping;

    将来可能会添加其他源代码提供程序。

    注释

    单行注释(//)和多行注释(//)都可以。

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

    此外,还有另一种类型的注释,称为natspec注释,文档尚未写入。 它们使用三斜线(///)或双星号(/ * … /)编写,并且应该直接在函数声明或语句之上使用。 您可以在这些注释中使用Doxygen风格的标记来记录函数,注释形式验证的条件,并提供确认文本,当用户尝试调用函数时向用户显示。

    在下面的示例中,我们记录了合同的标题,两个输入参数的说明和两个返回值。

     pragma solidity ^0.4.0;
    
    /** @title Shape calculator.*/
    contract shapeCalculator{
        /**@dev Calculates a rectangle's surface and perimeter.
         * @param w Width of the rectangle.
         * @param h Height of the rectangle.
         * @return s The calculated surface.
         * @return p The calculated perimeter.
         */
        function rectangle(uint w, uint h) returns (uint s, uint p) {
            s = w * h;
            p = 2 * (w + h);
        }
    }

    二、合约的结构

    solidity写的合约类似于面向对象语言中的类。 每个合约可以包含状态变量,函数,函数修饰符,事件,结构类型和枚举类型的声明。 此外,合约可以继承其他合约。

    状态变量

    状态变量是永久存储在合约存储中的值。

    pragma solidity ^0.4.0;
    
    contract SimpleStorage {
        uint storedData; // State variable
        // ...
    }

    函数

    函数是合约中代码的可执行单元。

    pragma solidity ^0.4.0;
    
    contract SimpleAuction {
        function bid() payable { // Function
            // ...
        }
    }

    函数调用可以在内部或外部发生,对其他合同具有不同级别的可见性(后面会有详细讲解)。

    函数修饰符

    函数修饰符可以用于以声明方式修改函数的语义。

    pragma solidity ^0.4.0;
    
    contract Purchase {
        address public seller;
    
        modifier onlySeller() { // Modifier
            if (msg.sender != seller) throw;
            _;
        }
    
        function abort() onlySeller { // Modifier usage
            // ...
        }
    }

    事件

    事件是EVM日志记录工具的便利接口。

    pragma solidity ^0.4.0;
    
    contract SimpleAuction {
        event HighestBidIncreased(address bidder, uint amount); // Event
    
        function bid() payable {
            // ...
            HighestBidIncreased(msg.sender, msg.value); // Triggering event
        }
    }

    有关如何声明事件以及如何从dapp中使用事件的信息,请参阅后续部分。

    结构类型

    结构是可以将多个变量放在一组的自定义类型。

    pragma solidity ^0.4.0;
    
    contract Ballot {
        struct Voter { // Struct
            uint weight;
            bool voted;
            address delegate;
            uint vote;
        }
    }

    枚举类型

    枚举可用于创建具有有限值集合的自定义类型。

    pragma solidity ^0.4.0;
    
    contract Purchase {
        enum State { Created, Locked, Inactive } // Enum
    }
    展开全文
    ler_123 2017-01-09 16:54:24
  • 1.28MB lzq894880417 2019-04-02 17:10:24
  • 173KB weixin_42101056 2021-02-05 08:25:42
  • 3.96MB a1070060198 2018-05-22 05:17:29
  • 5星
    782KB sinat_36088709 2018-07-20 16:51:20
  • 4星
    1.5MB shebao3333 2018-03-07 16:32:12
  • Solidity AssemblySolidity定义了一个汇编语言,可以不同Solidity一起使用。...文档尚待完善的补充的地方:待补充内联汇编的变量作用域的不同,尤其是使用含internal的函数的库时所引入的复杂度。另外,还需补充,编...

    Solidity Assembly

    Solidity定义了一个汇编语言,可以不同Solidity一起使用。这个汇编语言还可以嵌入到Solidity源码中,以内联汇编的方式使用。下面我们将从内联汇编如何使用着手,介绍其与独立使用的汇编语言的不同,最后再介绍这门汇编语言。

    文档尚待完善的补充的地方:待补充内联汇编的变量作用域的不同,尤其是使用含internal的函数的库时所引入的复杂度。另外,还需补充,编译器定义的符号(symbols)。

    内联汇编

    通常我们通过库代码,来增强语言我,实现一些精细化的控制,Solidity为我们提供了一种接近于EVM底层的语言,内联汇编,允许与Solidity结合使用。由于EVM是栈式的,所以有时定位栈比较麻烦,Solidty的内联汇编为我们提供了下述的特性,来解决手写底层代码带来的各种问题:

    • 允许函数风格的操作码: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))) } }

    下面将详细介绍内联编译(inline assembly)语言。

    需要注意的是内联编译是一种非常底层的方式来访问EVM虚拟机。他没有Solidity提供的多种安全机制。

    示例

    下面的例子提供了一个库函数来访问另一个合约,并把它写入到一个bytes变量中。有一些不能通过常规的Solidity语言完成,内联库可以用来在某些方面增强语言的能力。

    pragma solidity ^0.4.0;
    
    library GetCode {
        function at(address _addr) returns (bytes o_code) {
            assembly {
                // retrieve the size of the code, this needs assembly
                let size := extcodesize(_addr)
                // allocate output byte array - this could also be done without assembly
                // by using o_code = new bytes(size)
                o_code := mload(0x40)
                // new "memory end" including padding
                mstore(0x40, add(o_code, and(add(add(size, 0x20), 0x1f), not(0x1f))))
                // store length in memory
                mstore(o_code, size)
                // actually retrieve the code, this needs assembly
                extcodecopy(_addr, add(o_code, 0x20), 0, size)
            }
        }
    }
    

    内联编译在当编译器没办法得到有效率的代码时非常有用。但需要留意的是内联编译语言写起来是比较难的,因为编译器不会进行一些检查,所以你应该只在复杂的,且你知道你在做什么的事情上使用它。

    pragma solidity ^0.4.0;
    
    library VectorSum {
        // This function is less efficient because the optimizer currently fails to
        // remove the bounds checks in array access.
        function sumSolidity(uint[] _data) returns (uint o_sum) {
            for (uint i = 0; i < _data.length; ++i)
                o_sum += _data[i];
        }
    
        // We know that we only access the array in bounds, so we can avoid the check.
        // 0x20 needs to be added to an array because the first slot contains the
        // array length.
        function sumAsm(uint[] _data) returns (uint o_sum) {
            for (uint i = 0; i < _data.length; ++i) {
                assembly {
                    o_sum := mload(add(add(_data, 0x20), mul(i, 0x20)))
                }
            }
        }
    }
    

    语法

    内联编译语言也会像Solidity一样解析注释,字面量和标识符。所以你可以使用///**/的方式注释。内联编译的在Solidity中的语法是包裹在assembly { ... },下面是可用的语法,后续有更详细的内容。

    • 字面量。如0x12342abc(字符串最多是32个字符)
    • 操作码(指令的方式),如mload sload dup1 sstore,后面有可支持的指令列表
    • 函数风格的操作码,如add(1, mlod(0)
    • 标签,如name:
    • 变量定义,如let x := 7 或 let x := add(y, 3)
    • 标识符(标签或内联局部变量或外部),如jump(name)3 x add
    • 赋值(指令风格),如,3 =: x
    • 函数风格的赋值,如x := add(y, 3)
    • 支持块级的局部变量,如{ let x := 3 { let y := add(x, 1) } }

    操作码

    这个文档不想介绍EVM虚拟机的完整描述,但后面的列表可以做为EVM虚拟机的指令码的一个参考。

    如果一个操作码有参数(通过在栈顶),那么他们会放在括号。需要注意的是参数的顺序可以颠倒(非函数风格,后面会详细说明)。用-标记的操作码不会将一个参数推到栈顶,而标记为*的是非常特殊的,所有其它的将且只将一个推到栈顶。

    在后面的例子中,mem[a...b)表示成位置a到位置b(不包含)的memory字节内容,storage[p]表示在位置pstrorage内容。

    操作码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

    字面量

    你可以使用整数常量,通过直接以十进制或16进制的表示方式,将会自动生成恰当的pushi指令。

    assembly { 2 3 add "abc" and }
    

    上面的例子中,将会先加2,3得到5,然后再与字符串abc进行与运算。字符串按左对齐存储,且不能超过32字节。

    函数风格

    你可以在操作码后接着输入操作码,它们最终都会生成正确的字节码。比如:

    3 0x80 mload add 0x80 mstore
    

    下面将会添加3memory中位置0x80的值。

    由于经常很难直观的看到某个操作码真正的参数,Solidity内联编译提供了一个函数风格的表达式,上面的代码与下述等同:

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

    函数风格的表达式不能在内部使用指令风格,如1 2 mstore(0x80, add)将不是合法的,必须被写为mstore(0x80, add(2, 1))。那些不带参数的操作码,括号可以忽略。

    需要注意的是函数风格的参数与指令风格的参数是反的。如果使用函数风格,第一个参数将会出现在栈顶。

    访问外部函数与变量

    Solidity中的变量和其它标识符,可以简单的通过名称引用。对于memory变量,这将会把地址而不是值推到栈上。Storage的则有所不同,由于对应的值不一定会占满整个storage槽位,所以它的地址由槽和实际存储位置相对起始字节偏移。要搜索变量x指向的槽位,使用x_slot,得到变量相对槽位起始位置的偏移使用x_offset

    在赋值中(见下文),我们甚至可以直接向Solidity变量赋值。

    还可以访问内联编译的外部函数:内联编译会推入整个的入口的label(应用虚函数解析的方式)。Solidity中的调用语义如下:

    • 调用者推入返回的label,arg1,arg2, ... argn
    • 调用返回ret1,ret2,..., retm

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

    pragma solidity ^0.4.11;
    
    contract C {
        uint b;
        function f(uint x) returns (uint r) {
            assembly {
                r := mul(x, sload(b_slot)) // ignore the offset, we know it is zero
            }
        }
    }
    

    标签

    另一个在EVM的汇编的问题是jumpjumpi使用了绝对地址,可以很容易的变化。Solidity内联汇编提供了标签来让jump跳转更加容易。需要注意的是标签是非常底层的特性,尽量使用内联汇编函数,循环,Switch指令来代替。下面是一个求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)
    }
    

    需要注意的是自动访问栈元素需要内联者知道当前的栈高。这在跳转的源和目标之间有不同栈高时将失败。当然你也仍然可以在这种情况下使用jump,但你最好不要在这种情况下访问栈上的变量(即使是内联变量)。

    此外,栈高分析器会一个操作码接着一个操作码的分析代码(而不是根据控制流),所以在下面的情况下,汇编程序将对标签two的堆栈高度产生错误的判断:

    
    {
        let x := 8
        jump(two)
        one:
            // Here the stack height is 2 (because we pushed x and 7),
            // but the assembler thinks it is 1 because it reads
            // from top to bottom.
            // Accessing the stack variable x here will lead to errors.
            x := 9
            jump(three)
        two:
            7 // push something onto the stack
            jump(one)
        three:
    }
    

    这个问题可以通过手动调整栈高来解决。你可以在标签前添加栈高需要的增量。需要注意的是,你没有必要关心这此,如果你只是使用循环或汇编级的函数。

    下面的例子展示了,在极端的情况下,你可以通过上面说的解决这个问题:

    {
        let x := 8
        jump(two)
        0 // This code is unreachable but will adjust the stack height correctly
        one:
            x := 9 // Now x can be accessed properly.
            jump(three)
            pop // Similar negative correction.
        two:
            7 // push something onto the stack
            jump(one)
        three:
        pop // We have to pop the manually pushed value here again.
    }
    

    定义汇编-局部变量

    你可以通过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 is "deallocated" here
                b := add(b, v)
            } // v is "deallocated" here
        }
    }
    

    赋值

    你可以向内联局部变量赋值,或者函数局部变量。需要注意的是当你向一个指向memorystorage赋值时,你只是修改了对应指针而不是对应的数据。

    有两种方式的赋值方式:函数风格和指令风格。函数风格,比如variable := value,你必须在函数风格的表达式中提供一个变量,最终将得到一个栈变量。指令风格=: variable,值则直接从栈底取。以于两种方式冒号指向的都是变量名称(译者注:注意语法中冒号的位置)。赋值的效果是将栈上的变量值替换为新值。

    assembly {
        let v := 0 // functional-style assignment as part of variable declaration
        let g := add(v, 2)
        sload(10)
        =: v // instruction style assignment, puts the result of sload(10) into v
    }
    

    Switch

    你可以使用switch语句来作为一个基础版本的if/else语句。它需要取一个值,用它来与多个常量进行对比。每个分支对应的是对应切尔西到的常量。与某些语言容易出错的行为相反,控制流不会自动从一个判断情景到下一个场景(译者注:默认是break的)。最后有个叫default的兜底。

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

    可以有的case不需要包裹到大括号中,但每个case需要用大括号的包裹。

    循环

    内联编译支持一个简单的for风格的循环。for风格的循环的头部有三个部分,一个是初始部分,一个条件和一个后叠加部分。条件必须是一个函数风格的表达式,而其它两个部分用大括号包裹。如果在初始化的块中定义了任何变量,这些变量的作用域会被默认扩展到循环体内(条件,与后面的叠加部分定义的变量也类似。译者注:因为默认是块作用域,所以这里是一种特殊情况)。

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

    函数

    汇编语言允许定义底层的函数。这些需要在栈上取参数(以及一个返回的代码行),也会将结果存到栈上。调用一个函数与执行一个函数风格的操作码看起来是一样的。

    函数可以在任何地方定义,可以在定义的块中可见。在函数内,你不能访问一个在函数外定义的一个局部变量。同时也没有明确的return语句。

    如果你调用一个函数,并返回了多个值,你可以将他们赋值给一个元组,使用a, b := f(x)let a, b := f(x)

    下面的例子中通过平方乘来实现一个指数函数。

    assembly {
        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) }
            }
        }
    }
    

    内联汇编中要注意的事

    内联汇编语言使用中需要一个比较高的视野,但它又是非常底层的语法。函数调用,循环,switch被转换为简单的重写规则,另外一个语言提供的是重安排函数风格的操作码,管理了jump标签,计算了栈高以方便变量的访问,同时在块结束时,移除块内定义的块内的局部变量。特别需要注意的是最后两个情况。你必须清醒的知道,汇编语言只提供了从开始到结束的栈高计算,它没有根据你的逻辑去计算栈高(译者注:这常常导致错误)。此外,像交换这样的操作,仅仅交换栈里的内容,并不是变量的位置。

    Solidity中的惯例

    与EVM汇编不同,Solidity知道类型少于256字节,如,uint24。为了让他们更高效,大多数的数学操作仅仅是把也们当成是一个256字节的数字进行计算,高位的字节只在需要的时候才会清理,比如在写入内存前,或者在需要比较时。这意味着如果你在内联汇编中访问这样的变量,你必须要手动清除高位的无效字节。

    Solidity以非常简单的方式来管理内存:内部存在一个空间内存的指针在内存位置0x40。如果你想分配内存,可以直接使用从那个位置的内存,并相应的更新指针。

    Solidity中的内存数组元素,总是占用多个32字节的内存(也就是说byte[]也是这样,但是bytesstring不是这样)。多维的memory的数组是指向memory的数组。一个动态数组的长度存储在数据的第一个槽位,紧接着就是数组的元素。

    固定长度的memory数组没有一个长度字段,但它们将很快增加这个字段,以让定长与变长数组间有更好的转换能力,所以请不要依赖于这点。


    库(Libraries)

    库与合约类似,但它的目的是在一个指定的地址,且仅部署一次,然后通过EVM的特性DELEGATECALL(Homestead之前是用CALLCODE)来复用代码。这意味着库函数调用时,它的代码是在调用合约的上下文中执行。使用this将会指向到调用合约,而且可以访问调用合约的存储(storage)。因为一个合约是一个独立的代码块,它仅可以访问调用合约明确提供的状态变量(state variables),否则除此之外,没有任何方法去知道这些状态变量。

    使用库合约的合约,可以将库合约视为隐式的父合约(base contracts),当然它们不会显式的出现在继承关系中。但调用库函数的方式非常类似,如库L有函数f(),使用L.f()即可访问。此外,internal的库函数对所有合约可见,如果把库想像成一个父合约就能说得通了。当然调用内部函数使用的是internal的调用惯例,这意味着所有internal类型可以传进去,memory类型则通过引用传递,而不是拷贝的方式。为了在EVM中实现这一点,internal的库函数的代码和从其中调用的所有函数将被拉取(pull into)到调用合约中,然后执行一个普通的JUMP来代替DELEGATECALL

    下面的例子展示了如何使用库(后续在using for章节有一个更适合的实现Set的例子)。

    pragma solidity ^0.4.0;
    
    library Set {
      // We define a new struct datatype that will be used to
      // hold its data in the calling contract.
      struct Data { mapping(uint => bool) flags; }
    
      // Note that the first parameter is of type "storage
      // reference" and thus only its storage address and not
      // its contents is passed as part of the call.  This is a
      // special feature of library functions.  It is idiomatic
      // to call the first parameter 'self', if the function can
      // be seen as a method of that object.
      function insert(Data storage self, uint value)
          returns (bool)
      {
          if (self.flags[value])
              return false; // already there
          self.flags[value] = true;
          return true;
      }
    
      function remove(Data storage self, uint value)
          returns (bool)
      {
          if (!self.flags[value])
              return false; // not there
          self.flags[value] = false;
          return true;
      }
    
      function contains(Data storage self, uint value)
          returns (bool)
      {
          return self.flags[value];
      }
    }
    
    
    contract C {
        Set.Data knownValues;
    
        function register(uint value) {
            // The library functions can be called without a
            // specific instance of the library, since the
            // "instance" will be the current contract.
            if (!Set.insert(knownValues, value))
                throw;
        }
        // In this contract, we can also directly access knownValues.flags, if we want.
    }
    

    上面的例子中:

    • Library定义了一个数据结构体,用来在调用的合约中使用(库本身并未实际存储的数据)。如果函数需要操作数据,这个数据一般是通过库函数的第一个参数传入,按惯例会把参数名定为self
    • 另外一个需要留意的是上例中self的类型是storage,那么意味着传入的会是一个引用,而不是拷贝的值,那么修改它的值,会同步影响到其它地方,俗称引用传递,非值传递。
    • 库函数的使用不需要实例化,c.register中可以看出是直接使用Set.insert。但实际上当前的这个合约本身就是它的一个实例。
    • 这个例子中,c可以直接访问,knownValues。虽然这个值主要是被库函数使用的。

    当然,你完全可以不按上面的方式来使用库函数,可以不需要定义结构体,不需要使用storage类型的参数,还可以在任何位置有多个storage的引用类型的参数。

    调用Set.containsSet.removeSet.insert都会编译为以DELEGATECALL的方式调用external的合约和库。如果使用库,需要注意的是一个实实在在的外部函数调用发生了。尽管msg.sendermsg.valuethis还会保持它们在此调用中的值(在Homestead之前,由于实际使用的是CALLCODEmsg.sendermsg.value会变化)。

    下面的例子演示了如何使用memory类型和内部函数(inernal function),来实现一个自定义类型,但不会用到外部函数调用(external function)

    pragma solidity ^0.4.0;
    
    library BigInt {
        struct bigint {
            uint[] limbs;
        }
    
        function fromUint(uint x) internal returns (bigint r) {
            r.limbs = new uint[](1);
            r.limbs[0] = x;
        }
    
        function add(bigint _a, bigint _b) internal returns (bigint r) {
            r.limbs = new uint[](max(_a.limbs.length, _b.limbs.length));
            uint carry = 0;
            for (uint i = 0; i < r.limbs.length; ++i) {
                uint a = limb(_a, i);
                uint b = limb(_b, i);
                r.limbs[i] = a + b + carry;
                if (a + b < a || (a + b == uint(-1) && carry > 0))
                    carry = 1;
                else
                    carry = 0;
            }
            if (carry > 0) {
                // too bad, we have to add a limb
                uint[] memory newLimbs = new uint[](r.limbs.length + 1);
                for (i = 0; i < r.limbs.length; ++i)
                    newLimbs[i] = r.limbs[i];
                newLimbs[i] = carry;
                r.limbs = newLimbs;
            }
        }
    
        function limb(bigint _a, uint _limb) internal returns (uint) {
            return _limb < _a.limbs.length ? _a.limbs[_limb] : 0;
        }
    
        function max(uint a, uint b) private returns (uint) {
            return a > b ? a : b;
        }
    }
    
    
    contract C {
        using BigInt for BigInt.bigint;
    
        function f() {
            var x = BigInt.fromUint(7);
            var y = BigInt.fromUint(uint(-1));
            var z = x.add(y);
        }
    }
    

    因为编译器并不知道库最终部署的地址。这些地址须由linker填进最终的字节码中(使用命令行编译器来进行联接)。如果地址没有以参数的方式正确给到编译器,编译后的字节码将会仍包含一个这样格式的占们符_Set___(其中Set是库的名称)。可以通过手动将所有的40个符号替换为库的十六进制地址。

    对比普通合约来说,库的限制:

    • 状态变量(state variables)
    • 不能继承或被继承
    • 不能接收ether

    这些限制将来也可能被解除!

    附着库(Using for)

    指令using A for B;用来附着库里定义的函数(从库A)到任意类型B。这些函数将会默认接收调用函数对象的实例作为第一个参数。语法类似,python中的self变量一样。

    using A for *的效果是,库A中的函数被附着在做任意的类型上。

    在这两种情形中,所有函数,即使那些第一个参数的类型与调用函数的对象类型不匹配的,也被附着上了。类型检查是在函数被真正调用时,函数重载检查也会执行。

    using A for B;指令仅在当前的作用域有效,且暂时仅仅支持当前的合约这个作用域,后续也非常有可能解除这个限制,允许作用到全局范围。如果能作用到全局范围,通过引入一些模块(module),数据类型将能通过库函数扩展功能,而不需要每个地方都得写一遍类似的代码了。

    下面我们来换个方式重写set的例子。

    pragma solidity ^0.4.0;
    
    // This is the same code as before, just without comments
    library Set {
      struct Data { mapping(uint => bool) flags; }
    
      function insert(Data storage self, uint value)
          returns (bool)
      {
          if (self.flags[value])
            return false; // already there
          self.flags[value] = true;
          return true;
      }
    
      function remove(Data storage self, uint value)
          returns (bool)
      {
          if (!self.flags[value])
              return false; // not there
          self.flags[value] = false;
          return true;
      }
    
      function contains(Data storage self, uint value)
          returns (bool)
      {
          return self.flags[value];
      }
    }
    
    
    contract C {
        using Set for Set.Data; // this is the crucial change
        Set.Data knownValues;
    
        function register(uint value) {
            // Here, all variables of type Set.Data have
            // corresponding member functions.
            // The following function call is identical to
            // Set.insert(knownValues, value)
            if (!knownValues.insert(value))
                throw;
        }
    }
    

    我们也可以通过这种方式来扩展基本类型(elementary types)

    pragma solidity ^0.4.0;
    
    library Search {
        function indexOf(uint[] storage self, uint value) returns (uint) {
            for (uint i = 0; i < self.length; i++)
                if (self[i] == value) return i;
            return uint(-1);
        }
    }
    
    
    contract C {
        using Search for uint[];
        uint[] data;
    
        function append(uint value) {
            data.push(value);
        }
    
        function replace(uint _old, uint _new) {
            // This performs the library function call
            uint index = data.indexOf(_old);
            if (index == uint(-1))
                data.push(_new);
            else
                data[index] = _new;
        }
    }
    

    需要注意的是所有库调用都实际上是EVM函数调用。这意味着,如果你传的是memory类型的,或者是值类型(vaue types),那么仅会传一份拷贝,即使是self变量。变通之法就是使用存储(storage)类型的变量,这样就不会拷贝内容。


    接口

    接口与抽象合约类似,与之不同的是,接口内没有任何函数是已实现的,同时还有如下限制:

    1. 不能继承其它合约,或接口。
    2. 不能定义构造器
    3. 不能定义变量
    4. 不能定义结构体
    5. 不能定义枚举类

    其中的一些限制可能在未来放开。

    接口基本上限制为合约ABI定义可以表示的内容,ABI和接口定义之间的转换应该是可能的,不会有任何信息丢失。

    接口用自己的关键词表示:

    interface Token {
        function transfer(address recipient, uint amount);
    }
    

    合约可以继承于接口,因为他们可以继承于其它的合约。


    抽象(Abstract Contracts)

    抽象函数是没有函数体的的函数。如下:

    pragma solidity ^0.4.0;
    
    contract Feline {
        function utterance() returns (bytes32);
    }
    

    这样的合约不能通过编译,即使合约内也包含一些正常的函数。但它们可以做为基合约被继承。

    pragma solidity ^0.4.0;
    
    contract Feline {
        function utterance() returns (bytes32);
        
        function getContractName() returns (string){
            return "Feline";
        }
    }
    
    
    contract Cat is Feline {
        function utterance() returns (bytes32) { return "miaow"; }
    }
    

    如果一个合约从一个抽象合约里继承,但却没实现所有函数,那么它也是一个抽象合约


    继承(Inheritance)

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

    所有函数调用是虚拟(virtual)的,这意味着最远的派生方式会被调用,除非明确指定了合约。

    当一个合约从多个其它合约那里继承,在区块链上仅会创建一个合约,在父合约里的代码会复制来形成继承合约。

    基本的继承体系与python有些类似,特别是在处理多继承上面。

    下面用一个例子来详细说明:

    pragma solidity ^0.4.0;
    
    contract owned {
        function owned() { owner = msg.sender; }
        address owner;
    }
    
    
    // Use "is" to derive from another contract. Derived
    // contracts can access all non-private members including
    // internal functions and state variables. These cannot be
    // accessed externally via `this`, though.
    contract mortal is owned {
        function kill() {
            if (msg.sender == owner) selfdestruct(owner);
        }
    }
    
    
    // These abstract contracts are only provided to make the
    // interface known to the compiler. Note the function
    // without body. If a contract does not implement all
    // functions it can only be used as an interface.
    contract Config {
        function lookup(uint id) returns (address adr);
    }
    
    
    contract NameReg {
        function register(bytes32 name);
        function unregister();
     }
    
    
    // Multiple inheritance is possible. Note that "owned" is
    // also a base class of "mortal", yet there is only a single
    // instance of "owned" (as for virtual inheritance in C++).
    contract named is owned, mortal {
        function named(bytes32 name) {
            Config config = Config(0xd5f9d8d94886e70b06e474c3fb14fd43e2f23970);
            NameReg(config.lookup(1)).register(name);
        }
    
        // Functions can be overridden by another function with the same name and
        // the same number/types of inputs.  If the overriding function has different
        // types of output parameters, that causes an error.
        // Both local and message-based function calls take these overrides
        // into account.
        function kill() {
            if (msg.sender == owner) {
                Config config = Config(0xd5f9d8d94886e70b06e474c3fb14fd43e2f23970);
                NameReg(config.lookup(1)).unregister();
                // It is still possible to call a specific
                // overridden function.
                mortal.kill();
            }
        }
    }
    
    
    // If a constructor takes an argument, it needs to be
    // provided in the header (or modifier-invocation-style at
    // the constructor of the derived contract (see below)).
    contract PriceFeed is owned, mortal, named("GoldFeed") {
       function updateInfo(uint newInfo) {
          if (msg.sender == owner) info = newInfo;
       }
    
       function get() constant returns(uint r) { return info; }
    
       uint info;
    }
    

    上面的例子的named合约的kill()方法中,我们调用了motal.kill()调用父合约的销毁函数(destruction)。但这样可能什么引发一些小问题。

    pragma solidity ^0.4.0;
    
    contract mortal is owned {
        function kill() {
            if (msg.sender == owner) selfdestruct(owner);
        }
    }
    
    
    contract Base1 is mortal {
        function kill() { /* do cleanup 1 */ mortal.kill(); }
    }
    
    
    contract Base2 is mortal {
        function kill() { /* do cleanup 2 */ mortal.kill(); }
    }
    
    
    contract Final is Base1, Base2 {
    }
    

    Final.kill()的调用只会调用Base2.kill(),因为派生重写,会跳过Base1.kill,因为它根本就不知道有Base1。一个变通方法是使用super

    pragma solidity ^0.4.0;
    
    contract mortal is owned {
        function kill() {
            if (msg.sender == owner) selfdestruct(owner);
        }
    }
    
    
    contract Base1 is mortal {
        function kill() { /* do cleanup 1 */ super.kill(); }
    }
    
    
    contract Base2 is mortal {
        function kill() { /* do cleanup 2 */ super.kill(); }
    }
    
    
    contract Final is Base2, Base1 {
    }
    

    如果Base1调用了函数super,它不会简单的调用基类的合约函数,它还会调用继承关系图谱上的下一个基类合约,所以会调用Base2.kill()。需要注意的最终的继承图谱将会是:Final,Base1,Base2,mortal,owned。使用super时会调用的实际函数在使用它的类的上下文中是未知的,尽管它的类型是已知的。这类似于普通虚函数查找(ordinary virtual method lookup)

    基类构造器的方法(Arguments for Base Constructors)

    派生的合约需要提供所有父合约需要的所有参数,所以用两种方式来做,见下面的例子:

    pragma solidity ^0.4.0;
    
    contract Base {
        uint x;
        function Base(uint _x) { x = _x; }
    }
    
    
    contract Derived is Base(7) {
        function Derived(uint _y) Base(_y * _y) {
        }
    }
    

    或者直接在继承列表中使用is Base(7),或像修改器(modifier)使用方式一样,做为派生构造器定义头的一部分Base(_y * _y)。第一种方式对于构造器是常量的情况比较方便,可以大概说明合约的行为。第二种方式适用于构造的参数值由派生合约的指定的情况。在上述两种都用的情况下,第二种方式优先(一般情况只用其中一种方式就好了)。

    多继承与线性化(Multiple Inheritance and Linearization)

    实现多继承的编程语言需要解决几个问题,其中之一是菱形继承问题又称钻石问题,如下图。

    Solidity的解决方案参考Python,使用C3_linearization来强制将基类合约转换一个有向无环图(DAG)的特定顺序。结果是我们希望的单调性,但却禁止了某些继承行为。特别是基类合约在is后的顺序非常重要。下面的代码,Solidity会报错Linearization of inheritance graph impossible

    pragma solidity ^0.4.0;
    
    contract X {}
    contract A is X {}
    contract C is A, X {}
    

    原因是C会请求X来重写A(因为继承定义的顺序是A,X),但A自身又是重写X的,所以这是一个不可解决的矛盾。

    一个简单的指定基类合约的继承顺序原则是从most base-likemost derived

    继承有相同名字的不同类型成员

    当继承最终导致一个合约同时存在多个相同名字的修改器或函数,它将被视为一个错误。同新的如果事件与修改器重名,或者函数与事件重名都将产生错误。作为一个例外,状态变量的getter可以覆盖一个public的函数。


    事件(Events)

    事件是使用EVM日志内置功能的方便工具,在DAPP的接口中,它可以反过来调用Javascript的监听事件的回调。

    事件在合约中可被继承。当被调用时,会触发参数存储到交易的日志中(一种区块链上的特殊数据结构)。这些日志与合约的地址关联,并合并到区块链中,只要区块可以访问就一直存在(至少Frontier,Homestead是这样,但Serenity也许也是这样)。日志和事件在合约内不可直接被访问,即使是创建日志的合约。

    日志的SPV(简单支付验证)是可能的,如果一个外部的实体提供了一个这样证明的合约,它可以证明日志在区块链是否存在。但需要留意的是,由于合约中仅能访问最近的256个区块哈希,所以还需要提供区块头信息。

    可以最多有三个参数被设置为indexed,来设置是否被索引。设置为索引后,可以允许通过这个参数来查找日志,甚至可以按特定的值过滤。

    如果数组(包括stringbytes)类型被标记为索引项,会用它对应的Keccak-256哈希值做为topic

    除非是匿名事件,否则事件签名(比如:Deposit(address,hash256,uint256))是其中一个topic,同时也意味着对于匿名事件无法通过名字来过滤。

    所有未被索引的参数将被做为日志的一部分被保存起来。

    被索引的参数将不会保存它们自己,你可以搜索他们的值,但不能检索值本身。

    下面是一个简单的例子:

    pragma solidity ^0.4.0;
    
    contract ClientReceipt {
        event Deposit(
            address indexed _from,
            bytes32 indexed _id,
            uint _value
        );
    
        function deposit(bytes32 _id) {
            // Any call to this function (even deeply nested) can
            // be detected from the JavaScript API by filtering
            // for `Deposit` to be called.
            Deposit(msg.sender, _id, msg.value);
        }
    }
    

    下述是使用javascript来获取日志的例子。

    var abi = /* abi as generated by the compiler */;
    var ClientReceipt = web3.eth.contract(abi);
    var clientReceipt = ClientReceipt.at(0x123 /* address */);
    
    var event = clientReceipt.Deposit();
    
    // watch for changes
    event.watch(function(error, result){
        // result will contain various information
        // including the argumets given to the Deposit
        // call.
        if (!error)
            console.log(result);
    });
    
    // Or pass a callback to start watching immediately
    var event = clientReceipt.Deposit(function(error, result) {
        if (!error)
            console.log(result);
    });
    

    底层的日志接口(Low-level Interface to Logs)

    通过函数log0log1log2log3log4,可以直接访问底层的日志组件。logi表示总共有带i + 1个参数(i表示的就是可带参数的数目,只是是从0开始计数的)。其中第一个参数会被用来做为日志的数据部分,其它的会做为主题(topics)。前面例子中的事件可改为如下:

    log3(
        msg.value,
        0x50cb9fe53daa9737b786ab3646f04d0150dc50ef4e75f59509d83667ad5adb20,
        msg.sender,
        _id
    );
    

    其中的长16进制串是事件的签名,计算方式是keccak256("Deposit(address,hash256,uint256)")

    更多的理解事件的资源


    回退函数(fallback function)

    每一个合约有且仅有一个没有名字的函数。这个函数无参数,也无返回值。如果调用合约时,没有匹配上任何一个函数(或者没有传哪怕一点数据),就会调用默认的回退函数。

    此外,当合约收到ether时(没有任何其它数据),这个函数也会被执行。在此时,一般仅有少量的gas剩余,用于执行这个函数(准确的说,还剩2300gas)。所以应该尽量保证回退函数使用少的gas。

    下述提供给回退函数可执行的操作会比常规的花费得多一点。

    • 写入到存储(storage)
    • 创建一个合约
    • 执行一个外部(external)函数调用,会花费非常多的gas
    • 发送ether

    请在部署合约到网络前,保证透彻的测试你的回退函数,来保证函数执行的花费控制在2300gas以内。

    一个没有定义一个回退函数的合约。如果接收ether,会触发异常,并返还ether(solidity v0.4.0开始)。所以合约要接收ether,必须实现回退函数。下面来看个例子:

    pragma solidity ^0.4.0;
    
    contract Test {
        // This function is called for all messages sent to
        // this contract (there is no other function).
        // Sending Ether to this contract will cause an exception,
        // because the fallback function does not have the "payable"
        // modifier.
        function() { x = 1; }
        uint x;
    }
    
    
    // This contract keeps all Ether sent to it with no way
    // to get it back.
    contract Sink {
        function() payable { }
    }
    
    
    contract Caller {
        function callTest(Test test) {
            test.call(0xabcdef01); // hash does not exist
            // results in test.x becoming == 1.
    
            // The following call will fail, reject the
            // Ether and return false:
            test.send(2 ether);
        }
    }
    

    在浏览器中跑的话,记得要先存ether。


    常量(constant state variables)

    状态变量可以被定义为constant,常量。这样的话,它必须在编译期间通过一个表达式赋值。赋值的表达式不允许:1)访问storage;2)区块链数据,如nowthis.balanceblock.number;3)合约执行的中间数据,如msg.gas;4)向外部合约发起调用。也许会造成内存分配副作用表达式是允许的,但不允许产生其它内存对象的副作用的表达式。内置的函数keccak256keccak256ripemd160ecrecoveraddmodmulmod可以允许调用,即使它们是调用的外部合约。

    允许内存分配,从而带来可能的副作用的原因是因为这将允许构建复杂的对象,比如,查找表。虽然当前的特性尚未完整支持。

    编译器并不会为常量在storage上预留空间,每个使用的常量都会被对应的常量表达式所替换(也许优化器会直接替换为常量表达式的结果值)。

    不是所有的类型都支持常量,当前支持的仅有值类型和字符串。

    pragma solidity ^0.4.0;
    
    contract C {
        uint constant x = 32**22 + 8;
        string constant text = "abc";
        bytes32 constant myHash = keccak256("abc");
    }
    

    常函数(Constant Functions)

    函数也可被声明为常量,这类函数将承诺自己不修改区块链上任何状态。

    pragma solidity ^0.4.0;
    
    contract C {
        function f(uint a, uint b) constant returns (uint) {
            return a * (b + 42);
        }
    }
    

    访问器(Accessor)方法默认被标记为constant。当前编译器并未强制一个constant的方法不能修改状态。但建议大家对于不会修改数据的标记为constant


    函数修改器(Function Modifiers)

    修改器(Modifiers)可以用来轻易的改变一个函数的行为。比如用于在函数执行前检查某种前置条件。修改器是一种合约属性,可被继承,同时还可被派生的合约重写(override)。下面我们来看一段示例代码:

    pragma solidity ^0.4.0;
    
    contract owned {
        function owned() { owner = msg.sender; }
        address owner;
    
        // This contract only defines a modifier but does not use
        // it - it will be used in derived contracts.
        // The function body is inserted where the special symbol
        // "_;" in the definition of a modifier appears.
        // This means that if the owner calls this function, the
        // function is executed and otherwise, an exception is
        // thrown.
        modifier onlyOwner {
            if (msg.sender != owner)
                throw;
            _;
        }
    }
    
    
    contract mortal is owned {
        // This contract inherits the "onlyOwner"-modifier from
        // "owned" and applies it to the "close"-function, which
        // causes that calls to "close" only have an effect if
        // they are made by the stored owner.
        function close() onlyOwner {
            selfdestruct(owner);
        }
    }
    
    
    contract priced {
        // Modifiers can receive arguments:
        modifier costs(uint price) {
            if (msg.value >= price) {
                _;
            }
        }
    }
    
    
    contract Register is priced, owned {
        mapping (address => bool) registeredAddresses;
        uint price;
    
        function Register(uint initialPrice) { price = initialPrice; }
    
        // It is important to also provide the
        // "payable" keyword here, otherwise the function will
        // automatically reject all Ether sent to it.
        function register() payable costs(price) {
            registeredAddresses[msg.sender] = true;
        }
    
        function changePrice(uint _price) onlyOwner {
            price = _price;
        }
    }
    
    

    修改器可以被继承,使用将modifier置于参数后,返回值前即可。

    特殊_表示使用修改符的函数体的替换位置。

    从合约Register可以看出全约可以多继承,通过,号分隔两个被继承的对象。

    修改器也是可以接收参数的,如pricedcosts

    使用修改器实现的一个防重复进入的例子。

    pragma solidity ^0.4.0;
    contract Mutex {
        bool locked;
        modifier noReentrancy() {
            if (locked) throw;
            locked = true;
            _;
            locked = false;
        }
    
        /// This function is protected by a mutex, which means that
        /// reentrant calls from within msg.sender.call cannot call f again.
        /// The `return 7` statement assigns 7 to the return value but still
        /// executes the statement `locked = false` in the modifier.
        function f() noReentrancy returns (uint) {
            if (!msg.sender.call()) throw;
            return 7;
        }
    }
    

    例子中,由于call()方法有可能会调回当前方法,修改器实现了防重入的检查。

    如果同一个函数有多个修改器,他们之间以空格隔开,修饰器会依次检查执行。

    需要注意的是,在Solidity的早期版本中,有修改器的函数,它的return语句的行为有些不同。

    在修改器中和函数体内的显式的return语句,仅仅跳出当前的修改器和函数体。返回的变量会被赋值,但整个执行逻辑会在前一个修改器后面定义的"_"后继续执行。

    修改器的参数可以是任意表达式。在对应的上下文中,所有的函数中引入的符号,在修改器中均可见。但修改器中引入的符号在函数中不可见,因为它们有可能被重写。


    访问函数(Getter Functions)

    编译器为自动为所有的public的状态变量创建访问函数。下面的合约例子中,编译器会生成一个名叫data的无参,返回值是uint的类型的值data。状态变量的初始化可以在定义时完成。

    pragma solidity ^0.4.0;
    
    
    contract C{
        uint public c = 10;
    }
    
    contract D{
        C c = new C();
        
        function getDataUsingAccessor() returns (uint){
            return c.c();
        }
    }
    

    访问函数有外部(external)可见性。如果通过内部(internal)的方式访问,比如直接访问,你可以直接把它当一个变量进行使用,但如果使用外部(external)的方式来访问,如通过this.,那么它必须通过函数的方式来调用。

    pragma solidity ^0.4.0;
    
    
    contract C{
        uint public c = 10;
        
        function accessInternal() returns (uint){
            return c;
        }
        
        function accessExternal() returns (uint){
            return this.c();
        }
    }
    

    acessExternal函数中,如果直接返回return this.c;,会出现报错Return argument type function () constant external returns (uint256) is not implicitly convertible to expected type (type of first return variable) uint256.。原因应该是通过外部(external)的方式只能访问到this.c作为函数的对象,所以它认为你是想把一个函数转为uint故而报错。

    下面是一个更加复杂的例子:

    pragma solidity ^0.4.0;
    
    contract ComplexSimple{
        struct Cat{
            uint a;
            bytes3 b;
            mapping(uint => uint) map;
        }
        
        //
        mapping(uint => mapping(bool => Cat)) public content;
        
        function initial(){
            content[0][true] = Cat(1, 1);
            content[0][true].map[0] = 10;
        }
        
        function get() returns (uint, bytes3, uint){
            return (content[0][true].a, content[0][true].b, content[0][true].map[0]);
        }
    }
    
    contract Complex {
        struct Data {
            uint a;
            bytes3 b;
            mapping (uint => uint) map;
        }
        mapping (uint => mapping(bool => Data[])) public data;
        
        
    }
    

    文档中自带的的这个Demo始终跑不通,数组类型这里不知为何会抛invalid jump。把这块简化了写了一个ComplextSimple供参考。

    需要注意的是publicmapping默认访问参数是需要参数的,并不是之前说的访问函数都是无参的。

    mapping类型的数据访问方式变为了data[arg1][arg2][arg3].a

    结构体(struct)里的mapping初始化被省略了,因为并没有一个很好的方式来对键赋值。


    可见性或权限控制(Visibility And Accessors)

    Solidity有两种函数调用方式,一种是内部调用,不会创建一个EVM调用(也叫做消息调用),另一种则是外部调用,会创建EVM调用(会发起消息调用)。Solidity对函数和状态变量提供了四种可见性。分别是external,public,internal,private。其中函数默认是public。状态变量默认的可见性是internal

    可见性

    external:

    外部函数是合约接口的一部分,所以我们可以从其它合约或通过交易来发起调用。一个外部函数f,不能通过内部的方式来发起调用,(如f()不可以,但可以通过this.f())。外部函数在接收大的数组数据时更加有效。

    public:

    公开函数是合约接口的一部分,可以通过内部,或者消息来进行调用。对于public类型的状态变量,会自动创建一个访问器(详见下文)。

    internal

    这样声明的函数和状态变量只能通过内部访问。如在当前合约中调用,或继承的合约里调用。需要注意的是不能加前缀this,前缀this是表示通过外部方式访问。

    private

    私有函数和状态变量仅在当前合约中可以访问,在继承的合约内,不可访问。

    备注

    所有在合约内的东西对外部的观察者来说都是可见,将某些东西标记为private仅仅阻止了其它合约来进行访问和修改,但并不能阻止其它人看到相关的信息。

    可见性的标识符的定义位置,对于state variable是在类型后面,函数是在参数列表和返回关键字中间。来看一个定义的例子:

    pragma solidity ^0.4.0;
    
    contract C {
        function f(uint a) private returns (uint b) { return a + 1; }
        function setData(uint a) internal { data = a; }
        uint public data;
    }
    

    在下面的例子中,D可以调用c.getData()来访问data的值,但不能调用f。合约E继承自C,所以它可以访问compute函数。

    pragma solidity ^0.4.0;
    
    contract C {
        uint private data;
    
        function f(uint a) private returns(uint b) { return a + 1; }
        function setData(uint a) { data = a; }
        function getData() public returns(uint) { return data; }
        function compute(uint a, uint b) internal returns (uint) { return a+b; }
    }
    
    
    contract D {
        function readData() {
            C c = new C();
            uint local = c.f(7); // error: member "f" is not visible
            c.setData(3);
            local = c.getData();
            local = c.compute(3, 5); // error: member "compute" is not visible
        }
    }
    
    
    contract E is C {
        function g() {
            C c = new C();
            uint val = compute(3, 5);  // acces to internal member (from derivated to parent contract)
        }
    }
    


    合约

    Solidity中合约有点类似面向对象语言中的类。合约中有用于数据持久化的状态变量(state variables),和可以操作他们的函数。调用另一个合约实例的函数时,会执行一个EVM函数调用,这个操作会切换执行时的上下文,这样,前一个合约的状态变量(state variables)就不能访问了。

    创建合约

    合约可以通过Solidity,或不通过Solidity创建。当合约创建时,一个和合约同名的函数(构造器函数)会调用一次,用于初始化。

    构造器函数是可选的。仅能有一个构造器,所以不支持重载。

    如果不通过Solidity,我们可以通过web3.js,使用JavaScript的API来完成合约创建:

    // Need to specify some source including contract name for the data param below
    var source = "contract CONTRACT_NAME { function CONTRACT_NAME(unit a, uint b) {} }";
    
    // The json abi array generated by the compiler
    var abiArray = [
        {
            "inputs":[
                {"name":"x","type":"uint256"},
                {"name":"y","type":"uint256"}
            ],
            "type":"constructor"
        },
        {
            "constant":true,
            "inputs":[],
            "name":"x",
            "outputs":[{"name":"","type":"bytes32"}],
            "type":"function"
        }
    ];
    
    var MyContract_ = web3.eth.contract(source);
    MyContract = web3.eth.contract(MyContract_.CONTRACT_NAME.info.abiDefinition);
    // deploy new contract
    var contractInstance = MyContract.new(
        10,
        11,
        {from: myAccount, gas: 1000000}
    );
    

    具体内部实现里,构造器的参数是紧跟在合约代码的后面,但如果你使用web3.js,可以不用关心这样的细节。

    如果一个合约要创建另一个合约,它必须要知道源码。这意味着循环创建依赖是不可能的。

    pragma solidity ^0.4.0;
    
    contract OwnedToken {
        // TokenCreator is a contract type that is defined below.
        // It is fine to reference it as long as it is not used
        // to create a new contract.
        TokenCreator creator;
        address owner;
        bytes32 name;
    
        // This is the constructor which registers the
        // creator and the assigned name.
        function OwnedToken(bytes32 _name) {
            // State variables are accessed via their name
            // and not via e.g. this.owner. This also applies
            // to functions and especially in the constructors,
            // you can only call them like that ("internall"),
            // because the contract itself does not exist yet.
            owner = msg.sender;
            // We do an explicit type conversion from `address`
            // to `TokenCreator` and assume that the type of
            // the calling contract is TokenCreator, there is
            // no real way to check that.
            creator = TokenCreator(msg.sender);
            name = _name;
        }
    
        function changeName(bytes32 newName) {
            // Only the creator can alter the name --
            // the comparison is possible since contracts
            // are implicitly convertible to addresses.
            if (msg.sender == address(creator))
                name = newName;
        }
    
        function transfer(address newOwner) {
            // Only the current owner can transfer the token.
            if (msg.sender != owner) return;
            // We also want to ask the creator if the transfer
            // is fine. Note that this calls a function of the
            // contract defined below. If the call fails (e.g.
            // due to out-of-gas), the execution here stops
            // immediately.
            if (creator.isTokenTransferOK(owner, newOwner))
                owner = newOwner;
        }
    }
    
    contract TokenCreator {
        function createToken(bytes32 name)
           returns (OwnedToken tokenAddress)
        {
            // Create a new Token contract and return its address.
            // From the JavaScript side, the return type is simply
            // "address", as this is the closest type available in
            // the ABI.
            return new OwnedToken(name);
        }
    
        function changeName(OwnedToken tokenAddress, bytes32 name) {
            // Again, the external type of "tokenAddress" is
            // simply "address".
            tokenAddress.changeName(name);
        }
    
        function isTokenTransferOK(
            address currentOwner,
            address newOwner
        ) returns (bool ok) {
            // Check some arbitrary condition.
            address tokenAddress = msg.sender;
            return (keccak256(newOwner) & 0xff) == (bytes20(tokenAddress) & 0xff);
        }
    }

    内联汇编(Inline Assembly)

    为了增强对语言的细粒度的控制,特别是在写通用库时,可以在一个语言中交错使用Solidity的语句来接近其中一个虚拟机。但由于EVM是基于栈执行的,所以有时很难定位到正确的栈槽位,从而提供正确的的参数或操作码。Solidit的内联汇编尝试解决这个问题,但也引入了其它的问题,当你通过下述特性进行手动的汇编时:

    • 函数式的操作码: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 := sub(x, 1) jumpi(repeat, eq(x, 0))

    Solidity Assembly是对内联汇编的详细介绍。


    异常(Excepions)

    有一些情况下,异常是自动抛出来的(见下),你也可以使用throw来手动抛出一个异常。抛出异常的效果是当前的执行被终止且被撤销(值的改变和帐户余额的变化都会被回退)。异常还会通过Solidity的函数调用向上冒泡(bubbled up)传递。(send,和底层的函数调用call,delegatecallcallcode是一个例外,当发生异常时,这些函数返回false)。

    捕捉异常是不可能的(或许因为异常时,需要强制回退的机制)。

    在下面的例子中,我们将如展示如何使用throw来回退转帐,以及演示如何检查send的返回值。

    pragma solidity ^0.4.0;
    
    contract Sharer {
        function sendHalf(address addr) payable returns (uint balance) {
            if (!addr.send(msg.value / 2))
                throw; // also reverts the transfer to Sharer
            return this.balance;
        }
    }
    

    当前,Solidity在下述场景中自动产生运行时异常。

    1. 如果越界,或是负的序号值访问数组。
    2. 如果访问一个定长的bytesN,序号越界,或是负的序号值。
    3. 如果你通过消息调用一个函数,但在调用的过程中,并没有正确结束(gas不足,没有匹配到对应的函数,或他自己出现异常)。底层操作如call,send,delegatecallcallcode除外,它们不会抛出异常,但它们会通过返回false来表示失败。
    4. 如果在使用new创建一个新合约时,但合约的初化化由于类似3中的原因没有正常完成。
    5. 被除数为0。
    6. 对一个二进制移动一个负的值。
    7. 使用枚举时,将过大值,负值转为枚举类型。
    8. 使用外部函数调用时,被调用的对象并不包含代码。
    9. 如果你的public的函数在没有payable关键字时,却尝试在接收ether(包括构造函数,和回退函数)。
    10. 合约通过一个publicgetter函数(public getter funciton)接收ether
    11. 调用一个未初始化的内部函数。
    12. .transfer()执行失败
    13. assert返回false

    当一个用户通过下述方式触发一个异常:

    1. 调用throw
    2. 调用require,但参数值为false。

    当上述情况发生时,在Solidity会执行一个回退操作(指令0xfd)。与之相对的是,如果发生运行时异常,或assert失败时,将执行无效操作(指令0xfe)。在上述的情况下,由此促使EVM撤回所有的状态改变。这样做的原因是,没有办法继续安全执行了,因为想要发生的事件并未发生。因为我们想保持交易的原子性(一致性),所以撤销所有操作,让整个交易没有任何影响。

    通过assert判断内部条件是否达成,require验证输入的有效性。这样的分析工具,可以假设正确的输入,减少错误。这样无效的操作码将永远不会出现。


    作用范围和声明(Scoping And Decarations)

    一个变量在声明后都有初始值为字节表示的全0值。也就是所有类型的默认值是典型的零态(zero-state)。举例来说,默认的bool的值为false,uintint的默认值为0

    对从byte1byte32定长的字节数组,每个元素都被初始化为对应类型的初始值(一个字节的是一个字节长的全0值,多个字节长的是多个字节长的全零值)。对于变长的数组bytesstring,默认值则为空数组和空字符串。

    函数内定义的变量,在整个函数中均可用,无论它在哪里定义)。因为Solidity使用了javascript的变量作用范围的规则。与常规语言语法从定义处开始,到当前块结束为止不同。由此,下述代码编译时会抛出一个异常,Identifier already declared

    pragma solidity ^0.4.0;
    
    contract ScopingErrors {
        function scoping() {
            uint i = 0;
    
            while (i++ < 1) {
                uint same1 = 0;
            }
    
            while (i++ < 2) {
                uint same1 = 0;// Illegal, second declaration of same1
            }
        }
    
        function minimalScoping() {
            {
                uint same2 = 0;
            }
    
            {
                uint same2 = 0;// Illegal, second declaration of same2
            }
        }
    
        function forLoopScoping() {
            for (uint same3 = 0; same3 < 1; same3++) {
            }
    
            for (uint same3 = 0; same3 < 1; same3++) {// Illegal, second declaration of same3
            }
        }
        
        function crossFunction(){
           uint same1 = 0;//Illegal
        }
    
    }
    

    另外的,如果一个变量被声明了,它会在函数开始前被初始化为默认值。所以下述例子是合法的。

    pragma solidity ^0.4.0;
    
    contract C{
        function foo() returns (uint) {
        // baz is implicitly initialized as 0
        uint bar = 5;
        if (true) {
            bar += baz;
        } else {
            uint baz = 10;// never executes
        }
        return bar;// returns 5
    }
    }
    

    感谢您的支持


    展开全文
    xiatiancc 2017-12-27 11:34:14
  • 1.32MB eric0593 2018-06-05 09:52:01
  • 8.52MB weixin_43405220 2020-12-17 16:03:50
  • HiBlock 2018-08-10 21:07:17
  • HiBlock 2018-08-09 23:34:52
  • HiBlock 2018-08-09 23:37:36
  • HiBlock 2018-08-08 00:16:00
  • HiBlock 2018-08-03 22:25:53
  • HiBlock 2018-04-25 22:42:54
  • jjavabboy 2018-04-20 09:26:32
  • HiBlock 2018-08-08 00:17:05

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 876
精华内容 350
关键字:

solidity中文文档