第四章:以太坊智能合约 - 合约


以太坊智能合约 - 合约

合约概述

Solidity 中合约和面向对象语言中的类差不多。合约包含状态变量(状态变量的数据保存在链上的区块中)及函数。 调用另一个合约实例的函数时,会执行一个 EVM 函数调用 , 这个操作会切换执行时的上下文 ,这时上一个合约的状态变量就无法访问了 。

创建合约

合约可以通过发起交易(通过 Web3 发起)或是 Solidity 创建。 在以太坊上动态地创建合约可以使用 JavaScript API Web3.js 的 web3.eth.Contract 来创建合约。

使用 new 创建合约
合约内可以通过 new 关键字来创建一个新合约。

pragma solidity ^0.4.0;
contract D {
	uint x;
	function D(uint a) public payable {
		x = a;
	}
}

contract C {
	D d = new D(4);
	function createD(uint arg) public {
		D newD = new D(arg);
	}
	function createAndEndowD(uint arg,uint amount) public payable {
		D newD = (new D).value(amount)(arg);
	}
}

可以在创建的合约中发送 Ether,但不能限制 gas。 如果创建发生 out-of-stack 或无足够的余额, 则会抛出一个异常。

pragma solidity ^0.4.24;

contract OwnedToken {
    TokenCreator creator;
    address owner;
    bytes32 name;
    
    constructor(bytes32 _name) public {
        owner = msg.sender;
        creator = TokenCreator(msg.sender);
        name = _name;
    }
    function changeName(bytes32 newName) public {
        if (msg.sender == address(creator))
            name = newName;
    }
    function getCreator() returns (address) {
        return creator;
    }
}

contract TokenCreator {
    function createToken(bytes32 name) public returns (OwnedToken tokenAddress) {
        return new OwnedToken(name);        
    }
    function changeName(OwnedToken tokenAddress, bytes32 name) public{
        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);
    }
}

Solidity 有两种函数调用方式,一种 是内部调用,不会创建 EVM 调用(也叫作消息调用),另一种是外部调用,会创建 EVM 调用。

public - (任意访问,作为合约接口)可以通过内部调用或通过消息调用。对公共状态变量而言,会有的自动访问限制符的函数生成。
private - (仅当前合约内)私有函数和状态变量仅仅在定义该合约中可见, 在派生的合约中不可见。
internal - (仅当前合约及所继承的合约)这些函数和状态变量只能内部访问(即在当前合约或由它派生的合约),而不使用(关键字)this 。
external - (仅外部访问,也是合约接口)它们可以从其他合约调用, 也可以通过事务调用。外部函数f不能被内部调用(在内部也只能用外部访问方式访问,即 f()不执行,但this.f()执行)。

在下面的例子中, D 可以调用 c.getData()来访问 data 的值,但不能调用 f。 合约 E 继承自 C,所以它可以访问 compute 函数。

pragma solidity ^0.4.24;

contract C {
    uint private data;
    
    function f(uint a) private returns (uint) {
        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); // private不可访问
        c.setData(3);
        uint local = c.getData();
        // local = c.compute(3,5); // 内部函数调用
    }
}
// E继承C
contract E is C{
    function g() {
        C c = new C();
        uint val = compute(3,5);
    }
}

访问函数(Getter Function)

编译器会自动为所有的 public 的状态变量生成访问函数。

pragma solidity ^0.4.0;
contract C {
	uint public data = 42;
}
contract Caller {
	C c = new C();
	function f() public {
		uint local = c.data();
	}
}

访问函数有外部可见性。 如果是内部访问的,则可以直接访问变量(不用this)。但如果使用外部方式来访问 (通过 this. ),则必须通过函数的方式来调用 。

pragma solidity ^0.4.24;

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) public returns (uint a,bytes3 b) {
	a = data[arg1][arg2].a;
	b = data[arg1][arg2].b;
}

函数修改器(Function Modifier)

熟悉Python 的读者会发现函数修改器的作用和 Python 的装饰器很相似。

