区块链100讲:Solidity语法的合约/抽象合约/接口/库的定义

image

以太坊智能合约语言Solitidy是一种面向对象的语言,本文清楚合约定义,以及派生的抽象合约,接口,库的定义。html

1

合约定义(Contract)

Solidity 合约相似于面向对象语言中的类。合约中有用于数据持久化的状态变量,和能够修改状态变量的函数。 调用另外一个合约实例的函数时,会执行一个 EVM 函数调用,这个操做会切换执行时的上下文,这样,前一个合约的状态变量就不能访问了。web

1.1 建立合约编程

能够经过以太坊交易“从外部”或从 Solidity 合约内部建立合约。app

一些集成开发环境,例如 Remix, 经过使用一些用户界面元素使建立过程更加流畅。 在以太坊上编程建立合约最好使用 JavaScript API web3.js。 如今,咱们已经有了一个叫作 web3.eth.Contract 的方法可以更容易的建立合约。ide

建立合约时,会执行一次构造函数(与合约同名的函数)。构造函数是可选的。只容许有一个构造函数,这意味着不支持重载。函数

在内部,构造函数参数在合约代码以后经过 ABI 编码 传递,可是若是你使用 web3.js 则没必要关心这个问题。学习

若是一个合约想要建立另外一个合约,那么建立者必须知晓被建立合约的源代码(和二进制代码)。 这意味着不可能循环建立依赖项。区块链

pragma solidity ^0.4.16;

contract OwnedToken {

    // TokenCreator 是以下定义的合约类型.

    // 不建立新合约的话,也能够引用它。

    TokenCreator creator;

    address owner;

    bytes32 name;

    // 这是注册 creator 和设置名称的构造函数。

    function OwnedToken(bytes32 _name) public {

        // 状态变量经过其名称访问,而不是经过例如 this.owner 的方式访问。

        // 这也适用于函数,特别是在构造函数中,你只能像这样(“内部地”)调用它们,

        // 由于合约自己还不存在。

        owner = msg.sender; 

       // 从 `address` 到 `TokenCreator` ,是作显式的类型转换

        // 而且假定调用合约的类型是 TokenCreator,没有真正的方法来检查这一点。

        creator = TokenCreator(msg.sender);

        name = _name;

    }

    function changeName(bytes32 newName) public {

        // 只有 creator (即建立当前合约的合约)可以更更名称 —— 由于合约是隐式转换为地址的,

        // 因此这里的比较是可行的。

        if (msg.sender == address(creator))

            name = newName;

    }

    function transfer(address newOwner) public {

        // 只有当前全部者才能发送 token。

        if (msg.sender != owner) return;

        // 咱们也想询问 creator 是否能够发送。

        // 请注意,这里调用了一个下面定义的合约中的函数。

        // 若是调用失败(好比,因为 gas 不足),会当即中止执行。

        if (creator.isTokenTransferOK(owner, newOwner))

            owner = newOwner;

    }

}

contract TokenCreator {

    function createToken(bytes32 name)

       public

       returns (OwnedToken tokenAddress)

    {

        // 建立一个新的 Token 合约而且返回它的地址。

        // 从 JavaScript 方面来讲,返回类型是简单的 `address` 类型,由于

        // 这是在 ABI 中可用的最接近的类型。

        return new OwnedToken(name);

    }

    function changeName(OwnedToken tokenAddress, bytes32 name)  public {

        // 一样,`tokenAddress` 的外部类型也是 `address` 。

        tokenAddress.changeName(name);

    }

    function isTokenTransferOK(address currentOwner, address newOwner)

        public

        view

        returns (bool ok)

    {

        // 检查一些任意的状况。

        address tokenAddress = msg.sender;

        return (keccak256(newOwner) & 0xff) == (bytes20(tokenAddress) & 0xff);

    }

}

2

抽象合约(Abstract Contract)

合约函数能够缺乏实现,以下例所示(请注意函数声明头由 ; 结尾):ui

pragma solidity ^0.4.0;

contract Feline {

    function utterance() public returns (bytes32);
}

这些合约没法成功编译(即便它们除了未实现的函数还包含其余已经实现了的函数),但他们能够用做基类合约:this

pragma solidity ^0.4.0;

contract Feline {

    function utterance() public returns (bytes32);

}

contract Cat is Feline {

    function utterance() public returns (bytes32) { return "miaow"; }

}

若是合约继承自抽象合约,而且没有经过重写来实现全部未实现的函数,那么它自己就是抽象的。

