【Solidity】6. 合约 - 深入理解Solidity

索引

  • 【Solidity】1.一个Solidity源文件的布局
  • 【Solidity】2.合约的结构体
  • 【Solidity】3.类型
  • 【Solidity】4.单位和全局可变量
  • 【Solidity】5.表达式和控制结构
  • 【Solidity】6. 合约
  • 【Solidity】7. 部件
  • 【Solidity】8. 杂项

合约

Solidity的合约类似于面向对象语言的类。 它们包含可以修改这些变量的状态变量和函数中的持久性数据。 在不同的合同(实例)上调用函数将执行EVM函数调用,从而切换上下文以使状态变量无法访问。

创建合约

合同可以从“外部”或“固定”合同中创建。 当创建合同时,其构造函数(与合同名称相同的函数)将被执行一次。

构造函数是可选的。 只允许一个构造函数,这意味着不支持重载。

从web3.js,即JavaScript API,这样做如下:

// 需要指定一些来源,包括下面的数据参数的合同名称
var source = "contract CONTRACT_NAME { function CONTRACT_NAME(uint a, uint b) {} }";

// 由编译器生成的json abi数组
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);
// 部署新合约
var contractInstance = MyContract.new(
    10,
    11,
    {from: myAccount, gas: 1000000}
);

在内部,构造函数参数在合同代码本身之后传递,但如果使用web3.js,则不需要关心此参数。

如果合约要创建另一个合约,创建合约的源代码(和二进制文件)必须由创建者知道。 这意味着循环创建依赖是不可能的。

pragma solidity ^0.4.0;

contract OwnedToken {
    // TokenCreator是如下定义的合约类型.
    // 只要不用于创建新合约就可以参考它。
    TokenCreator creator;
    address owner;
    bytes32 name;

    // 这是注册创建者和分配名称的构造函数。
    function OwnedToken(bytes32 _name) {
        // 状态变量通过其名称访问,而不是通过例如this.owner. 这也适用于函数,特别是在构造函数中,只能称之为“内部”,因为合约本身不存在t.
        owner = msg.sender;
        // 我们从`address'到`TokenCreator`做一个明确的类型转换,并假定调用合约的类型是TokenCreator,没有真正的方法来检查。
        creator = TokenCreator(msg.sender);
        name = _name;
    }

    function changeName(bytes32 newName) {
        // 只有创建者可以改变名称 - 比较是可能的,因为契约可以隐式转换为地址。
        if (msg.sender == address(creator))
            name = newName;
    }

    function transfer(address newOwner) {
        // 只有当前所有者才能转移令牌。
        if (msg.sender != owner) return;
        // 我们也想问创建者是否转让是罚款。 请注意,这称为下面定义的合同的功能。 如果调用失败(例如由于out-of-ga),这里的执行将立即停止。
        if (creator.isTokenTransferOK(owner, newOwner))
            owner = newOwner;
    }
}

contract TokenCreator {
    function createToken(bytes32 name)
       returns (OwnedToken tokenAddress)
    {
        // 创建一个Token合约,返回地址.
        // 从JavaScript方面,返回类型只是“地址”,因为这是ABI中最接近的类型。
        return new OwnedToken(name);
    }

    function changeName(OwnedToken tokenAddress, bytes32 name) {
        // 再次,外部类型的“tokenAddress”只是“地址”。
        tokenAddress.changeName(name);
    }

    function isTokenTransferOK(
        address currentOwner,
        address newOwner
    ) returns (bool ok) {
        // 检查一些任意条件。
        address tokenAddress = msg.sender;
        return (keccak256(newOwner) & 0xff) == (bytes20(tokenAddress) & 0xff);
    }
}

可见性和Getters

由于Solidity知道两种函数调用(内部函数调用不会创建实际的EVM调用(也称为“消息调用”)和外部函数调用),因此函数和状态变量有四种可见性类型。

函数可以被指定为外部的,公共的,内部的或私有的,默认是公共的。 对于状态变量,外部是不可能的,默认是内部的。

