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

创建合约

可以通过以太坊交易“从外部”或从 Solidity 合约内部创建合约。
一些集成开发环境,例如 Remix, 通过使用一些UI用户界面使创建合约的过程更加顺畅。 在以太坊上通过编程创建合约使用 JavaScript API web3.js。
创建合约时, 合约的 构造函数 (一个用关键字 constructor 声明的函数)会执行一次。 构造函数是可选的。只允许有一个构造函数,这意味着不支持重载。
如果一个合约想要创建另一个合约,那么创建者必须知晓被创建合约的源代码(和二进制代码)。 这意味着不可能循环创建依赖项。


contract OwnedToken {
    // TokenCreator 是如下定义的合约类型.
    // 不创建新合约的话,也可以引用它。
    TokenCreator creator;
    address owner;
    bytes32 name;

    // 这是注册 creator 和设置名称的构造函数。
    constructor(bytes32 _name) {
        // 状态变量通过其名称访问,而不是通过例如 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);
    }
}

可见性和getter函数

函数和状态变量有四种可见性类型。 函数可以指定为 external ,public ,internal 或者 private。 对于状态变量,不能设置为 external ,默认是 internal 。
external
外部函数作为合约接口的一部分,意味着我们可以从其他合约和交易中调用。 一个外部函数 f 不能从内部调用(即 f 不起作用,但 this.f() 可以)。 当收到大量数据的时候,外部函数有时候会更有效率,因为数据不会从calldata复制到内存.
public
public 函数是合约接口的一部分,可以在内部或通过消息调用。对于 public 状态变量, 会自动生成一个 getter 函数(见下面)。
internal
这些函数和状态变量只能是内部访问(即从当前合约内部或从它派生的合约访问),不使用 this 调用。
private
private 函数和状态变量仅在当前定义它们的合约中使用,并且不能被派生合约使用。

Getter函数

编译器自动为所有 public 状态变量创建 getter 函数。


contract C {
    uint public data = 42;
}

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

getter 函数具有外部(external)可见性。如果在内部访问 getter(即没有 this. ),它被认为一个状态变量。 如果使用外部访问(即用 this. ),它被认作为一个函数。


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

如果你有一个数组类型的 public 状态变量,那么你只能通过生成的 getter 函数访问数组的单个元素。这个机制以避免返回整个数组时的高成本gas。 可以使用如 data(0) 用于指定参数要返回的单个元素。 如果要在一次调用中返回整个数组,则需要写一个函数,例如:


contract arrayExample {
  // public state variable
  uint[] public myArray;

  // 指定生成的Getter 函数
  /*
  function myArray(uint i) public view returns (uint) {
      return myArray[i];
  }
  */

  // 返回整个数组
  function getArray() public view returns (uint[] memory) {
      return myArray;
  }
}
现在可以使用 getArray() 获得整个数组,而 myArray(i) 是返回单个元素。

函数修改器

使用 修改器modifier 可以轻松改变函数的行为。 例如,它们可以在执行函数之前自动检查某个条件。 修改器modifier 是合约的可继承属性,并可能被派生合约覆盖 , 但前提是它们被标记为 virtual。

pragma solidity >0.7.0 <0.9.0;