pragma solidity ^0.4.24;

contract owned {
    function owned() public{
        owner = msg.sender;
    }
    address owner;
    // 修改器
    // 定义了一个函数修改器,可被继承 
    // 修改时, 函数体被插入到 ”_; "处 
    // 不符合条件时,将抛出异常

    modifier onlyOwner {
        require(msg.sender == owner);
        _;
    }
}
contract mortal is owned {
    function close() public onlyOwner {
        selfdestruct(owner); // 用于销毁合约。所以只需要暴露出自毁接口即可:
    }
}

contract priced {
    modifier costs (uint price) {
        if (msg.value >= price) {
            _;
        }
    }
}

contract Register is priced, owned {
    mapping (address => bool) registerAddresses;
    uint price;
    
    function Register (uint initialPrice) public {
        price = initialPrice;
    }
    
    function register() public payable costs(price) {
        registerAddresses[msg.sender] = true;
    }
    
    function changePrice(uint _price) public onlyOwner {
        price = _price;
    }
    
    function getPrice() public returns (uint){
        return price;
    }
    
    function getMapping(address addr) returns (bool){
        return registerAddresses[addr];
    }
}

上面的 onlyOwner 就是一个函数修改器。当用这个修改器修饰一个函数时, 函数必须满足 onlyOwner 的条件才能运行。 这里的条件是: 函数必须是合约创 建的才能调用,否则抛出异常。

多个函数修改器

如果同一个函数有多个修改器,并且他们之间以空格隔开,那么修改器会 依次检查执行。
在修改器中或函数内的显式的 return语句, 仅仅跳出当前的修改器或函数。 返回的变量会被赋值,但执行流会在前一个修改器后面定义的"_"后继续执行,

pragma solidity ^0.4.24;

contract Mutex {
    bool locked;
    modifier noReentrancy() {
        require(!locked);
        locked = true;
        _;
        locked = false;
    }
    // 防止递归调用 
    //return 7 之后, locked = false 依然会执行 
    function f() public noReentrancy returns (uint) {
        require(msg.sender.call());
        return 7;
    }
}

理解修改器的执行次序

pragma solidity ^0.4.24;

contract modifysample {
    uint a = 10;
    modifier mf1 (uint b) {
        uint c = b;
        _;
        c = a;
        a = 11;
    }
    modifier mf2() {
        uint c = a;
        _;
    }
    modifier mf3() {
        a = 12;
        return ;
        _;
        a = 13;
    }
    function test1() mf1(a) mf2 mf3 public {
        a = 1;
    }
    
    function test2() public constant returns (uint) {
        return a;
    }
}

结果是11。
顺序表为:
在这里插入图片描述

状态常量

状态常量可以被定义为 constant。不过表达式有一些限制。

  1. 不允许访问 storage。
  2. 不允许访问 区块链数据,如 now、 this.balance、 block.number。
  3. 不允许访问合约执行中的数据,如 msg.value 或 gasleft()。
  4. 不允许向外部合约发起调用 。
    但内置的函数 keccak256、keccak256、ripemd160、ecrecover、addmod、mulmod 可以允许调用,即使它们调用的是外部合约。 编译器并不会为常量在 storage 上预留空间, 每个使用的常量都会被对应的 常量表达式所替换。

视图函数(View Function)

函数可以声明为 view,表示它不能修改状态。下面几种情况被认为是修改了状态。

  1. 写状态变量。
  2. 触发事件。
  3. 创建其他的合约。
  4. call 调用附加了以太币 。
  5. 调用了任何没有 view 或 pure 修饰的函数。
  6. 使用了低级别的调用( low-level call)。
  7. 使用 了包含特定操作符的内联汇编。
pragma solidity ^0.4.16;
contract C {
	function f(uint a, uint b) public view returns (uint) {
		return a * (b + 42) + now;
	}
}