external:
外部功能是合约接口的一部分,这意味着它们可以通过其他合约和交易进行调用。 外部函数f不能在内部调用(即f()不起作用,但this.f()可以正常工作

public:
公共功能是合同接口的一部分,可以在内部或通过消息进行调用。 对于公共状态变量,生成自动Getters函数(见下文)。

internal:
这些功能和状态变量只能在内部进行访问(即从当前合约或从其中获得的合约),而不使用这些变量。

private:
私有函数和状态变量只对其中定义的合约而不是衍生合约可见。

一切,这是一个合同里面是所有外部观察者可见。 使某些私有化只能阻止其他合约访问和修改信息,但仍然可以在整个世界之外的块链可见。

可见性说明符在状态变量的类型和函数的参数列表和返回参数列表之后给出。

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); // 错误:成员“f”不可见
        c.setData(3);
        local = c.getData();
        local = c.compute(3, 5); // 错误:成员“compute”是不可见的
    }
}


contract E is C {
    function g() {
        C c = new C();
        uint val = compute(3, 5);  // 访问内部成员(从派生到父合约)
    }
}

getter函数

编译器会自动为所有公共状态变量创建getter函数。 对于下面给出的合约,编译器将生成一个名为data的函数,它不接受任何参数,并返回一个uint,即状态变量数据的值。 状态变量的初始化可以在声明中完成。

pragma solidity ^0.4.0;

contract C {
    uint public data = 42;
}


contract Caller {
    C c = new C();
    function f() {
        uint local = c.data();
    }
}

getter具有外部可视性。 如果符号在内部被访问(即没有这个),则被评估为状态变量。 如果从外部访问(即使用此方式),则将其评估为函数。

pragma solidity ^0.4.0;

contract C {
    uint public data;
    function x() {
        data = 3; // 内部访问
        uint val = this.data(); // 外部访问
    }
}

下一个例子有点复杂:

pragma solidity ^0.4.0;

contract Complex {
    struct Data {
        uint a;
        bytes3 b;
        mapping (uint => uint) map;
    }
    mapping (uint => mapping(bool => Data[])) public data;
}

它将生成以下形式的函数:

function data(uint arg1, bool arg2, uint arg3) returns (uint a, bytes3 b) {
    a = data[arg1][arg2][arg3].a;
    b = data[arg1][arg2][arg3].b;
}

请注意,结构中的映射被省略,因为没有提供映射密钥的好方法。

功能修饰符

修饰符可用于轻松更改功能的行为。 例如,它们可以在执行功能之前自动检查条件。 修饰符是合同的可继承属性,可能会被派生合同覆盖。

pragma solidity ^0.4.11;

contract owned {
    function owned() { owner = msg.sender; }
    address owner;

    // 此合约仅定义修饰符,但不使用它 - 它将用于派生合约。
    // 函数体插入其中,特殊符号“_”; 在修饰符的定义出现。
    // 这意味着如果所有者调用此函数,则执行该函数,否则抛出异常。
    modifier onlyOwner {
        require(msg.sender == owner);
        _;
    }
}


contract mortal is owned {
    // 这个合同从“拥有”继承“onlyOwner” - “变体”,并将其应用于“关闭”功能,这导致“关闭”的调用只有由存储的所有者进行生效。
    function close() onlyOwner {
        selfdestruct(owner);
    }
}


contract priced {
    // 修饰符可以接收参数:
    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; }

    // 在此处也提供“payable”关键字很重要,否则功能将自动拒绝发送给它的所有以太网。
    function register() payable costs(price) {
        registeredAddresses[msg.sender] = true;
    }

    function changePrice(uint _price) onlyOwner {
        price = _price;
    }
}

contract Mutex {
    bool locked;
    modifier noReentrancy() {
        require(!locked);
        locked = true;
        _;
        locked = false;
    }

    /// 此函数受互斥体保护,这意味着来自msg.sender.call中的重入调用不能再次调用f。
    /// “return 7”语句将7分配给返回值,但仍然在修改器中执行“locked = false”语句。
    function f() noReentrancy returns (uint) {
        require(msg.sender.call());
        return 7;
    }
}

多个修饰符通过在空白分隔的列表中指定来应用于函数,并按所呈现的顺序进行计算。

在早期版本的Solidity中,具有修饰符的函数中的return语句行为不同。

