solidity基础(3)

  1. solidity的自定义结构体深入详解
    结构体,solidity中的自定义类型,我们可以使用关键字struct来进行自定义,结构体内可以包含字符串,整型等基本数据类型,以及数组,映射,结构体等复杂类型。数组,映射,结构体也支持自定义的结构体:

     pragma solidity ^0.4.18;
     contract SmpleStruct {
       struct student {
         string name;
         int num;
         //student a; error
         student[] students;
         mapping(string => string)index;
       }
    
       struct Class {
         string className;
         //学生列表
         student[] students;
         mapping(string => string)index;
       }
     }
    
    1. 结构体定义的限制
      不能在结构体中定义一个自己作为类型,这样限制原因是,自定义类型的大小不允许是无限的,但是我们仍然能在类型内用数组,映射来引用当前定义的类型

    2. 初始化
      在初始化时,需要忽略映射类型

      1. 直接初始化
        如果声明的结构体类型为A,可以使用A(变量1,变量2,...)的方式来完成初始化,用法和go语言中的struct类似
      2. 命名初始化
        还可以使用类似JavaScript的命名参数初始化额方式,通过传入参数名和对应值的对象,这样做的好处在于可以好处在于可以不按定义的顺序传入值
      3. 结构体中映射的初始化
        由于映射是一种特殊的数据结构
        参考资料
        只能在storage变量中使用,这里的storage为状态变量,如果尝试对memory的映射赋值,则会报错
        [图片上传失败...(image-7f3e2-1540205314127)]
    3. 结构体的可见性
      当前只支持internal的,后续不排除开发这个限制
      参考资料

      • 继承中使用
        结构体由于不对外可见的,所以只可以在当前合约或继承的子合约中使用,但包含定义结构体的函数均需声明为internal
    4. 跨合约的临时解决方案
      手动的将要返回的结构体拆解为基本类型进行返回

        pragma solidity ^0.4.18;
        contract TestA {
          struct A {string para1;int para2}
          function call(B b) internal {
            A memory a = A("test",1);
            b.g(a.para1,a.para2);
          }
        }
      
        contract TestB {
          function g(string s,int i) public {
            //要实现的内容
          }
        }
      


  1. solidity的映射类型深入详解
    映射是一种引用类型,存储键值对,提供根据键查找值,与其他语言的字典,map等类似,但也有非常大的不同,尤其它在区块链中独特的储存模型

    1. 只能是状态变量
      由于在映射中键的数量是任意的,导致映射的大小也是变长的。映射只能声明为storage的状态变量 ,或被赋值给一个storage的对象引用:

      pragma solidity ^0.4.18;
      contract Test {
        mapping(uint =>uint) stateVar;
        function fTest() public returns(uint) {
          //mapping(uint => uint) memory memVar;
          //TypeError:只能为数组或结构体类型指定内存
          //mapping(uint =>uint) b;
          //TypeError:未被初始化,映射只能分配给状态变量
          mapping(uint =>uint) b = stateVar;
          //可以被赋值为storage的引用
        }
      }
      
    2. 支持的类型
      映射类型的支持除映射,变长数组,合约,枚举,结构体以外的任意类型则允许任意类型,甚至是映射

    3. setter方法
      对于映射类型,也能标记为public,以让solidity自动生成访问器

      pragma solidity ^0.4.18;
      contract Test{
        mapping(uint => uint) public a;
        mapping(uint => mapping(uint => string)) b;
        //需要注意的是public不能不写,不然不会自动生成访问器
        function set() {
          a[1] = 1;
          b[1][1] = "aaa";
        }
      }
      
    4. 映射的储存模型
      由于状态变量是储存在区块链上的,所以储存空间需要预先分配,但映射的储存值是可以动态增改的,那么最终是如何支持的呢?
      关于状态的储存模型中提到,实际储存时是以哈希键值对的方式。其中哈希是由键值和映射的储存槽位序号拼接后计算的哈希值(映射只占一个槽位序号),也就是说值是存到由keccak256(k . p)计算的哈希串里,这里的k表示的是映射要查找的键,p表示映射在整个合约中相对序号位置
      这里没有看明白,所以附上原文链接,如果想看的可以看一下原文链接

    5. 与其他语言映射的不同
      由于映射的储存模型决定了,映射实际不存在一个映射的键大小,没有一个键集合的概念,但是可以通过扩展默认映射来实现这样的功能
      官方提供的映射扩展示例

  2. solidity的delete深入详解
    solidity中有个特殊的操作符delete用于释放空间,因为区块链作为一个公用资源,为避免大家滥用,且鼓励主动对空间的回收,释放空间将会返还一些gas
    delete关键字的作用是对某个类型值a赋予初始值。比如如果删除整数delete a等同于a = 0

    1. 删除基本类型
      • 对于基本类型,如
        bool,uint,address,bytes,string,使用delete会设置成对应的初始值,bool类型是false,变长字节数组是0x0string则是空串
    • 删除枚举
      删除枚举类型时,会将其值重置为序号为0的值

        pragma solidity ^0.4.18;
        contract DeleteEnum {
          eum Lignt{red,green,yellow}
          Light light;
          function f() returns(Light) {
            light = Light.green;//此时light=1
            delete light;//此时light=0
            return light;
          }
        }
      
    • 删除函数
      函数不可以删除,会报错

    1. 删除复杂类型
      • 删除结构体
        删除一个结构体,会将其中的所有成员一一置为初值

      • 删除映射
        映射是一个特殊的存在,由于映射的键并不总是能有效遍历(数据结构没有提供接口,也并不总是需要关心所有键是什么),所存的键的数量往往是非常大的,所以我们并不能删除一个映射,但是我们可以指定键来删除映射中的某一项

          mapping(address => uint) map;
          map[msg.sender] = 100;
          delete map[msg.sender];
        
      • 删除结构体中的映射

        如果删除一个结构体时,其中含有映射类型,会跳过映射类型

    2. 删除数组
    • 删除整个数组
      对于定长数组,删除时,是将数组内所有元素置为初值,而对于变长数组时,则是将长度置为0
    • 删除数组的一个元素
      我们也可以删除数组的一个元素,这里需要注意的是,删除一个元素并不会改变数组长度,即和删除其他类型一样,都是赋类型的零值,并不会移动元素
    1. gas使用的考虑

      • gas相关
        上文中,我们了解到,删除时会忽略映射,以及数组的某个元素被删除后,并不会自动整理数组。这些看起来很不符合常理,其实是基于对gas限制的考虑。因为如果映射或数组非常大的情况下,删除或维护它们将变得非常消耗gas
        不过,清理空间,可以获得gas的返还,但无特别意义的数组的整理和删除,只会消耗更多gas,需要在业务实现上进行权衡

      • 清理的最佳实践
        由于本身并未提供对映射这样的大对象的清理,所有储存并遍历它们来清理,显得特别消耗gas,一种实践就是能复用就复用,一般不主动清理,下面是一个数组的插入实现,比如增加一个计数器,直接忽略已使用过的位置:

        uint numElements = 0;
        uint[] arrary;
        function insert(uint value) {
          if(numElements == arrary.length) {
            arrary.length += 1;
          }
          //需要注意的是,solidity中也有前自增和后自增的区别
          arrary[numElements++] = value;
        }
        
        function clear() {
          numElements = 0;
        }
        

      上面的例子中,我们在数组新增时,直接忽略已使用过的槽位。而在代码内,我们使用numElements来代替arrary.length,以获取当前数组所在的位置。
      如果这种大对象是在某个事件发生时,一次性使用,然后需要回收。一个更更有效的方式是,在发生某个事件时,创建一个新合约,在新合约完成逻辑,完成后,让合约suicide。清理合约占用空间返还的gas就退还给了调用者,来节省主动遍历消除的额外gas

    2. 删除的注意事项
      删除的本质是对一个变量置其类型的零值,所以我们删除storage的引用时会报错

      pragma solidity ^0.4.18;
      contract Test {
        struct S{}
        S s;
        function fTest() {
          S storage storageVar = s;
          //delete storageVar;
          //Error:一元运算符不能应用于删除储存指针
          delete s;
        }
      }
      

    删除storageVar会报错

    1. 参考资料
      delte语法介绍

  3. solidity中的基本类型转换

    1. 隐式转换
      如果一个运算符能支持不同类型,编译器会隐式的尝试将一个操作数的类型,转为另一个操作数的类型,赋值同理。
      一般来说,值类型间的互换只要不丢失信息,语义可通则可转换,如:

      pragma solidity ^0.4.18;
      contract Test {
        function conversion() public returns(uint16) {
          uint8 a = 1;
          //隐式转换
          uint16 b = a;
          return b;
        }
      }
      

    另外,无符号整数可以被转换为同样,或更大的字节类型,由于address是20字节大小,所以它与int160大小是一样的,所以也可以转换的

    1. 显示转换
      编译器不会将语法上不可转换的类型进行隐式转换,此时需要强转,如:int8 a =1;uint16 b =uint16(a);
    2. 类型推断:var
      如:var i = 0;,通过类型推断,i的实际类型为uint8,而且后续不会改变,即在第一次类型推断之后,该变量的类型就已经确定了
    3. 一些常见的转换方案
      • uint转为bytes
        将一个uint转换为bytes,可以使用assembly

        function toBytes(uint256 x) public returns(bytes b) {
          b = new bytes(32);
          assembly{mstore(add(b,32),x)}
        }
        
      我试了一下,貌似只能转换为32位的,可能不对,望见谅
      • string转为bytes
        string可以显示的转为bytes,但如果要转为bytes32,可能只能使用assembly,字符串转为bytes

        pragma solidity ^0.4.18;
        contract stringTobytes{
          function sTb1(string memory source) returns(bytes result) {
            return bytes(source);
          }
          //要记住,字符串是UTF8,所以在将它们转换为字节后,每个字节不一定是字符
          function sTb2(string memory source) returns(bytes32 result) {
            assembly{
              result := mload(add(source,32))
            }
          }
        }
        
  4. solidity变量的声明及作用域
    solidity中的作用域规则同于Javascript
    题外话【补遗】:
    值类型:储存值的变量;引用类型:储存地址的变量,通俗来说:“值类型是现金,引用类型是存折”。链接

    1. 声明及默认值
      变量声明后均有一个初值,是对应类型的“零态”,即对应的类型的字节表示的全0,使用中需要特别小心的是这与其他语言的默认值如nullundefined有所不同,因为有时0也是一种业务值

    2. 引用类型的初始化
      对于值类型,声明变量后,即赋值为默认值,可正常使用。而对于引用类型是否仍需同其它语言一样进行显式初始化,进行内存分配,才能进一步使用呢,我们来分别看一下。

      1. 动态数组
        对于数组,声明后,仍需分配内存后访问,下面的代码会报越界错误

         pragma solidity ^0.4.18;
         contract inital{
             function f() returns(byte,uint8){
             bytes memory bs;
             uint[] memory arr;
             return (bs[0],arr[0]);
           }
         }
        

      需要主动分配内存,进行初始化

        pragma solidity ^0.4.18;
        contract Arraryinitalok {
          function f() returns(bytes1,uint8) {
            bytes memory bs = new bytes(1);
            uint8[] memory arr = new uint8[](1);
            return (bs[0],arr[0]);
          }
        }
      

      上述代码通过new关键字进行了内存分配,现在即可正常访问第一个数组元素了

      1. 映射
        映射的声明后,不用显式初始化即可使用,只是里面不会有任何值,[回顾]:映射只能是状态变量或者是storage对象的引用
      2. 枚举
        枚举类型不用显示初始化,默认值将为0,即顺位第一值,枚举也只能是状态变量
      3. 结构体
        结构体声明后,不用显式初始化即可使用。当没有显式初始化时,其成员值均为默认值。
      4. delete操作符,具体可以看上文
    3. 作用域
      变量无论在函数内什么位置定义,其作用域均为整个函数,而非大多数据语言常见的块级作用域
      下面的例子会报错,重复定义

        pragma solidity ^0.4.18;
        contract scopeErr {
          function f() {
            {uint8 a = 0;}
            //{uint8 a = 1;}
          }
        }
      

    我们来看一个稍微隐蔽点的例子

        pragma solidity ^0.4.18;
        contract Test {
          function fTest() returns(uint8) {
            for(var i=0;i<10;i++) {
              //do sth
            }
            return i;
          }
        }
    

    学过其他语言的,会认为这样是会报错的,但是在solidity中,无论变量在函数中任意位置定义,整个函数都是可以访问的,所以i变量虽然是在for循环中被定义,但它的的作用域仍是整个函数,所以在函数内的任何位置均可以访问到这个变量
    我们来看一个更加极端的例子

        pragma solidity ^0.4.18;
        contract Test {
          function f() returns(uint8){
            if (false) {
              uint8 a =10;
            }
            return a;
          }
        }
    

    上述的代码中,虽然变量的声明看似没有执行到,但是由于变量作用域是整个函数,且由于第一部分讲到,任何变量声明后均有其默认值。故上例中将返回默认值0

  5. solidity的getter访问器
    对于所有public的状态变量(回顾:只有struct是强制internal),solidity语言编译器,提供了自动为状态变量(定义在函数外部)生成对应的getter访问器的特性,暂不支持setter(因为不能很好控制访问权限的改值函数,不一定实用)
    状态变量所创建的访问器函数,与变量同名。以internal访问时,按状态变量的方式使用,若以external的方式访问时,则需要通过访问器函数

    pragma solidty ^0.4.18;
    contract Test{
      uint public data = 10;
      function f() returns(uint,uint) {
        //分别以internal,external的方式访问
        return (data,this.data())
      }
    }
    
    contract test {
      //注意外部合约调用时,必须要先初始创建合约,不然不能访问
      Test t = new Test();
      function f() returns(address,uint) {
        return (t,t.data());
      }
    }
    

    上面的例子中,编译器会自动生成data()函数,在合约内我们可以直接以internal的方式访问data状态变量,但是在合约外我们只能用data()的方式来访问,因为访问器函数的可见性是external

    1. 枚举
      枚举的访问器与基本类型类似,均是生成与状态变量同名的函数

      pragma solidity ^0.4.18;
      contract Test {
        enum Color{red,green,yellow}
        Color public color = Color.green;
        function f() returns(Color,Color) {
          return (color,this.color());
        }
      }
      
    2. 数组
      前面的访问器函数,是一个与状态变量同名的无参函数。那么访问器一定是无参的吗,其实不然,对于数组,我们必须提供序号来使用访问器,下面我们来看一个数组的访问器示例:

      pragma solidity ^0.4.18;
      contract ArraryGetter{
        uint[] public arr = new uint[](1);
        function f() returns(uint,uint) {
          return (arr[0],this.arr(0));
        }
      }
      

    我们可以发现,对于数组访问器的使用,需要增加序号值作为参数,如果不传序号值,会报错

    1. 映射
      对于映射类型,它的访问器也是有参数的,参数为映射定义的键类型。我们来看一下映射的访问器示例:

      pragma solidity ^0.4.18;
      contract MappingGetter {
        mapping(uint => uint) public data;
        function f() returns(uint,uint) {
          data[25] = 100;
          return (data[25],this.data(25));
        }
      }
      


    回顾,映射类型只能定义为状态变量或storage变量的引用,映射也是可变长的

    1. 结构体
      结构体的访问器也是同名的函数,访问器返回结果是结构体中的每个变量

      pragma solidity ^0.4.18;
      contract Test{
        struct S {
          uint a;
          string b;
          bytes32 c;
        }
        S public s = S(10,"hello",hex"1234");
        function f() returns (uint,bytes32) {
          var(a,b,c) = this.s();
          return (a,c);
        }
      }
      

    通过上例,我们可以看出,结构体的访问器分别返回了s中的a,b,c三个变量值,但由于solidity不支持通过external的访问变长内容,故上面的代码,通过f()不能返回b的值,会报错,但是通过Remix,则可以访问到结构体中的所有变量值

    1. 一个复杂的例子
      下面是一个集合结构体,数组,映射的一个复杂例子,但访问器访问方式遵循前述的规则:

      pragma solidity ^0.4.18;
      contract Complex {
        struct Data {
          uint a;
          bytes3 b;
          mapping(uint => uint) map;
        }
        mapping(uint => mapping(bool =>Data[])) public data;
        Data[] internal arr;
        function f() returns(uint,bytes3) {
          //初始化时跳过mapping类型
          Data memory d = Data(1,0x123);
          arr.push(d);
          data[0][true] = arr;
          return this.data(0,true,0);
        }
      }
      

    上面代码中,data状态变量的访问器,有三个参数。第一个参数是第一层映射的键uint,第二个参数是映射内嵌映射的键类型bool,由于两层映射最终映射到的是数组。故而,第三个参数是数组的序号值,访问器返回的类型是结构体类型Data,所以上述代码将返回1,0x000123
    需要注意的是,访问器返回结果结构体内的映射map,由于访问器没有较好的方式来提供访问,所以直接被忽略

你可能感兴趣的:(solidity基础(3))