contract owned {
    constructor() { owner = payable(msg.sender); }

    address owner;

    // 这个合约只定义一个修改器,但并未使用: 它将会在派生合约中用到。
    // 修改器所修饰的函数体会被插入到特殊符号 _; 的位置。
    // 这意味着如果是 owner 调用这个函数,则函数会被执行,否则会抛出异常。
    modifier onlyOwner {
        require(
            msg.sender == owner,
            "Only owner can call this function."
        );
        _;
    }
    contract Mutex {
    bool locked;
    modifier noReentrancy() {
        require(
            !locked,
            "Reentrant call."
        );
        locked = true;
        _;
        locked = false;
    }

    // 这个函数受互斥量保护,这意味着 `msg.sender.call` 中的重入调用不能再次调用  `f`。
    // `return 7` 语句指定返回值为 7,但修改器中的语句 `locked = false` 仍会执行。
    function f() public noReentrancy returns (uint) {
        (bool success,) = msg.sender.call("");
        require(success);
        return 7;
    }
}

如果你想访问定义在合约 C 的 修改器modifier m , 可以使用 C.m 去引用它,而不需要使用虚拟表查找。
只能使用在当前合约或在基类合约中定义的 修改器modifier , 修改器modifier 也可以定义在库里面,但是他们被限定在库函数使用。
如果同一个函数有多个 修改器modifier,它们之间以空格隔开,修改器modifier 会依次检查执行。

Constant 和 Immutable 状态变量

状态变量声明为 constant (常量)或者 immutable (不可变量),在这两种情况下,合约一旦部署之后,变量将不在修改。对于 constant 常量, 他的值在编译器确定,而对于 immutable, 它的值在部署时确定。
与常规状态变量相比,常量和不可变量的gas成本要低得多。 对于常量,赋值给它的表达式将复制到所有访问该常量的位置,并且每次都会对其进行重新求值。 这样可以进行本地优化。
不可变变量在构造时进行一次求值,并将其值复制到代码中访问它们的所有位置。 对于这些值,将保留32个字节,即使它们适合较少的字节也是如此。 因此,常量有时可能比不可变量更便宜。

Constant

如果状态变量声明为 constant (常量)。在这种情况下,只能使用那些在编译时有确定值的表达式来给它们赋值。 任何通过访问 storage,区块链数据(例如 block.timestamp, address(this).balance 或者 block.number)或执行数据( msg.value 或 gasleft() ) 或对外部合约的调用来给它们赋值都是不允许的。

Immutable

声明为不可变量(immutable)的变量的限制要比声明为常量(constant) 的变量的限制少:可以在合约的构造函数中或声明时为不可变的变量分配任意值。 不可变量在构造期间无法读取其值,并且只能赋值一次。

函数

可以在合约内部和外部定义函数。
合约之外的函数(也称为“自由函数”)始终具有隐式的 internal 可见性。 它们的代码包含在所有调用它们合约中,类似于内部库函数。

pragma solidity >0.7.0 <0.9.0;
function sum(uint[] memory _arr) pure returns (uint s) {
    for (uint i = 0; i < _arr.length; i++)
        s += _arr[i];
}
contract ArrayExample {
    bool found;
    function f(uint[] memory _arr) public {
        // This calls the free function internally.
        // The compiler will add its code to the contract.
        uint s = sum(_arr);
        require(s >= 10);
        found = true;
    }
}

函数参数及返回值

与 Javascript 一样,函数可能需要参数作为输入; 而与 Javascript 和 C 不同的是,它们可能返回任意数量的参数作为输出。

函数参数(输入参数)

函数参数的声明方式与变量相同。不过未使用的参数可以省略参数名。


contract Simple {
    uint sum;
    function taker(uint _a, uint _b) public {
        sum = _a + _b;
    }
}

返回变量

函数返回变量的声明方式在关键词 returns 之后,与参数的声明方式相同。


contract Simple {
    function arithmetic(uint _a, uint _b)
        public
        pure
        returns (uint o_sum, uint o_product)
    {
        o_sum = _a + _b;
        o_product = _a * _b;
    }
}

返回多个值

当函数需要使用多个值,可以用语句 return (v0, v1, ..., vn) 。 参数的数量需要和声明时候一致。

View 视图函数

可以将函数声明为 view 类型,这种情况下要保证不修改状态。view常被用来作为只读函数,不消耗GAS。

Pure 纯函数

函数可以声明为 pure ,在这种情况下,承诺不读取也不修改状态。pure常被用来作为常数计算。不消耗GAS。

receive 接收以太函数

个合约最多有一个 receive 函数, 声明函数为: receive() external payable { ... }
不需要 function 关键字,也没有参数和返回值并且必须是 external 可见性和 payable 修饰. 它可以是 virtual 的,可以被重载也可以有 修改器modifier 。在对合约没有任何附加数据调用(通常是对合约转账)是会执行 receive 函数. 例如 通过 .send() or .transfer() 如果 receive 函数不存在, 但是有payable 的 fallback 回退函数 那么在进行纯以太转账时,fallback 函数会调用.
如果两个函数都没有,这个合约就没法通过常规的转账交易接收以太(会抛出异常).
更糟的是,receive 函数可能只有 2300 gas 可以使用(如,当使用 send 或 transfer 时), 除了基础的日志输出之外,进行其他操作的余地很小。下面的操作消耗会操作 2300 gas :
1.写入存储
2.创建合约
3.调用消耗大量 gas 的外部函数
4.发送以太币

fallback

合约可以最多有一个回退函数。函数声明为: fallback () external [payable]没有function 关键字。 必须是 external 可见性,它可以是 virtual 的,可以被重载也可以有 修改器modifier 。
如果在一个对合约调用中,没有其他函数与给定的函数标识符匹配fallback会被调用. 或者在没有 receive 函数 时,而没有提供附加数据对合约调用,那么fallback 函数会被执行。
fallback 函数始终会接收数据,但为了同时接收以太时,必须标记为payable 。

contract Test {
    // 发送到这个合约的所有消息都会调用此函数(因为该合约没有其它函数)。
    // 向这个合约发送以太币会导致异常,因为 fallback 函数没有 `payable` 修饰符
    fallback() external { x = 1; }
    uint x;
}

// 这个合约会保留所有发送给它的以太币,没有办法返还。
contract TestPayable {
    // 除了纯转账外,所有的调用都会调用这个函数.
    // (因为除了 receive 函数外,没有其他的函数).
    // 任何对合约非空calldata 调用会执行回退函数(即使是调用函数附加以太).
    fallback() external payable { x = 1; y = msg.value; }

    // 纯转账调用这个函数,例如对每个空empty calldata的调用
    receive() external payable { x = 2; y = msg.value; }
    uint x;
    uint y;
}

contract Caller {
    function callTest(Test test) public returns (bool) {
        (bool success,) = address(test).call(abi.encodeWithSignature("nonExistingFunction()"));
        require(success);
        //  test.x 结果变成 == 1。

        // address(test) 不允许直接调用 ``send`` ,  因为 ``test`` 没有 payable 回退函数
        //  转化为 ``address payable`` 类型 , 然后才可以调用 ``send``
        address payable testPayable = payable(address(test));


        // 以下将不会编译,但如果有人向该合约发送以太币,交易将失败并拒绝以太币。
        // test.send(2 ether);
    }

    function callTestPayable(TestPayable test) public returns (bool) {
        (bool success,) = address(test).call(abi.encodeWithSignature("nonExistingFunction()"));
        require(success);
        // 结果 test.x 为 1  test.y 为 0.
        (success,) = address(test).call{value: 1}(abi.encodeWithSignature("nonExistingFunction()"));
        require(success);
        // 结果test.x 为1 而 test.y 为 1.

        // 发送以太币, TestPayable 的 receive 函数被调用.
        require(payable(test).send(2 ether));
        // 结果 test.x 为 2 而 test.y 为 2 ether.

        return true;
    }

}

函数重载

合约可以具有多个不同参数的同名函数,称为“重载”(overloading),这也适用于继承函数。以下示例展示了合约 A 中的重载函数 f。


contract A {
    function f(uint _in) public pure returns (uint out) {
        out = _in;
    }

    function f(uint _in, bool _really) public pure returns (uint out) {
        if (_really)
            out = _in;
    }
}

重载函数也存在于外部接口中。如果两个外部可见函数仅区别于 Solidity 内的类型而不是它们的外部类型则会导致错误。以下两个 f 函数重载都接受了 ABI 的地址类型,虽然它们在 Solidity 中被认为是不同的。

pragma solidity >=0.4.16 <0.9.0;

contract A {
    function f(B _in) public pure returns (B out) {
        out = _in;
    }

    function f(address _in) public pure returns (address out) {
        out = _in;
    }
}

contract B {
}

重载解析和参数匹配

通过将当前范围内的函数声明与函数调用中提供的参数相匹配,可以选择重载函数。 如果所有参数都可以隐式地转换为预期类型,则选择函数作为重载候选项。如果一个候选都没有,解析失败。


contract A {
    function f(uint8 _in) public pure returns (uint8 out) {
        out = _in;
    }

    function f(uint256 _in) public pure returns (uint256 out) {
        out = _in;
    }
}

事件 Events

声明事件,再 emit 事件,类似与 nodejs,与 web3 交互类似与 websocket


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

    function deposit(bytes32 _id) public payable {
        // 事件使用 emit 触发事件。
        // 我们可以过滤对 `Deposit` 的调用,从而用 Javascript API 来查明对这个函数的任何调用(甚至是深度嵌套调用)。
        emit Deposit(msg.sender, _id, msg.value);
    }
}