修饰符或函数体的显式返回仅离开当前修饰符或函数体。 返回变量被分配,并且控制流程在前面的修改器中的“_”之后继续。

修饰符参数允许使用任意表达式,在此上下文中,可以在修饰符中看到从函数可见的所有符号。 在修改器中引入的符号在函数中不可见(因为它们可能通过覆盖而改变)。

常数变量

状态变量可以声明为常数。 在这种情况下,它们必须从编译时的常量表达式分配。 不允许任何访问存储,块链数据(例如现在,this.balance或block.number)或执行数据(msg.gas)或调用外部契约的表达式。 可能会对内存分配造成副作用的表达式,但可能对其他内存对象有副作用的表达式不是。 允许内置函数keccak256,sha256,ripemd160,ecrecover,addmod和mulmod(尽管它们调用外部契约)。

允许内存分配器副作用的原因在于可以构建复杂的对象,例如。 查找表。 此功能尚未完全使用。

编译器不保留这些变量的存储空间,并且每次出现都会被相应的常量表达式替换(可能由优化程序计算为单个值)。

不是所有类型的常量都是在这个时候实现的。 唯一支持的类型是值类型和字符串。

pragma solidity ^0.4.0;

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

查看功能

函数可以被声明为view,在这种情况下,它们承诺不修改状态。

以下语句被认为是修改状态:

1.写入状态变量。
2.发射事件..
3.创建其他合约。
4.使用selfdestruct
5.通过调用发送Ether
6.调用其他函数不被标记view或者pure
7.使用低等级调用
8.使用包含某些操作码的内联汇编。

pragma solidity ^0.4.16;

contract C {
    function f(uint a, uint b) view returns (uint) {
        return a * (b + 42) + now;
    }
}

constantview的别名.

Getter方法被标记为view

编译器并没有强制执行view方法不修改状态。

纯函数

函数可以声明为pure,在这种情况下,它们承诺不会从该状态中读取或修改该状态。

除了上述状态修改语句的列表外,以下是从状态读取的:

1.从状态变量读取。
2.访问this.balance

.balance
3.访问blocktxmsg的任何成员(除了msg.sigmsg.data)。
4.调用任何未标记为pure的功能。
5.使用包含某些操作码的内联汇编。

pragma solidity ^0.4.16;

contract C {
    function f(uint a, uint b) pure returns (uint) {
        return a * (b + 42);
    }
}

编译器并没有强制执行纯粹的方法不是从状态中读取的。

后备函数

合约可以有一个未命名的功能。 这个函数不能有参数,不能返回任何东西。 如果没有其他功能与给定的功能标识符(或者根本没有提供任何数据)匹配,则在对合约的调用中执行。

此外,只要合约收到普通的Ether(无数据),就执行此功能。 在这种情况下,函数调用通常只有很少的气体(精确的是2300个gas),所以重要的是使后备功能尽可能的便宜。

特别是,以下操作将比提供给回退功能的津贴消耗更多的气体:

  • 写入存储
  • 创建合约
  • 称为消耗大量gas的外部功能
  • 发送Ether

请确保您彻底测试您的后备功能,以确保执行成本低于2300gas,然后再部署合同。

直接接收Ether(即不使用函数调用,即使用发送或传输)但不定义回退函数的合约引发异常,发回Ether(在Solidity v0.4.0之前不同)。 因此,如果您希望您的合同获得Ether,您必须实施一个备用功能。

pragma solidity ^0.4.0;

contract Test {
    // 对于发送到此合同的所有邮件(没有其他功能),将调用此函数。
    // 将Ether发送到此合约将导致异常,因为fallback函数没有“payable”修饰符。
    function() { x = 1; }
    uint x;
}


// 这个合同让所有的Ether发送给它,没有办法让它回来。
contract Sink {
    function() payable { }
}


contract Caller {
    function callTest(Test test) {
        test.call(0xabcdef01); // 哈希不存在
        // 导致test.x成为== 1。

        // 以下将不会编译,但即使有人向该合约发送了Ether,该事务将失败并拒绝Ether。
        //test.send(2 ether);
    }
}

事件