3

接口(Interface)

接口相似于抽象合约,可是它们不能实现任何函数。还有进一步的限制:

  • 没法继承其余合约或接口。

  • 没法定义构造函数。

  • 没法定义变量。

  • 没法定义结构体

  • 没法定义枚举。 未来可能会解除这里的某些限制。

接口基本上仅限于合约 ABI 能够表示的内容,而且 ABI 和接口之间的转换应该不会丢失任何信息。

接口由它们本身的关键字表示:

pragma solidity ^0.4.11;

interface Token {

    function transfer(address recipient, uint amount) public;
}

4

库(Libary)

库与合约相似,它们只须要在特定的地址部署一次,而且它们的代码能够经过 EVM 的 DELEGATECALL (Homestead 以前使用 CALLCODE 关键字)特性进行重用。 这意味着若是库函数被调用,它的代码在调用合约的上下文中执行,即 this 指向调用合约,特别是能够访问调用合约的存储。 由于每一个库都是一段独立的代码,因此它仅能访问调用合约明确提供的状态变量(不然它就没法经过名字访问这些变量)。 由于咱们假定库是无状态的,因此若是它们不修改状态(也就是说,若是它们是 view 或者 pure 函数), 库函数仅能够经过直接调用来使用(即不使用 DELEGATECALL 关键字), 特别是,除非能规避 Solidity 的类型系统,不然是不可能销毁任何库的。

库能够看做是使用他们的合约的隐式的基类合约。虽然它们在继承关系中不会显式可见,但调用库函数与调用显式的基类合约十分相似 (若是 L 是库的话,可使用 L.f() 调用库函数)。此外,就像库是基类合约同样,对全部使用库的合约,库的 internal 函数都是可见的。 固然,须要使用内部调用约定来调用内部函数,这意味着全部内部类型,内存类型都是经过引用而不是复制来传递。 为了在 EVM 中实现这些,内部库函数的代码和从其中调用的全部函数都在编译阶段被拉取到调用合约中,而后使用一个 JUMP 调用来代替 DELEGATECALL。

下面的示例说明如何使用库(但也请务必看看 using for-https://solidity-cn.readthedocs.io/zh/develop/contracts.html?highlight=view#using-for 有一个实现 set 更好的例子)。

 library Set {

  // 咱们定义了一个新的结构体数据类型,用于在调用合约中保存数据。

  struct Data { mapping(uint => bool) flags; }

  // 注意第一个参数是“storage reference”类型,所以在调用中参数传递的只是它的存储地址而不是内容。

  // 这是库函数的一个特性。若是该函数能够被视为对象的方法,则习惯称第一个参数为 `self` 。

  function insert(Data storage self, uint value)

      public

      returns (bool)

  {

      if (self.flags[value])

          return false; // 已经存在

      self.flags[value] = true;

      return true;

  }

  function remove(Data storage self, uint value)

      public

      returns (bool)

  {

      if (!self.flags[value])

          return false; // 不存在

      self.flags[value] = false;

      return true;

  }

  function contains(Data storage self, uint value)

      public

      view

      returns (bool)

  {

      return self.flags[value];

  }

}

contract C {

    Set.Data knownValues;

    function register(uint value) public {

        // 不须要库的特定实例就能够调用库函数,

        // 由于当前合约就是“instance”。

        require(Set.insert(knownValues, value));

    }

    // 若是咱们愿意,咱们也能够在这个合约中直接访问 knownValues.flags。

}

固然,你没必要按照这种方式去使用库:它们也能够在不定义结构数据类型的状况下使用。 函数也不须要任何存储引用参数,库能够出如今任何位置而且能够有多个存储引用参数。

调用 Set.contains,Set.insert 和 Set.remove 都被编译为外部调用( DELEGATECALL )。 若是使用库,请注意实际执行的是外部函数调用。 msg.sender, msg.value 和 this 在调用中将保留它们的值, (在 Homestead 以前,由于使用了 CALLCODE,改变了 msg.sender 和 msg.value)。

如下示例展现了如何在库中使用内存类型和内部函数来实现自定义类型,而无需支付外部函数调用的开销:

 library BigInt {

    struct bigint {

        uint[] limbs;

    }

    function fromUint(uint x) internal pure returns (bigint r) {

        r.limbs = new uint[](1);

        r.limbs[0] = x;

    }

    function add(bigint _a, bigint _b) internal pure 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) {

            // 太差了,咱们须要增长一个 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 pure returns (uint) {

        return _limb < _a.limbs.length ? _a.limbs[_limb] : 0;

    }

    function max(uint a, uint b) private pure returns (uint) {

        return a > b ? a : b;

    }

}