有几个地方需要注意一下:

  1. 声明为 view 和声明为 constant 是等价的, constant 是 view 的别名 。 constant 计划在 Solidity 0.5.0 版本之后被弃用 。
  2. 访问函数都被标记为 view。

纯函数(Pure Function)

函数可以声明为 view, 表示它既不能读取状态,也不能修改状态。

  1. 读状态变量。
  2. 访问了 this.balance 或
    .balance。
  3. 访问了 block, tx, msg 的成员 Cmsg.sig 和 msg.data 除外)。
  4. 调用了任何没有 pure 修饰的函数。
  5. 使用了包含特定操作符的内联汇编。
pragma solidity ^0.4.16;
contract C {
	function f(uint a, uint b) public view returns (uint) {
		return a * (b + 42);
	}
}

回退函数(Fallback Function)

一个合约可以有一个(最多也只能有一个)没有名字的函数。 这个函数无参数,也无返回值。
如果调用合约时,没有匹配上任何一个函数(或者没有提供数据),则会调用回退函数。
另外,尽管回退函数没有参数,其依然可以使用 msg.data 读取调用时提供 的 payload。

函数重载(Function Overloading)

函数重载是指一个合约可以有多个参数不同的同名函数

pragma solidity ^0.4.16;

contract A{
    function f(uint _in) public pure returns (uint out) {
        out = 1;
    }
    
    function f(uint _in,bytes32 _key) public pure returns (uint out) {
        out = 2;
    }
    
}

有一种情况需要注意,如果仅仅是 Solidity类型不一样,但在外部接口(ABI) 上表现一样的话,那么是无法函数重载的。 例如,下面的例子就是无法编译的。

pragma solidity ^0.4.24;

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

重载的参数匹配问题
重载函数在调用时, 会根据调用提供的参数的类型去匹配重载函数, 这期间可能会发生隐式的类型转换。 如果可以匹配上多个重载函数,则会调用失败。 来看一个例子:

pragma solidity ^0.4.24;

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

f(50)会失败, 因为 50 可以隐式转换为 uint8 和 uint256。 而f(256)则会调用 f(uint256), 因为 256 不能转换为 uint8。 注意: 返回值不参与匹配。

事件

事件主题(Topic)
主题是用来把事件索引化( Index)的数值。没有主题,就不能搜索事件 (主题的作用是用来检索事件的)。一个事件最多可以有四个主题。

第一个主题是事件签名 ,剩下三个主题是索引化的参数数值(即最多有三个参数接收属性可以被设置为索引)。设置为主题后,可以允许通过这个参数来 查找日志,甚至可以按特定的值过滤。如果参数是字符串、字节或者数组,那 么主题是它的 keccak-256 哈希。

注意: 1. 事件签名 hash 是其中一个主题,匿名事件除外,这意味着对于匿 名事件无法通过名字来过滤。 2. 被索引的参数将不会保存它们自己,可以搜索它们的值,但不能检索值本身。所有未被索引的参数将被作为日志的一部分被保存起来。假设下面这个例子

event PersonCreated(uint indexed age,uint indexed height)
// 通过参数触发
emit PersonCreated(26,176);

这里会生成 3 个主题:

  • 0x6bel5e8568869blel00750dd5079151b32637268ec08d199b318b793181b8a7d 它是事件的签名,计算方法是 Keccak-256(“PersonCreated(uint256, uint256)”)。
  • 0x36383cc9cfbfl dc87c78c2529ae2fcd4e3fc4e575el 54b357ae3a8b2739113cf, 他是年龄 26 的Keccak-256 值。
  • 0x048dd4d5794e69cea63353d940276ad6 lf89c65942226a2bb5bd352536892f82, 它是身高 176 的 Keccak-256 值。
    在节点上就会创建这样的可搜索的索引,之后可以在 Web3 中对索引进行 过滤搜索,比如可以过滤出所有 26 岁的人,只需要使用 以下代码:
var createdEvent = myContract.PersonCreated({age: 26});
createdEvent.watch(function(err,result) {
	if(err) {
		console.log(err)
		return;
	}
	console.log("Found",result);
})