事件允许方便地使用EVM记录工具,这又可以用来“调用”dapp的用户界面中的JavaScript回调,该回调监听这些事件。

事件是合同的继承成员。 当它们被调用时,它们使参数存储在事务的日志中 - 块链中的特殊数据结构。 这些日志与合同的地址相关联,并且将被并入块链,只要块可以访问(永远是Frontier和Homestead,但是这可能会随着宁静而改变)。 日志和事件数据无法从合同中获取(甚至不是从创建它们的合同)。

日志的SPV证明是可能的,所以如果一个外部实体向合同提供了这样的证明,它可以检查该日志是否存在于该块内。 但是请注意,必须提供块头,因为合约只能看到最后256个块的哈希值。

最多三个参数可以接收索引的属性,这将导致搜索相应的参数:可以在用户界面中过滤索引参数的特定值。

如果使用数组(包括字符串和字节)作为索引参数,则将其Keccak-256哈希存储为主题。

除了使用匿名说明符声明事件之外,事件签名的散列是其中一个主题。 这意味着不可能按名称过滤特定的匿名事件。

所有非索引的参数都将存储在日志的数据部分。

索引参数不会自己存储。 您只能搜索值,但不可能自己检索值。

pragma solidity ^0.4.0;

contract ClientReceipt {
    event Deposit(
        address indexed _from,
        bytes32 indexed _id,
        uint _value
    );

    function deposit(bytes32 _id) payable {
        // 任何调用这个函数(甚至是深嵌套的)都可以从JavaScript API中通过过滤`Deposit`来调用。
        Deposit(msg.sender, _id, msg.value);
    }
}

JavaScript API中的用法如下:

var abi = /* abi由编译器生成*/;
var ClientReceipt = web3.eth.contract(abi);
var clientReceipt = ClientReceipt.at("0x1234...ab67" /* 地址 */);

var event = clientReceipt.Deposit();

// 监视更改
event.watch(function(error, result){
    // 结果将包含各种信息,包括给予Deposit调用的参数。
    if (!error)
        console.log(result);
});

// 或者通过回调立即开始观看
var event = clientReceipt.Deposit(function(error, result) {
    if (!error)
        console.log(result);
});

低层次的接口日志

还可以通过函数log0,log1,log2,log3和log4访问记录机制的低级接口。 logi采用类型为byte32的i + 1参数,其中第一个参数将用于日志的数据部分,而其他参数用作主题。 上述事件调用可以以相同的方式执行

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

其中长十六进制数等于keccak256("Deposit(address,hash256,uint256)") ,事件的签名。

了解事件的其他资源

  • Javascript文档
  • events使用的例子

- 如何在js中访问它们

遗产

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

所有函数调用都是虚拟的,这意味着调用最多的派生函数,除非明确地给出了合同名称。

当合同从多个合同继承时,在块链上只创建一个合同,并将所有基础合同的代码复制到创建的合同中。

一般继承系统与Python非常相似,特别是关于多重继承。

以下示例给出了详细信息。

pragma solidity ^0.4.0;

contract owned {
    function owned() { owner = msg.sender; }
    address owner;
}


// 使用is来自另一个合约.派生合同可以访问所有非私人成员,包括内部函数和状态变量。 这些不能通过`this`从外部访问。
contract mortal is owned {  //派生
    function kill() {
        if (msg.sender == owner) selfdestruct(owner);
    }
}


// 仅提供这些抽象契约以使编译器已知接口。 注意没有body的函数。 如果一个合约没有实现所有的功能,它只能用作一个接口。
contract Config {
    function lookup(uint id) returns (address adr);
}


contract NameReg {
    function register(bytes32 name);
    function unregister();
 }


// 多重继承是可能的。 注意,“拥有”也是“凡人”的基类,但只有一个“拥有”的实例(对于C ++中的虚拟继承)。
contract named is owned, mortal {
    function named(bytes32 name) {
        Config config = Config(0xd5f9d8d94886e70b06e474c3fb14fd43e2f23970);
        NameReg(config.lookup(1)).register(name);
    }

    // 函数可以由具有相同名称和相同数量/类型的输入的另一个功能覆盖。 如果覆盖函数具有不同类型的输出参数,则会导致错误。 本地和基于消息的函数调用都考虑到这些覆盖。
    function kill() {
        if (msg.sender == owner) {
            Config config = Config(0xd5f9d8d94886e70b06e474c3fb14fd43e2f23970);
            NameReg(config.lookup(1)).unregister();
            // 仍然可以调用特定的重写函数。
            mortal.kill();
        }
    }
}