使用 JavaScript API 调用事件的用法如下:

var ClientReceipt = web3.eth.contract(abi);
var clientReceipt = ClientReceipt.at("0x1234...xlb67" /* 地址 */);

var event = clientReceipt.Deposit();

// 监听变化
event.watch(function(error, result) {
    // 结果包含 非索引参数 以及 主题 topic
    if (!error)
        console.log(result);
});

// 或者通过传入回调函数,立即开始听监
var event = clientReceipt.Deposit(function(error, result) {
    if (!error)
        console.log(result);
});

继承

Solidity 支持多重继承包括多态。
Solidity 的继承系统与Python的继承系统非常相似,特别是多重继承方面,另外一样可以使用super.f();来调用父类方法。
下面的例子进行了详细的说明。

   pragma solidity >=0.7.0 <0.9.0;

   contract Owned {
       constructor() public { owner = payable(msg.sender); }
       address payable owner;
   }

   // 使用 is 从另一个合约派生。派生合约可以访问所有非私有成员,包括内部(internal)函数和状态变量,
   // 但无法通过 this 来外部访问。
   contract Destructible is Owned {

    // 关键字`virtual`表示该函数可以在派生类中“overriding”。

       function destroy() virtual public {
           if (msg.sender == owner) selfdestruct(owner);
       }
   }

   // 这些抽象合约仅用于给编译器提供接口。
   // 注意函数没有函数体。
   // 如果一个合约没有实现所有函数,则只能用作接口。
   abstract contract Config {
       function lookup(uint id) public virtual returns (address adr);
   }

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

   // 可以多重继承。请注意,owned 也是 Destructible 的基类,
   // 但只有一个 owned 实例(就像 C++ 中的虚拟继承)。
   contract Named is Owned, Destructible {
       constructor(bytes32 name) {
           Config config = Config(0xD5f9D8D94886E70b06E474c3fB14Fd43E2f23970);
           NameReg(config.lookup(1)).register(name);
       }

       // 函数可以被另一个具有相同名称和相同数量/类型输入的函数重载。
       // 如果重载函数有不同类型的输出参数,会导致错误。
       // 本地和基于消息的函数调用都会考虑这些重载。

//如果要覆盖函数,则需要使用 `override` 关键字。 如果您想再次覆盖此函数,则需要再次指定`virtual`关键字。

       function destroy() public virtual override {
           if (msg.sender == owner) {
               Config config = Config(0xD5f9D8D94886E70b06E474c3fB14Fd43E2f23970);
               NameReg(config.lookup(1)).unregister();
               // 仍然可以调用特定的重载函数。
               Destructible.destroy();
           }
       }
   }

   // 如果构造函数接受参数,
   // 则需要在声明(合约的构造函数)时提供,
   // 或在派生合约的构造函数位置以修改器调用风格提供(见下文)。
   contract PriceFeed is Owned, Destructible, Named("GoldFeed") {
       function updateInfo(uint newInfo) public {
           if (msg.sender == owner) info = newInfo;
       }

       // Here, we only specify `override` and not `virtual`.
       // This means that contracts deriving from `PriceFeed`
       // cannot change the behaviour of `destroy` anymore.
       function destroy() public override(Destructible, Named) { Named.destroy(); }

       function get() public view returns(uint r) { return info; }

       uint info;
   }