底层的日志接口(Low-level Interface to Log)

通过函数 log0、 log1 、 log2、 log3、 log4 直接访问底层的日志。 logi 表示带 i + 1 个 bytes32 类型的参数, i 表示的就是可带参数的数目, 从 0 开始计数。
其中第一个参数会被用来作为日志的数据部分,其他的参数作为主题。

继承

Solidity继承使用的是关键字 is。Solidity 是通过复制包括多态的代码来支持多重继承的。如果没有明确指定调用哪一个合约的函数,那么最终被派生的方法通常会被调用 。如果一个合约从多个其他合约那里继承,那么在区块链上仅会创建一个合约,并且在父合约里的代码会被复制用来创建被继承合约。

pragma solidity ^0.4.16;

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

contract mortal is owned {
    function kill() {
        if (msg.sender == owner) selfdestruct(owner);
    }
}

contract Config {
    function loopup(uint id) public returns (address adr);
}

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

contract named is owned,mortal {
    function named(bytes32 name) {
        Config config = Config(0xD5dfD8D94886E70b06E474c3fB14Fd43E2E23970);
        NameReg(config.loopup(1)).register(name);
    }
    function kill() public {
        if (msg.sender == owner) {
            Config config = Config(0xD5dfD8D94886E70b06E474c3fB14Fd43E2E23970);
            NameReg(config.loopup(1)).unregister();
            mortal.kill();
        }
    }
}

contract PriceFeed is owned,mortal,named("GoldFeed") {
    function updateInfo(uint newInfo) public {
        if (msg.sender == owner)
            info = newInfo;
    }
    
    function get() public view returns (uint r) {
        return info;
    }
    uint info;
}

**注意, 上面例子的 kill() 方法中,我们调用了 motal.kill() , 调用了父合约的 销毁函数(destruction)。但这可能会引发一些问题,看下面的例子:
**

pragma solidity ^0.4.0;

contract owned {
	function owned() public {
		owner = msg.sender;
	}
	address owner;
}
contract mortal is owned {
	function kill() public {
		if (msg.sender == owner) selfdestruct(owner);
	}
}
contract Base1 is mortal {
	function kill() public {
		mortal.kill();
	}
}

contract Base2 is mortal {
	function kill() public {
		mortal.kill();
	}
}
contract Final is Base1, Base2 {}

对 Final.kill() 的调用只会调用 Base2.kill() (最后重载的函数),而派生重写会跳过 Base1.kill, 因为它根本就不知道有 Basel 。一个变通的方法就是使用 super, 看下面的例子:

pragma solidity ^0.4.0;

contract owned {
	function owned() public {
		owner = msg.sender;
	}
	address owner;
}
contract mortal is owned {
	function kill() public {
		if (msg.sender == owner) selfdestruct(owner);
	}
}
contract Base1 is mortal {
	function kill() public {
		super.kill();
	}
}

contract Base2 is mortal {
	function kill() public {
		super.kill();
	}
}
contract Final is Base1, Base2 {}

如果 Base2 调用了函数 super,那么它不仅会调用基类的合约函数,还会调 用继承关系图谱上的下一个基类合约,所以会调用 Basel.kill() 。需要注意的是 最终的继承图谱将会是 Final、 Base2、 Basel 、 mortal、 owned。

构造函数(Constructor)

构造函数也叫“构造器气通常用来完成合约的初始化变量赋值。它是一个 可选函数,构造函数使用关键字 constructor 表示。 它仅在创建合约时执行二次。 如果没有实现构造函数,那么合约会添加一个默认的构造函数。
构造函数要么是 public, 要么是 internal。 如果是 internal,则作为抽象合约。

pragma solidity ^0.4.22;

contract A {
	uint public a;
	constructor (uint _a) internal {
		a = _a;
	}
}

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