contract C {

    using BigInt for BigInt.bigint;

    function f() public pure {

        var x = BigInt.fromUint(7);

        var y = BigInt.fromUint(uint(-1));

        var z = x.add(y);

    }

}

因为编译器没法知道库的部署位置,咱们须要经过连接器将这些地址填入最终的字节码中 (请参阅 使用命令行编译器-https://solidity-cn.readthedocs.io/zh/develop/using-the-compiler.html#commandline-compiler 以了解如何使用命令行编译器来连接字节码)。 若是这些地址没有做为参数传递给编译器,编译后的十六进制代码将包含 Set____ 形式的占位符(其中 Set 是库的名称)。 能够手动填写地址来将那 40 个字符替换为库合约地址的十六进制编码。

与合约相比,库的限制:

  • 没有状态变量

  • 不可以继承或被继承

  • 不能接收以太币

(未来有可能会解除这些限制)

4.1 库的调用保护

若是库的代码是经过 CALL 来执行,而不是 DELEGATECALL 或者 CALLCODE 那么执行的结果会被回退, 除非是对 view 或者 pure 函数的调用。

EVM 没有为合约提供检测是否使用 CALL 的直接方式,可是合约可使用 ADDRESS 操做码找出正在运行的“位置”。 生成的代码经过比较这个地址和构造时的地址来肯定调用模式。

更具体地说,库的运行时代码老是从一个 push 指令开始,它在编译时是 20 字节的零。当部署代码运行时,这个常数 被内存中的当前地址替换,修改后的代码存储在合约中。在运行时,这致使部署时地址是第一个被 push 到堆栈上的常数, 对于任何 non-view 和 non-pure 函数,调度器代码都将对比当前地址与这个常数是否一致。

4.2 Using For

指令 using A for B; 可用于附加库函数(从库 A)到任何类型(B)。 这些函数将接收到调用它们的对象做为它们的第一个参数(像 Python 的 self 变量)。

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

在这两种状况下,全部函数都会被附加一个参数,即便它们的第一个参数类型与对象的类型不匹配。 函数调用和重载解析时才会作类型检查。

using A for B; 指令仅在当前做用域有效,目前仅限于在当前合约中,后续可能提高到全局范围。 经过引入一个模块,不须要再添加代码就可使用包括库函数在内的数据类型。

让咱们用这种方式将 库 中的 set 例子重写:

// 这是和以前同样的代码,只是没有注释。

library Set {

  struct Data { mapping(uint => bool) flags; }

  function insert(Data storage self, uint value)

      public

      returns (bool)

  {

      if (self.flags[value])

        return false; // 已经存在

      self.flags[value] = true;

      return true;

  }

  function remove(Data storage self, uint value)

      public

      returns (bool)

  {

      if (!self.flags[value])

          return false; // 不存在

      self.flags[value] = false;

      return true;

  }

  function contains(Data storage self, uint value)

      public

      view

      returns (bool)

  {

      return self.flags[value];

  }

}

contract C {

    using Set for Set.Data; // 这里是关键的修改

    Set.Data knownValues;

    function register(uint value) public {

        // Here, all variables of type Set.Data have

        // corresponding member functions.

        // The following function call is identical to

        // `Set.insert(knownValues, value)`

        // 这里, Set.Data 类型的全部变量都有与之相对应的成员函数。

        // 下面的函数调用和 `Set.insert(knownValues, value)` 的效果彻底相同。

        require(knownValues.insert(value));

    }
}

也能够像这样扩展基本类型:

library Search {

    function indexOf(uint[] storage self, uint value)

        public

        view

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

        data.push(value);

    }

    function replace(uint _old, uint _new) public {

        // 执行库函数调用

        uint index = data.indexOf(_old);

        if (index == uint(-1)) 

           data.push(_new);

        else

            data[index] = _new;

    }

}

注意,全部库调用都是实际的 EVM 函数调用。这意味着若是传递内存或值类型,都将产生一个副本,即便是 self 变量。 使用存储引用变量是惟一不会发生拷贝的状况。

本文做者:HiBlock区块链社区技术布道者辉哥

原文发布于简书

区块链马拉松|Blockathon(2018)上海站开放报名

如下是咱们的社区介绍,欢迎各类合做、交流、学习:)

image

相关文章
相关标签/搜索