函数重写(Overriding)

父合约标记为 virtual 函数可以在继承合约里重写(overridden)以更改他们的行为。重写的函数需要使用关键字 override 修饰。
重写函数只能将覆盖函数的可见性从 external 更改为 public 。
可变性可以按照以下顺序更改为更严格的一种: nonpayable 可以被 view 和 pure 覆盖。 view 可以被 pure 覆盖。 payable 是一个例外,不能更改为任何其他可变性。

pragma solidity >=0.7.0 <0.9.0;

contract Base
{
    function foo() virtual external view {}
}

contract Middle is Base {}

contract Inherited is Middle
{
    function foo() override public pure {}
}

对于多重继承,如果有多个父合约有相同定义的函数, override 关键字后必须指定所有父合约名。


contract Base1
{
    function foo() virtual public {}
}

contract Base2
{
    function foo() virtual public {}
}

contract Inherited is Base1, Base2
{
    // 继承自两个基类合约定义的foo(), 必须显示的指定 override
    function foo() public override(Base1, Base2) {}
}

不过如果(重写的)函数继承自一个公共的父合约, override 是可以不用显示指定的。如果函数没有标记为 virtual , 那么派生合约将不能更改函数的行为(即不能重写)。
注意:private 的函数是不可以标记为 virtual 的。除接口之外(因为接口会自动作为 virtual ),没有实现的函数必须标记为 virtual。