在 Solidity 0.4.22 版本之前,构造是使用与合约同名的函数来实现的,语法如下所示(不过现在这个写法已经弃用了)。

pragma solidity ^0.4.22;

contract A {
	uint public a;
	constructor (uint _a) internal {
		a = _a;
	}
}

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

父合约构造函数的参数

派生的合约需要调用所有父合约的构造函数,并且需要提供所有父合约需 要的参数

pragma solidity ^0.4.22;

contract Base {
    uint x;
    constructor(uint _x) public {
        x = _x;
    }
     function getValue() returns (uint){
        return x;
    }
}

// 第1种方式
contract Derived1 is Base(7) {
    uint y;
    constructor (uint _y) public {
        y = _y;
    }
    function getValue() returns (uint){
        return y;
    }
}

//第2种方式
contract Derived2 is Base {
    uint y;
    constructor (uint _y) Base(_y * _y) public {
        y =_y;
    }
     function getValue() returns (uint){
        return y;
    }
}

一种称为“继承列表”式,即直接在继承列表中使用 is Base(7), 示例代码 Derived I 合约就是使用这个方式。

另一种称为“修改器风格”式,示例代码 Derived2 合约就是使用这个方式。 Base(_y * _y)就像是一个函数修改器,它会作为修饰派生构造函数的一部分得到执行。

第一种方式对于构造器是常量的情况比较方便,可以直接说明合约的行为。 第二种方式适用于构造的参数值是由派生合约指定的情况。

多继承与线性化

支持多继承的编程语言需要解决几个问题,其中之一就是菱形继承问题又 称“钻石问题”。 Solidity 的解决方案可以参考 Python, 使用 C3 线性化方式来强制将基类合约转换为一个有向无环图(DAG)的特定顺序。基类合约在 is 后 的顺序变得非常重要。

pragma solidity ^0.4.0;
contract X {}
contract A is X {}
contract C is A, X {}

原因是 C 会请求 X 来重写 A (因为继承定义的顺序是 A、 X),但 A 自身又是重写 X 的,所以这是一个不可解决的矛盾。
一个简单的指定基类合约的继承顺序,原则上是从最接近基类到最接近派生类的。

同名问题

如果在继承时出现一个合约同时存在多个相同名字的修改器或函数,那么会产生错误。 如果事件与修改器重名或函数与事件重名 ,则都会产生错误。
例外的是,状态变量的 getter 可以覆盖一个 public 函数。

抽象合约(Abstract Contract)

和java抽象一致。
如果一个合约从一个抽象合约里继承,但却没实现所有函数,那么它也是 一个抽象合约。
下面是一个函数类型的例子,声明一个变量。变量的类型是函数:
function (address) external returns (address) foo;
抽象合约分离了定义和实现,但也提供了扩展的能力。

接口(Interface)

接口被限制为合约 ABI 定义可以表示的内容, ABI 和接口定义之间应该可以进行转换而不会有任何信息丢失。 合约可以继承接口,就像合约可以继承其他的合约一样。

库与合约类似,它也部署在一个指定的地址上(仅被部署一次,当然代码 可以在不同的合约反复使用),然后通过 EVM 的特性 DELEGATECALL (Homestead 之前是用 CALLCODE)来复用代码。 库函数在被调用时,库代码 是在发起合约(下文称主调合约: 主动发起DELEGATECALL 调用的合约) 的 上下文中执行的,使用 this 将会指向主调合约,而且库代码可以访问主调合约 的存储(storage)。

对比普通合约来说, 库有以下的限制。

  1. 无状态变量。
  2. 不能继承或被继承。
  3. 不能接收以太币。
  4. 不能销毁一个库。

不会修改状态变量(例如被声明 view 或 pure), 库函数只能通过直接调用 (不用 DELEGATECALL )。这是因为库被认为是与状态无关的。