// 如果一个构造函数接受一个参数,则需要在标题中提供(或者在派生契约的构造函数中使用修饰符调用风格)(见下文))。
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;
}

请注意,我们称之为mortal.kill()来“转发”销毁请求。 这样做是有问题的,如以下示例所示:

pragma solidity ^0.4.0;

contract owned {
    function owned() { owner = msg.sender; }
    address owner;
}

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 owned {
    function owned() { owner = msg.sender; }
    address owner;
}

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()(注意,最后的继承序列是 - 从最为导出的合约开始:FinalBase1Base2owned)。 使用super时调用的实际函数在使用它的类的上下文中是未知的,尽管它的类型是已知的。 这对于普通虚拟方法查找是类似的。

基础构造函数的参数

派生合约需要提供基础构造函数所需的所有参数。 这可以通过两种方式完成:

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

一种方法是直接在继承列表(Base(7))。 另一个方法是将一个修饰符作为派生构造函数(Base(_y * _y))的头部的一部分被调用。 如果构造函数参数是一个常量,并定义了合同的行为或者描述了它的行为,那么第一种做法更为方便。 如果基础的构造函数参数取决于派生合同的参数,则必须使用第二种方法。 如果在这个愚蠢的例子中使用了两个地方,那么修饰符样式的参数就是优先级。

多重继承和线性化

允许多重继承的语言必须处理几个问题。 一个是[钻石问题]https://en.wikipedia.org/wiki/Multiple_inheritance#The_diamond_problem)。 Solidity遵循Python的路径,并使用“C3线性化”来强制基类的DAG中的特定顺序。 这导致单调性的理想属性,但不允许一些继承图。 特别地,在指令中给出基类的顺序很重要。 在下面的代码中,Solidity会给出“不可能的继承图的线性化”错误。

// 不会被编译

pragma solidity ^0.4.0;

contract X {}
contract A is X {}
contract C is A, X {}

原因是C请求X覆盖A(通过以此顺序指定A,X),但A本身请求覆盖X,这是无法解决的矛盾。

要记住的一个简单的规则是按照从“最基本像”到“最多派生”的顺序来指定基类。

继承的同名成员的不同类型

当继承导致与同名的函数和修饰符的合同时,它被认为是错误。 此错误也由相同名称的事件和修饰符以及相同名称的函数和事件生成。 作为例外,状态变量getter可以覆盖公共函数。

抽象合约

合约的函数可以缺少一个实现如下面的例子(注意,函数声明报头由终止;):

pragma solidity ^0.4.0;

contract Feline {
    function utterance() returns (bytes32);
}

这种合同无法编译(即使它们包含已实现的功能以及未实现的功能),但可以作为基础合同使用:

pragma solidity ^0.4.0;

contract Feline {
    function utterance() returns (bytes32);
}

contract Cat is Feline {
    function utterance() returns (bytes32) { return "miaow"; }
}

如果合约从抽象合约中继承,并且通过重写实现所有未实现的所有函数,它本身就是抽象的。

接口

接口类似于抽象合同,但它们不能实现任何功能。 还有进一步的限制:

无法继承其他合同或接口。
无法定义构造函数。
无法定义变量。
无法定义结构体。
无法定义枚举。
其中一些限制可能会在未来取消。

接口基本上限于合同ABI可以表示的内容,ABI和接口之间的转换应该是可能的,没有任何信息丢失。

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

pragma solidity ^0.4.11;

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

合约可以继承接口,因为他们将继承其他合约。

库与合约类似,但其目的是在特定地址部署一次,并使用EVMDELEGATECALL(CALLCODE until Homestead)功能重用其代码。 这意味着如果调用库函数,则它们的代码在呼叫契约的上下文中执行,即这指向呼叫合同,特别是可以访问来自呼叫合同的存储。 由于库是孤立的源代码,所以只有明确提供调用契约的状态变量才可以访问(否则就无法命名)。

