solidity learning (1)

学习文档笔记:
http://solidity-cn.readthedocs.io/zh/develop/layout-of-source-files.html

1.pragma solidity ^0.4.0;
这样,意味着源文件将既不允许低于 0.4.0 版本的编译器编译, 也不允许高于(包含) 0.5.0 版本的编译器编译(第二个条件因使用 ^ 被添加)

2.导入
import * as symbolName from “filename”;//或“.sol”文件
等同于import "filename" as symbolName;

3.注释
单行(//)、多行注释(/*…*/)和(/** ... */)在函数开头或内部做的注释
/** @title 形状计算器。 */
contract shapeCalculator {
    /** @dev 求矩形表明面积与周长。
    * @param w 矩形宽度。
    * @param h 矩形高度。
    * @return s 求得表面积。
    * @return p 求得周长。
    */
    function rectangle(uint w, uint h) returns (uint s, uint p) {
        s = w * h;
        p = 2 * (w + h);
    }
}

4.两者差别是什么啊?????
枚举类型¶
枚举可用来创建由一定数量的“常量值”构成的自定义类型
enum State { Created, Locked, Inactive }
枚举下标定义从左至右从零开始。
Created=0, Locked=1, Inactive =2
访问枚举方式 State.Created 实际等于数字 0


结构类型¶
结构是可以将几个变量分组的自定义类型
    struct Voter { // 结构
        uint weight;
        bool voted;
        address delegate;
        uint vote;
    }

enum ActionChoices { GoLeft, GoRight, GoStraight, SitStill }
    ActionChoices choice;
    ActionChoices constant defaultChoice = ActionChoices.GoStraight;

    function setGoStraight() public {
        choice = ActionChoices.GoStraight;
    }

    // 由于枚举类型不属于 |ABI| 的一部分,因此对于所有来自 Solidity 外部的调用,
    // "getChoice" 的签名会自动被改成 "getChoice() returns (uint8)"。
    // 整数类型的大小已经足够存储所有枚举类型的值,随着值的个数增加,
    // 可以逐渐使用 `uint16` 或更大的整数类型。
    function getChoice() public view returns (ActionChoices) {
        return choice;

我知道枚举的差别在哪里了,枚举里面的是常量,就像是我们之前设值时,性别枚举的值就有男和女,所以当你声明一个枚举类型的变量的时候,它的值要么是男,要么是女这样

5.整型¶
int / uint :分别表示有符号和无符号的不同位数的整型变量。 支持关键字 uint8 到 uint256 (无符号,从 8 位到 256 位)以及 int8 到 int256,以 8 位为步长递增。 uint 和 int 分别是 uint256 和 int256 的别名

6.定长浮点型¶-现在还不用考虑怎么使用它
警告
Solidity 还没有完全支持定长浮点型。可以声明定长浮点型的变量,但不能给它们赋值或把它们赋值给其他变量。。
fixed / ufixed:表示各种大小的有符号和无符号的定长浮点型。 在关键字 ufixedMxN 和 fixedMxN 中,M 表示该类型占用的位数,N 表示可用的小数位数。 M 必须能整除 8,即 8 到 256 位。 N 则可以是从 0 到 80 之间的任意数。 ufixed 和 fixed 分别是 ufixed128x19 和 fixed128x19 的别名。

7.地址类型¶
address:地址类型存储一个 20 字节的值(以太坊地址的大小)
可以使用 balance 属性来查询一个地址的余额, 也可以使用 transfer 函数向一个地址发送 以太币
Ether
(以 wei 为单位)

注解
如果 x 是一个合约地址,它的代码(更具体来说是它的 fallback 函数,如果有的话)会跟 transfer 函数调用一起执行(这是 EVM 的一个特性,无法阻止)。 如果在执行过程中用光了 gas 或者因为任何原因执行失败,以太币Ether交易会被打回,当前的合约也会在终止的同时抛出异常。
to.transfer(msg.value)//to为接受者的地址

8.call()\delegeteCall()
(1)为了与不符合 应用二进制接口
Application Binary Interface(ABI)
的合约交互,于是就有了可以接受任意类型任意数量参数的 call 函数(但是还是不太理解它的用处)
call 返回的布尔值表明了被调用的函数已经执行完毕(true)或者引发了一个 EVM 异常(false)。 无法访问返回的真实数据
可以使用 .gas() 修饰器
modifier
调整提供的 gas 数量类似地,也能控制提供的 以太币
Ether
的值,每个修改器出现的顺序不重要
nameReg.call.gas(1000000).value(1 ether)("register", “MyName");

(2)delegatecall 的目的是使用存储在另外一个合约中的库代码

⚠️这三个函数 call, delegatecall 和 callcode 都是非常低级的函数,需要谨慎使用。 具体来说,任何未知的合约都可能是恶意的。 你在调用一个合约的同时就将控制权交给了它,它可以反过来调用你的合约, 因此,当调用返回时要为你的状态变量的改变做好准备


9.定长字节数组¶
关键字有:bytes1, bytes2, bytes3, ..., bytes32。byte 是 bytes1 的别名。
.length 表示这个字节数组的长度(只读)
注解
可以将 byte[] 当作字节数组使用,但这种方式非常浪费存储空间,准确来说,是在传入调用时,每个元素会浪费 31 字节。 更好地做法是使用 bytes。

10.变长字节数组¶
bytes:
变长字节数组,参见 数组。它并不是值类型。
string:
变长 UTF-8 编码字符串类型,参见 数组。并不是值类型。


11.字面常数-需要好好看看
Solidity 中是没有八进制的,因此前置 0 是无效的
注解
数值字面常数表达式只要在非字面常数表达式中使用就会转换成非字面常数类型。 在下面的例子中,尽管我们知道 b 的值是一个整数,但 2.5 + a 这部分表达式并不进行类型检查,因此编译不能通过。
//在remix中编译的时候的确是会报错
browser/test.sol:4:17: TypeError: Operator + not compatible with types rational_const 5 / 2 and uint128
    uint128 b = 2.5 + a + 0.5;
                ^-----^
uint128 a = 1;
uint128 b = 2.5 + a + 0.5;

意思应该就是说因为在字面上我们可以看见不进行类型检查时a是非字面常数类型

十六进制字面常数以关键字 hex 打头,后面紧跟着用单引号或双引号引起来的字符串(例如,hex”001122FF")


12.函数
function (<parameter types>) {internal|external} [pure|constant|view|payable] [returns (<return types>)]


与参数类型相反,返回类型不能为空 —— 如果函数类型不需要返回,则需要删除整个 returns (<return types>) 部分。

有两种方法可以访问当前合约中的函数:一种是直接使用它的名字,f ,另一种是使用 this.f 。 前者适用于内部函数internal,后者适用于外部函数external
请注意,当前合约的 public 函数既可以被当作内部函数也可以被当作外部函数使用。 如果想将一个函数当作内部函数使用,就用 f 调用,如果想将其当作外部函数,使用 this.f 。

13.library ArrayUtils {
  // internal内部函数可以在内部库函数中使用,
  // 因为它们会成为同一代码上下文的一部分
…}
contract Pyramid {
  using ArrayUtils for *;//使用,然后通过ArrayUtils.函数名()调用
}

14.数据位置——重点,一直没有搞懂的地方
有三种类型,memory,storage和calldata,一般只有外部函数的参数(不包括返回参数)被强制指定为calldata。这种数据位置是只读的,不会持久化到区块链

storage存储或memory内存
memory存储位置同我们普通程序的内存类似,即分配,即使用,动态分配,越过作用域即不可被访问,等待被回收。
而对于storage的变量,数据将永远存在于区块链上。

总结¶
强制指定的数据位置:
    •    外部函数的参数(不包括返回参数): calldata,效果跟 memory 差不多
    •    状态变量: storage
默认数据位置:
    •    函数参数(包括返回参数): memory
    •    所有其它局部变量: storage

举例说明:
contract C {
    uint[] x; // x 的数据存储位置是 storage,状态变量

    // memoryArray 的数据存储位置是 memory,函数参数
    function f(uint[] memoryArray) public {
        x = memoryArray; // 将整个数组拷贝到 storage 中,可行
        var y = x;  // 分配一个指针(其中 y 的数据存储位置是 storage),可行,其他局部变量
        y[7]; // 返回第 8 个元素,可行
        y.length = 2; // 通过 y 修改 x,可行
        delete x; // 清除数组,同时修改 y,可行
        // 下面的就不可行了;需要在 storage 中创建新的未命名的临时数组
        // 但 storage 是“静态”分配的:
        // y = memoryArray;




        // 下面这一行也不可行,因为这会“重置”指针,
        // 但并没有可以让它指向的合适的存储位置。
        // delete y;

    }
}

赋值行为:

(1)当我们把一个storage类型的变量赋值给另一个storage时,我们只是修改了它的指针,一个变,另一个也变
struct S{string a;uint b;}
  S s;
    
  function convertStorage(S storage s) internal{
    S tmp = s;
    tmp.a = “Test”;//s的a的值也改变了
  }

(2)memory转为memory,memory之间是引用传递,并不会拷贝数据,一个变,另一个也跟着变

(3)memory转换为storage
因为局部变量和状态变量的类型都可能是storage。所以我们要分开来说这两种情况。
1. memory赋值给状态变量
将一个memory类型的变量赋值给一个状态变量时,实际是将内存变量拷贝到存储中,后续两者不会有任何关系
  S s;

  function memoryToState(S memory tmp) internal{
    s = tmp;//从内存中复制到状态变量中。

    //修改旧memory中的值,并不会影响状态变量
    tmp.a = “Test”;//s的就不会变了
  }

2.memory赋值给局部变量是不可以的
由于在区块链中,storage必须是静态分配存储空间的。局部变量虽然是一个storage的,但它仅仅是一个storage类型的指针。如果进行这样的赋值,实际会产生一个错误。
function assign(S s) internal{ //默认的变量是storage的指针
//error:Type struct MemoryToLocalVar.S memory is not implicitly convertible to expected type struct MemoryToLocalVar.S storage pointer.
S tmp = s; //修改变量为memory类型S memory tmp = s;即可

}

(4)storage转为memory,会创建一份独立的拷贝,两两互不影响



数组:
可以在声明时指定长度,也可以动态调整大小。
对于 存储storage的数组来说,元素类型可以是任意的(即元素也可以是数组类型,映射类型或者结构体)。
 对于 内存memory的数组来说,元素类型不能是映射类型,如果作为 public 函数的参数,它只能是 ABI 类型。

(1)在memory中:一经创建,内存memory数组的大小就是固定的(但却是动态的,也就是说,它依赖于运行时的参数
1.内存中的数组是不允许创建变长数组的
uint[] memory a;//wrong
2.数组字面常数是一种定长的 内存memory数组类型
在memory中,声明定长数组:
uint[] memory a = new uint[](2);

在memory中,声明定长数组并初始化:
uint[2] memory a = [uint(1),2];
uint[] memory a = [uint(1),2];
//wrong,定长的 内存memory数组并不能赋值给变长的 内存memory数组
为什么要在第一个元素一前面加上uint的类型,这是因为我们前面声明的是uint的数组,而在solidity中有uint8,uint248等多种不同位数的无符号int型常量,在不声明的情况下,默认使用uint256,而我们初始化的数组元素是1和2,系统会判定为uint8,所以需要我们在第一个元素1前面强制转化为uint型

当然,我们也可以如下的声明方式:
uint8[2] memory a = [1,2];
但要注意,那么a数组里面就不可以出现2进制里面大于8位的数了

定长数组是无法通过length和push方法更改长度或插入的

(2)在storage中

1.在storage中,声明定长数组,并初始化变量:
uint[2] a = [1,2];

2.在storage中,声明变长数组,并初始化变量:

uint[] a = [1,2];

3.二维数组初始化,行列是反过来定义的:
uint[2][3] a = [[1,1],[2,2],[3,3]];
但访问是相同的:
访问第三个动态数组的第二个元素,你应该使用 a[2][1]


(3)
bytes 和 string 类型的变量是特殊的数组。 bytes 类似于 byte[],但它在 calldata 中会被“紧打包”(译者注:将元素连续地存在一起,不会按每 32 字节一单元的方式来存放)。 string 与 bytes 相同,但(暂时)不允许用长度或索引来访问。
(4)变长的 存储storage数组以及 bytes 类型(而不是 string 类型)都有一个叫做 push 的成员函数

15.一个合约如何调用另一个合约
contract MappingExample {
    mapping(address => uint) public balances;

    function update(uint newBalance) public {
        balances[msg.sender] = newBalance;
    }
}

contract MappingUser {
    function f() public returns (uint) {
        MappingExample m = new MappingExample();
        m.update(100);
        return m.balances(this);//this是该合约的地址
    }
}

16.delete的作用:
delete a 的结果是将 a 的类型在初始化时的值赋值给 a。即对于整型变量来说,相当于 a = 0, 但 delete 也适用于数组,对于动态数组来说,是将数组的长度设为 0,而对于静态数组来说,是将数组中的所有元素重置。 如果对象是结构体,则将结构体中的所有属性重置。
delete 对整个映射是无效的(因为映射的键可以是任意的,通常也是未知的)。 因此在你删除一个结构体时,结果将重置所有的非映射属性,这个过程是递归进行的,除非它们是映射。 然而,单个的键及其映射的值是可以被删除的。

contract DeleteExample {
    uint data;
    uint[] dataArray;

    function f() public {
        uint x = data;
        delete x; // 将 x 设为 0,并不影响数据
        delete data; // 将 data 设为 0,并不影响 x,因为它仍然有个副本
        uint[] storage y = dataArray;
//可以通过改变dataArray来改变y,也可以通过改变y来改变dataArray
        delete dataArray;
        // 将 dataArray.length 设为 0,但由于 uint[] 是一个复杂的对象,y 也将受到影响,
        // 因为它是一个存储位置是 storage 的对象的别名。
        // 另一方面:"delete y" 是非法的,引用了 storage 对象的局部变量只能由已有的 storage 对象赋值。
    }
}

17.基本类型之间的转换
(1)隐式转换
int8 不能转换成 uint256(因为 uint256 不能涵盖某些值,例如,-1)。 更进一步来说,无符号整型可以转换成跟它大小相等或更大的字节类型,但反之不能。 任何可以转换成 uint160 的类型都可以转换成 address 类型。
(2)显式转换
但有些时候会出问题
int8 y = -3;
uint x = uint(y);
这段代码的最后,x 的值将是 0xfffff..fd (64 个 16 进制字符),因为这是 -3 的 256 位补码形式。

如果一个类型显式转换成更小的类型,相应的高位将被舍弃
uint32 a = 0x12345678;
uint16 b = uint16(a); // 此时 b 的值是 0x5678

类型推断
为了方便起见,没有必要每次都精确指定一个变量的类型,编译器会根据分配该变量的第一个表达式的类型自动推断该变量的类型
uint24 x = 0x123;
var y = x;
这里 y 的类型将是 uint24。不能对函数参数或者返回参数使用 var。

18.时间seconds、 minutes、 hours、 days、 weeks 和 years
years 后缀已经不推荐使用了,因为从 0.5.0 版本开始将不再支持。

这些后缀不能直接用在变量后边。如果想用时间单位(例如 days)来将输入变量换算为时间,你可以用如下方式来完成:
function f(uint start, uint daysAfter) public {
    if (now >= start + daysAfter * 1 days) {
        // ...
    }
}

19.在一个私链上,你很有可能碰到由于 sha256、ripemd160 或者 ecrecover 引起的 Out-of-Gas。这个原因就是他们被当做所谓的预编译合约而执行,并且在第一次收到消息后这些合约才真正存在(尽管合约代码是硬代码)。发送到不存在的合约的消息非常昂贵,所以实际的执行会导致 Out-of-Gas 错误。在你的合约中实际使用它们之前,给每个合约发送一点儿以太币,比如 1 Wei。这在官方网络或测试网络上不是问题。

20.
合约相关¶
this (current contract's type):
当前合约,可以明确转换为 地址类型。
selfdestruct(address recipient):
销毁合约,并把余额发送到指定 地址类型。
suicide(address recipient):
与 selfdestruct 等价,但已不推荐使用。

21.solidity与 Javascript 和 C 不同的是,它们可能返回任意数量的参数作为输出。

例子:
    function arithmetics(uint _a, uint _b)
        public
        pure
        returns (uint o_sum, uint o_product)
    {
        o_sum = _a + _b;
        o_product = _a * _b;
    }


22.
JavaScript 中的大部分控制结构在 Solidity 中都是可用的,除了 switch 和 goto。 因此 Solidity 中有 if,else,while,do,for,break,continue,return,``? :``这些与在 C 或者 JavaScript 中表达相同语义的关键词。
用于表示条件的括号 不可以 被省略,单语句体两边的花括号可以被省略。

注意,与 C 和 JavaScript 不同, Solidity 中非布尔类型数值不能转换为布尔类型,因此 if (1) { ... } 的写法在 Solidity 中 无效 。

23.省略函数参数名称¶
未使用参数的名称(特别是返回参数)可以省略。这些参数仍然存在于堆栈中,但它们无法访问

contract C {
    // 省略参数名称
    function func(uint k, uint) public pure returns(uint) {
        return k;
    }

24.通过 new 创建合约¶
使用关键字 new 可以创建一个新合约。待创建合约的完整代码必须事先知道,因此递归的创建依赖是不可能的。

即一个合约中创建另一个合约,另一个合约一定要在该合约之前就已经声明了


25.赋值¶
解构赋值和返回多值¶
Solidity 内部允许元组 (tuple) 类型,也就是一个在编译时元素数量固定的对象列表,列表中的元素可以是不同类型的对象。这些元组可以用来同时返回多个数值,也可以用它们来同时给多个新声明的变量或者既存的变量(或通常的 LValues):
pragma solidity >0.4.23 <0.5.0;

contract C {
    uint[] data;

    function f() public pure returns (uint, bool, uint) {
        return (7, true, 2);
    }

    function g() public {
        //基于返回的元组来声明变量并赋值
        (uint x, bool b, uint y) = f();
        //交换两个值的通用窍门——但不适用于非值类型的存储 (storage) 变量。
        (x, y) = (y, x);
        //元组的末尾元素可以省略(这也适用于变量声明)。
        (data.length,,) = f(); // 将长度设置为 7
        //省略元组中末尾元素的写法,仅可以在赋值操作的左侧使用,除了这个例外:
        (x,) = (1,);
        //(1,) 是指定单元素元组的唯一方法,因为 (1)
        //相当于 1。
    }
}

26.变量声明后将有默认初始值,其初始值字节表示全部为零。任何类型变量的“默认值”是其对应类型的典型“零状态”,所以并不需要自己赋值,声明即可
例如, bool 类型的默认值是 false 。 uint 或 int 类型的默认值是 0 。对于静态大小的数组和 bytes1 到 bytes32 ,每个单独的元素将被初始化为与其类型相对应的默认值。 最后,对于动态大小的数组, bytes 和 string 类型,其默认缺省值是一个空数组或字符串。


27.错误处理:
在下例中,你可以看到如何轻松使用``require``检查输入条件以及如何使用``assert``检查内部错误,注意,你可以给 require 提供一个消息字符串,而 assert 不行。
pragma solidity ^0.4.22;

contract Sharer {
    function sendHalf(address addr) public payable returns (uint balance) {
        require(msg.value % 2 == 0, "Even value required.");
        uint balanceBeforeTransfer = this.balance;
        addr.transfer(msg.value / 2);
                    //由于转移函数在失败时抛出异常并且不能在这里回调,因此我们应该没有办法仍然有一半的钱。
        assert(this.balance == balanceBeforeTransfer - msg.value / 2);
        return this.balance;
    }
}

下列情况将会产生一个 assert 式异常:
    1    如果你访问数组的索引太大或为负数(例如 x[i] 其中 i >= x.length 或 i < 0)。
    2    如果你访问固定长度 bytesN 的索引太大或为负数。
    3    如果你用零当除数做除法或模运算(例如 5 / 0 或 23 % 0 )。
    4    如果你移位负数位。
    5    如果你将一个太大或负数值转换为一个枚举类型。
    6    如果你调用内部函数类型的零初始化变量。
    7    如果你调用 assert 的参数(表达式)最终结算为 false。
下列情况将会产生一个 require 式异常:
    1    调用 throw 。
    2    如果你调用 require 的参数(表达式)最终结算为 false 。
    3    如果你通过消息调用调用某个函数,但该函数没有正确结束(它耗尽了 gas,没有匹配函数,或者本身抛出一个异常),上述函数不包括低级别的操作 call , send , delegatecall 或者 callcode 。低级操作不会抛出异常,而通过返回 false 来指示失败。
    4    如果你使用 new 关键字创建合约,但合约没有正确创建(请参阅上条有关”未正确完成“的定义)。
    5    如果你对不包含代码的合约执行外部函数调用。
    6    如果你的合约通过一个没有 payable 修饰符的公有函数(包括构造函数和 fallback 函数)接收 Ether。
    7    如果你的合约通过公有 getter 函数接收 Ether 。
    8    如果 .transfer() 失败。
在内部, Solidity 对一个 require 式的异常执行回退操作(指令 0xfd )并执行一个无效操作(指令 0xfe )来引发 assert 式异常。 在这两种情况下,都会导致 EVM 回退对状态所做的所有更改。回退的原因是不能继续安全地执行,因为没有实现预期的效果。 因为我们想保留交易的原子性,所以最安全的做法是回退所有更改并使整个交易(或至少是调用)不产生效果。 请注意, assert 式异常消耗了所有可用的调用 gas ,而从 Metropolis 版本起 require 式的异常不会消耗任何 gas。
下边的例子展示了如何在 revert 和 require 中使用错误字符串:
pragma solidity ^0.4.22;

contract VendingMachine {
    function buy(uint amount) payable {
        if (amount > msg.value / 2 ether)
            revert("Not enough Ether provided.");
        // 下边是等价的方法来做同样的检查:
        require(
            amount <= msg.value / 2 ether,
            "Not enough Ether provided."
        );
        // 执行购买操作
    }
}
这里提供的字符串应该是经过 ABI 编码 之后的,因为它实际上是调用了 Error(string) 函数。在上边的例子里,revert("Not enough Ether provided."); 会产生如下的十六进制错误返回值:
0x08c379a0                                                         // Error(string) 的函数选择器
0x0000000000000000000000000000000000000000000000000000000000000020 // 数据的偏移量(32)
0x000000000000000000000000000000000000000000000000000000000000001a // 字符串长度(26)
0x4e6f7420656e6f7567682045746865722070726f76696465642e000000000000 // 字符串数据("Not enough Ether





28.合约
contract OwnedToken {

    TokenCreator creator;

    function OwnedToken(bytes32 _name) public {
//这里的意思就是显示地将msg.sender这个地址转换成了TokenCreator合约,这样之后就可以调用该合约中的函数了
        creator = TokenCreator(msg.sender);

       creator.isTokenTransferOK(owner, newOwner)

    }


contract TokenCreator {…}

29.可见性
contract C {
    uint private data;

    function f(uint a) private returns(uint b) { return a + 1; }//不可继承
    function setData(uint a) public { data = a; }
    function getData() public returns(uint) { return data; }
    function compute(uint a, uint b) internal returns (uint) { return a+b; }//可继承
}

contract D {//这能访问public
    function readData() public {
        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 {//继承至C,所以可以访问internal,public
    function g() public {
        C c = new C();
        uint val = compute(3, 5); // access to internal member (from derived to parent contract)
    }
}


30.getter函数
(1)就是在合约中声明的状态变量其实都自动地生成了getter函数,就是可以像访问函数一样访问它的值,在合约外访问时就可以直接 合约名.data()
pragma solidity ^0.4.0;

contract C {
    uint public data = 42;
}

contract Caller {
    C c = new C();
    function f() public {
        uint local = c.data();
    }
}
(2)如果是在合约内部访问,它有两种访问的形式:internal和external
internal则是直接变量名访问即可
external则是使用this.data()
pragma solidity ^0.4.0;

contract C {
    uint public data;
    function x() public {
        data = 3; // internal access
        uint val = this.data(); // external access
    }
}


31.
contract Complex {
//Note that the mapping in the struct is omitted because there is no good way to provide the key for the mapping.
    struct Data {
//如果在一个结构体中声明了一个映射,一般赋值时都先省略,然后在赋值mapping,因为它的key是不固定的
        uint a;
        bytes3 b;
        mapping (uint => uint) map;
    }
    mapping (uint => mapping(bool => Data[])) public data;
//调用方法:data[arg1][arg2][arg3].a
}

32.修饰器modifier
继承后可以直接使用被继承处的修饰器

使用修改器实现的一个防重复进入的例子。
pragma solidity ^0.4.0;
contract Mutex {
    bool locked;
    modifier noReentrancy() {
        if (locked) throw;
        locked = true;
        _;//函数体f()return前的内容执行的区域
        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语句,仅仅跳出当前的修改器和函数体。返回的变量会被赋值,但整个执行逻辑会在前一个修改器后面定义的”_"后继续执行。

33.fallback function \ call()\send()
contract ExecuteFallback{

  //回退事件,会把调用的数据打印出来
  event FallbackCalled(bytes data);
  //fallback函数,注意是没有名字的,没有参数,没有返回值的
  function(){
    FallbackCalled(msg.data);
  }

  //调用已存在函数的事件,会把调用的原始数据,请求参数打印出来
  event ExistFuncCalled(bytes data, uint256 para);
  //一个存在的函数
  function existFunc(uint256 para){
    ExistFuncCalled(msg.data, para);
  }

  // 模拟从外部对一个存在的函数发起一个调用,将直接调用函数
  function callExistFunc(){
    bytes4 funcIdentifier = bytes4(keccak256("existFunc(uint256)"));
    this.call(funcIdentifier, uint256(1));
  }

  //模拟从外部对一个不存在的函数发起一个调用,由于匹配不到函数,将调用回退函数
  function callNonExistFunc(){
    bytes4 funcIdentifier = bytes4(keccak256("functionNotExist()"));
    this.call(funcIdentifier);
  }
}

Call():call()是一个底层的接口,用来向一个合约发送消息,进行合约之间的交互,但是很不安全,一般是不用的。这个函数是这样的,第一个参数是该访问的函数的名字,后面的参数则是该函数所需的参数,调用它的是一个合约的地址

回退函数:则是当调用一个合约里的某个函数时,如果该函数并不存在,那么就会去调用该回调函数,回调函数无参,无名,无返回值,你可以通过在里面返回个什么或者emit一个事件来显示调用了该回退函数,该函数还是很有用的

msg.data其实就是你调用一个函数后,函数名,参数进行keccak256哈希后连接起来的data

send()函数发送ether
当我们使用address.send(ether to send)向某个合约直接转帐时,由于这个行为没有发送任何数据,所以接收合约总是会调用fallback函数
function() payable{fallbackTrigged(msg.data);}

  function deposit() payable{//用来给合约存点钱
  }
在上述的代码中,我们先要使用deposit()合约存入一些ether,否则由于余额不足,调用send()函数将报错

fallbackTrigged[
  "0x"
]
可以看到,我们成功使用send()发送了1wei到合约,触发了fallback函数,附带的数据是0x(bytes类型的默认空值),空数据。
这里需要特别注意的是:
    1.    如果我们要在合约中通过send()函数接收,就必须定义fallback函数,否则会抛异常。
    2.    fallback函数必须增加payable关键字,否则send()执行结果将会始终为false。



fallback中的限制
send()函数总是会调用fallback,这个行为非常危险,著名的DAO被黑也与这有关。如果我们在分红时,对一系列帐户进行send()操作,其中某个做恶意帐户中的fallback函数实现了一个无限循环,将因为gas耗尽,导致所有send()失败。为解决这个问题,send()函数当前即便gas充足,也只会附带限定的2300gas,故而fallback函数内除了可以进行日志操作外,你几乎不能做任何操作。如果你还想做一些复杂的操作,解决方案看这里:http://me.tryblockchain.org/blockchain-solidity-fallback-bestpractice.html。
下述行为消耗的gas都将超过fallback函数限定的gas值:
    •    向区块链中写数据
    •    创建一个合约
    •    调用一个external的函数
    •    发送ether
所以一般,我们只能在fallback函数中进行一些日志操作:

33.底层的日志接口(Low-level Interface to Logs):可以代替event
通过函数log0,log1,log2,log3,log4,可以直接访问底层的日志组件。logi表示总共有带i + 1个参数(i表示的就是可带参数的数目,只是是从0开始计数的)。其中第一个参数会被用来做为日志的数据部分,其它的会做为主题(topics)。前面例子中的事件可改为如下:
log3(
    msg.value,
    0x50cb9fe53daa9737b786ab3646f04d0150dc50ef4e75f59509d83667ad5adb20,
    msg.sender,
    _id
);
其中的长16进制串是事件的签名,计算方式是keccak256("Deposit(address,hash256,uint256)")


34.继承
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)

当基类的构造函数中如果需要传参,那么继承它时的方式是:
contract Base {
    uint x;
    function Base(uint _x) { x = _x; }
}


contract Derived is Base(7) {
    function Derived(uint _y) Base(_y * _y) {
    }
}

继承写的顺序是很重要的,从继承少的到多的

35.抽象(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"; }
}
如果一个合约从一个抽象合约里继承,但却没实现所有函数,那么它也是一个抽象合约。

36.接口(interface)
接口与抽象合约类似,与之不同的是,接口内没有任何函数是已实现的,同时还有如下限制:
    1.    不能继承其它合约,或接口。
    2.    不能定义构造器
    3.    不能定义变量
    4.    不能定义结构体
    5.    不能定义枚举类
其中的一些限制可能在未来放开。
接口基本上限制为合约ABI定义可以表示的内容,ABI和接口定义之间的转换应该是可能的,不会有任何信息丢失。
接口用自己的关键词表示:
interface Token {
    function transfer(address recipient, uint amount);
}
合约可以继承于接口,因为他们可以继承于其它的合约。

37.library
使用库合约的合约,可以将库合约视为隐式的父合约(base contracts),当然它们不会显式的出现在继承关系中。意思就是不用写is来继承,直接可以在合约中使用:
library Set {
  struct Data { mapping(uint => bool) flags; }
}

contract C {
    Set.Data knownValues;
}
但调用库函数的方式非常类似,如库L有函数f(),使用L.f()即可访问。此外,internal的库函数对所有合约可见,如果把库想像成一个父合约就能说得通了。当然调用内部函数使用的是internal的调用惯例,这意味着所有internal类型可以传进去,memory类型则通过引用传递,而不是拷贝的方式。


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

对比普通合约来说,库的限制:
    •    无状态变量(state variables)。
    •    不能继承或被继承
    •    不能接收ether。


附着库(Using for)
指令using A for B;用来附着库里定义的函数(从库A)到任意类型B。这些函数将会默认接收调用函数对象的实例作为第一个参数。语法类似,python中的self变量一样。
using A for *的效果是,库A中的函数被附着在做任意的类型上。
在这两种情形中,所有函数,即使那些第一个参数的类型与调用函数的对象类型不匹配的,也被附着上了。类型检查是在函数被真正调用时,函数重载检查也会执行。
using A for B;指令仅在当前的作用域有效,且暂时仅仅支持当前的合约这个作用域,后续也非常有可能解除这个限制,允许作用到全局范围。如果能作用到全局范围,通过引入一些模块(module),数据类型将能通过库函数扩展功能,而不需要每个地方都得写一遍类似的代码了。
上面的例子就改成了:

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;
    }
}
//其实就是本来要访问的话的语句是Set.insert(knownValues, value),现在是knownValues.insert(value),即将库中的函数都附着在了结构体struct Data上,因为之前库函数的第一个参数的声明其实也是这个结构体(self),附着后调用时就可以省略掉第一个参数了,就算没有结构体,那么附着的一定是函数的第一个参数声明的那个self的类型
如:
library Search {
    function indexOf(uint[] storage self, uint value)

contract C {
    using Search for uint[];
    uint[] data;









posted @ 2018-08-27 16:15  慢行厚积  阅读(1243)  评论(0编辑  收藏  举报