库有许多使用场景。 其中两个主要的场景如下:

  1. 如果有许多合约, 它们有一些共同代码,则可以把共同代码部署成一个 库。这将节省 gas, 因为 gas 也依赖于合约的规模。因此, 可以把库想象成使用 其合约的父合约。
  2. 库可用于给数据类型添加成员函数。(Using for用法)
    由于库被当成隐式的父合约(它们不会显式地出现在继承关系中 ,但调用 库函数和调用父合约的方式是非常类似的,如库 L 有函数 f(),则使用 L.f() 即可访问 ), 库里面的内部函数被复制给使用它的合约。
    同样按调用内部函数的调用方式, 这意味着所有内部类型可以传进去, memory 类型则通过引用传递,而不是拷贝的方式。同样库里面的结构体和枚举 也会被复制给使用它的合约。 因此,如果一个库里只包含内部函数或结构体或 枚举,则不需要部署库,因为库里面的所有内容都被复制给使用它的合约了 。
pragma solidity ^0.4.16;

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 conntains(Data storage self,uint value) public view returns (bool){
        return self.flags[value];
    }
}

contract C {
    Set.Data knownValues;
    
    function register(uint value) public {
        // 库函数不需要实例化就可以调用,因为实例就是当前的合约
        require(Set.insert(knownValues,value));
        // 在这个合约中,如果需要的话,可以直接访问 knownValues.flags
    }
}

我们也可以不按上面的方式来使用库函数,可以不定义结构体, 可以不使用 storage 类型引用的参数, 还可以在任何位置有多个 storage 引用类型参数。
调用 Set.contains、 Set.remove、 Set.insert 会编译为 以 DELEGATECALL 的 方式调用外部合约和库。 使用库时,需要注意的是一个真实的外部函数调用发生了 。尽管 msg.sender、 msg.value、 this 还会保持它们在主调合约中的值。
下面的例子演示了在库中如何使用 memo叩 类型和内部函数来实现一个自 定义类型,而不会用到外部函数调用 。

pragma solidity ^0.4.16;

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 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;
    }
    
    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) {
            uint[] memory newLimbs = new uint[](r.limbs.length + 1);
            for (i = 0; i< r.limbs.length; ++i) 
                newLimbs[i] = r.limbs[i];
            newLimbs[i] = r.limbs[i];
            r.limbs = newLimbs;
        }
    }
}

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

在合约的源码中不能添加库地址,它是在编译时向编译器以参数形式提供的。

Using for 指令

指令 using A for B;用来把库函数(从库 A)关联到类型 B。。这些函数将会 把调用函数的实例作为第一个参数。 语法与 Python 中的 self 变量一样。 例如, 库 A 有函数 add(B b1, B b2),使用 Using A for B 指令后,如果有 B b1 ,就可以使用 b1.add(b2)。
using A for *表示库 A 中的函数可以关联到任意的类型上。
using A for B,指令仅在当前的作用域有效, 即目前仅仅支持当前合约的作用域,后续也非常有可能解除这个限制,允许作用到全局范围。如果能作用到全局范围, 通过引入一些模块(module),数据类型将能通过库函数扩展功能, 而不需要每个地方都写一遍类似的代码了。
以之前的例子来说:

pragma solidity ^0.4.16;

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 conntains(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 {
        // 库函数不需要实例化就可以调用,因为实例就是当前的合约
    //   Set.insert(knownValues,value);
        require(knownValues.insert(value));
        // require(Set.insert(knownValues,value));
        // 在这个合约中,如果需要的话,可以直接访问 knownValues.flags
    }
}

同样可以使用 Using for 的方式来对基本类型(elementa可 type)进行扩展

pragma solidity ^0.4.16;

library Search {
	function indexOf(uint[] storage slef, 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 函数调用 。 这意味着如果传的是 memory 类型或者值类型,那么进行拷贝时即使是 self变量,解决方法也是使用 存储(storage)类型的引用来避免拷贝内容的。

借鉴https://blog.csdn.net/lj900911/article/details/83037536

posted @ 2020-09-02 14:49  绯色之空  阅读(990)  评论(0)    收藏  举报
Live2D