如果getter 函数的参数和返回值都和外部函数一致时,外部(external)函数是可以被 public 的状态变量被重写的,例如:


contract A
{
    function f() external view virtual returns(uint) { return 5; }
}

contract B is A
{
    uint public override f;
}

注意:尽管public 的状态变量可以重写外部函数,但是public 的状态变量不能被重写。

修改器重写

修改器重写也可以被重写,工作方式和 :ref:函数重写 <function-overriding>_ 类似。 需要被重写的修改器也需要使用 virtual 修饰,override 则同样修饰重载,例如:


contract Base
{
    modifier foo() virtual {_;}
}

contract Inherited is Base
{
    modifier foo() override {_;}
}

如果是多重继承,所有直接父合约必须显示指定override, 例如:


contract Base1
{
    modifier foo() virtual {_;}
}

contract Base2
{
    modifier foo() virtual {_;}
}

contract Inherited is Base1, Base2
{
    modifier foo() override(Base1, Base2) {_;}
}

构造函数

构造函数是使用 constructor 关键字声明的一个可选函数, 它在创建合约时执行, 可以在其中运行合约初始化代码。
如果没有构造函数, 合约将假定采用默认构造函数, 它等效于 constructor() {}

pragma solidity >0.6.99 <0.8.0;

abstract contract A {
    uint public a;

    constructor(uint _a) {
        a = _a;
    }
}

contract B is A(1) {
    constructor() {}
}

基类构造函数的参数

所有基类合约的构造函数将在下面解释的线性化规则被调用。如果基构造函数有参数, 派生合约需要指定所有参数。这可以通过两种方式来实现。
一种方法直接在继承列表中调用基类构造函数(is Base(7))。 另一种方法是像 修改器modifier 使用方法一样, 作为派生合约构造函数定义头的一部分,(Base(_y * _y))。 如果构造函数参数是常量并且定义或描述了合约的行为,使用第一种方法比较方便。 如果基类构造函数的参数依赖于派生合约,那么必须使用第二种方法。

pragma solidity >0.6.99 <0.8.0;

contract Base {
    uint x;
    constructor(uint _x) { x = _x; }
}

// 直接在继承列表中指定参数
contract Derived1 is Base(7) {
    constructor() {}
}

// 或通过派生的构造函数中用 修饰符 "modifier"
contract Derived2 is Base {
    constructor(uint _y) Base(_y * _y) {}
}

多重继承与线性化

简单来说继承层次结构中有多个构造函数时,继承线性化特别重要。 构造函数将始终以线性化顺序执行,无论在继承合约的构造函数中提供其参数的顺序如何。

pragma solidity >=0.4.0 <0.8.0;

contract X {}
contract A is X {}
// 编译出错
contract C is A, X {}
pragma solidity >0.6.99 <0.8.0;

contract Base1 {
    constructor() {}
}

contract Base2 {
    constructor() {}
}

//  构造函数以以下顺序执行:
//  1 - Base1
//  2 - Base2
//  3 - Derived1
contract Derived1 is Base1, Base2 {
    constructor() Base1() Base2() {}
}

// 构造函数以以下顺序执行:
//  1 - Base2
//  2 - Base1
//  3 - Derived2
contract Derived2 is Base2, Base1 {
    constructor() Base2() Base1() {}
}