库可以被看作是使用它们的合约隐含基类合约。 它们不会在继承层次结构中显式显示,但对库函数的调用就像调用显式基础合约的函数(L.f(),如果L是库的名称)。 此外,图书馆的内部功能在所有合同中都可见,就像图书馆是基本合同一样。 当然,对内部函数的调用使用内部调用约定,这意味着所有内部类型都可以传递,内存类型将被引用传递而不被复制。 为了在EVM中实现这一点,内部库函数的代码和从其中调用的所有函数将被拉入呼叫合约,并且将使用常规JUMP调用而不是DELEGATECALL。

以下示例说明了如何使用库(但请确保使用更高级的示例查看以实现集合)。

pragma solidity ^0.4.11;

library Set {
  // 我们定义一个新的结构数据类型,用于将其数据保存在调用合约中。
  struct Data { mapping(uint => bool) flags; }

  // 请注意,第一个参数类型为“存储引用”,因此只有其存储地址而不是其内容作为调用的一部分传递。 这是库函数的特殊功能。 调用第一个参数“self”是惯用的,如果该函数可以看作是该对象的一个方法。
  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) {
        // 库函数可以在没有库的特定实例的情况下调用,因为“实例”将是当前的合约。
        require(Set.insert(knownValues, value));
    }
    // 在这个合约中,如果我们想要的话,我们也可以直接访问`knownValues.flags`。
}

当然,您不需要按照这种方式使用库 - 也可以在不定义struct数据类型的情况下使用它们。 函数也可以在没有任何存储参考参数的情况下工作,并且它们可以具有多个存储参考参数并处于任何位置

对Set.contains,Set.insert和Set.remove的调用都被编译为外部契约/库的调用(DELEGATECALL)。 如果使用库,请注意执行实际的外部函数调用。 msg.sender,msg.value,这将保留在此调用中的值,尽管(在Homestead之前,由于使用CALLCODE,msg.sender和msg.value已更改)。

以下示例显示了如何在库中使用内存类型和内部函数,以便实现自定义类型,而不需要外部函数调用的开销:

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);
    }
}

由于编译器无法知道库将在哪里部署,这些地址必须通过链接器填充到最终的字节码(请参阅使用Commandline编译器了解如何使用命令行编译器进行链接)。 如果地址不作为参数提供给编译器编译后的十六进制代码将包含形式Set____(这里是集库的名称)的占位符。 可以通过使用图书馆契约地址的十六进制编码替换所有40个符号来手动填充地址。

在比较合约库限制:

  • 没有状态变量
  • 不能继承也不能继承
  • 无法接收Ether

(这些可能在稍后的时候被解除。)

using for

指令使用A为B; 可用于将库函数(从库A)附加到任何类型(B)。 这些函数将接收它们被调用的对象作为它们的第一个参数(如Python中的自变量)。

using A for *;的效果; 是从库A的功能附加到任何类型。

在这两种情况下,所有功能,即使那些第一个参数的类型与对象的类型不匹配的函数也被附加。 在调用该函数的时候检查该类型,并执行函数重载分辨率。

使用A为B; 指令对于当前的范围是活跃的,这个范围现在仅限于一个合同,但将在以后被提交到全球范围,因此通过包括一个模块,其数据类型包括库函数可用,而无需添加进一步的代码。

让我们以这种方式从库中重写集合示例:

pragma solidity ^0.4.11;

// 这是与以前一样的代码,只是没有注释
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; // 这是至关重要的变化
    Set.Data knownValues;

    function register(uint value) {
        // 这里,Set.Data类型的所有变量都有相应的成员函数。 以下函数调用与Set.insert(knownValues,value)相同
        require(knownValues.insert(value));
    }
}

也可以这样扩展基本类型:

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函数调用。 这意味着,如果您传递内存或值类型,将执行一个副本,甚至是自变量。 只有当使用存储参考变量时才会执行副本。

你可能感兴趣的:(【Solidity】6. 合约 - 深入理解Solidity)