// 构造函数仍然以以下顺序执行:
//  1 - Base2
//  2 - Base1
//  3 - Derived3
contract Derived3 is Base2, Base1 {
    constructor() Base1() Base2() {}
}

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

当继承时合约出现了一下相同名字会被认为是一个错误:
1.函数和 修改器modifier 同名
2.函数和事件同名
3.事件和 修改器modifier 同名
有一种例外情况,状态变量的 getter 函数可以覆盖 external 函数。

抽象合约

如果未实现合约中的至少一个函数,则需要将合约标记为 abstract。 即使实现了所有功能,合同也可能被标记为abstract。这样的抽象合约不能直接实例化。 如果抽象合约本身确实都有实现所有定义的函数,也是正确的。 下例显示了抽象合约作为基类的用法:


abstract contract Feline {
  function utterance() public pure returns (bytes32);
}

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

如果合约继承自抽象合约,并且没有通过重写来实现所有未实现的函数, 它依然需要标记为抽象 abstract 合约。
注意:抽象合约不能用一个无实现的函数重写一个实现了的虚函数。

接口

接口类似于抽象合约,但是它们不能实现任何函数。还有进一步的限制
1.法继承其他合约,不过可以继承其他接口。
2.所有的函数都需要是 external
3.无法定义构造函数。
4.无法定义状态变量。
接口基本上基本上仅限于合约 ABI 可以表示的内容,并且 ABI 和接口之间的转换应该不会丢失任何信息。


interface Token {
    enum TokenType { Fungible, NonFungible }
    struct Coin { string obverse; string reverse; }
    function transfer(address recipient, uint amount) external;
}

接口可以继承其他的接口,遵循同样继承规则。


interface ParentA {
    function test() external returns (uint256);
}

interface ParentB {
    function test() external returns (uint256);
}

interface SubInterface is ParentA, ParentB {
    // 必须重新定义 test 函数,以表示兼容父合约含义
    function test() external override(ParentA, ParentB) returns (uint256);
}

库与合约类似,它们只需要在特定的地址部署一次,并且它们的代码可以通过 EVM 的 DELEGATECALL (Homestead 之前使用 CALLCODE 关键字)特性进行重用。
下面的示例说明如何使用库:


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

library Set {

  // 注意第一个参数是“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 {
    Data knownValues;

    function register(uint value) public {
        // 不需要库的特定实例就可以调用库函数,
        // 因为当前合约就是“instance”。
        require(Set.insert(knownValues, value));
    }
    // 如果我们愿意,我们也可以在这个合约中直接访问 knownValues.flags。
}

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


struct bigint {
    uint[] limbs;
}

library BigInt {

    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 == type(uint).max && 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;

    function f() public pure {
        bigint memory x = BigInt.fromUint(7);
        bigint memory y = BigInt.fromUint(type(uint).max);
        bigint memory z = x.add(y);
        assert(z.limb(1) > 0);
    }
}

与合约相比,库的限制:
1.没有状态变量
2.不能够继承或被继承
3.不能接收以太币
4.不可以被销毁

库的函数签名与选择器

与合约 ABI 相似,选择器由签名的Keccak256哈希的前四个字节组成。可以使用 .selector 成员从Solidity中获取其值,如下所示:


library L {
    function f(uint256) external {}
}

contract C {
    function g() public pure returns (bytes4) {
        return L.f.selector;
    }
}

库的调用保护

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

Using For

在当前的合约上下里, 指令 using A for B; 可用于附加库函数(从库 A)到任何类型(B)。 这些函数将接收到调用它们的对象作为它们的第一个参数(像 Python 的 self 变量)。
using A for *; 的效果是,库 A 中的函数被附加在任意的类型上。


// 这是和之前一样的代码,只是没有注释。
struct Data { mapping(uint => bool) flags; }

library Set {

  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 Data; // 这里是关键的修改
    Data knownValues;

    function register(uint value) public {
        // Here, all variables of type Data have
        // corresponding member functions.
        // The following function call is identical to
        // `Set.insert(knownValues, value)`
        // 这里, Data 类型的所有变量都有与之相对应的成员函数。
        // 下面的函数调用和 `Set.insert(knownValues, value)` 的效果完全相同。
        require(knownValues.insert(value));
    }
}