1.6.6 Solidity中的智能合约
Solidity中的智能合约类似其它面向对象编程语言中的类。智能合约中的状态变量存储的数据是永久的。其成员函数可以改变状态变量的值。当调用其它智能合约中的函数时,系统会进行上下文切换。
1.6.6.1 创建智能合约
智能合约实例可以通过以太坊交易从外部创建也可以从solidity编写的合约内创建。当一个合约实例被创建时,它的构造函数会执行一次。构造函数是可选的,但只允许定义一个构造函数,不能重载构造函数,这一点和其它面向对象语言不同。
1.6.6.2 可见性和Getters
在Solidity中,有四种可见性关键字:external,public,internal和private。默认时函数可见性为public。对状态变量而言,不能用external来定义,其它三个都可以,状态变量默认的可见性为internal。
1) external – 关键字external定义的外部函数是合约的接口,可以被其它合约调用。外部函数f不能作为内部函数调用,也就是说f()的调用方式不行,必须用this.f()。
2) public – 关键字public定义的函数都是接口,可以被内部函数或外部消息调用。对用public定义的状态变量,系统会自动生成一个getter函数。
3) internal – 用关键字internal定义的函数和状态变量只能在(当前合约或当前合约派生的合约)内部进行访问。
4) private – 关键字private定义的函数和状态变量只对定义它的合约可见,该合约派生的合约都不能调用和访问该函数及状态变量。
注意:合约中每一个定义的变量对外部而言都是可见的。用关键字private定义的变量只是不让其它合约修改该变量,但其它合约仍然可以读取该变量的值。
当用可见性关键字定义状态变量时,关键字紧接在状态变量的类型后,当用其定义函数时,关键字紧接在函数后而放在参数列表前。如下例所示。
pragma solidity ^0.4.16;
contract C {
function f(uint a) private pure returns (uint b) { return a + 1; }
function setData(uint a) internal { data = a; }
uint public data;
}
下例中,合约D调用c.getData()读取data的值,但不能调用函数f。合约E派生自C,因而可以调用函数compute。
// This will not compile
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) public { 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() 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 {
function g() public {
C c = new C();
uint val = compute(3, 5); // access to internal member (from derived to parent contract)
}
}
对于所有用public关键字定义的状态变量,编译器都会自动产生一个getter函数。在下例中,编译器会产生一个不带任何输入参数的函数data,data函数返回一个uint值。状态变量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();
}
}
getter函数有外部可见性。如果该变量或函数从内部调用(没有关键字this.)就会被视作状态变量,如果是被外部调用(带关键字this.),则被视作为函数。
pragma solidity ^0.4.0;
contract C {
uint public data;
function x() public {
data = 3; // internal access
uint val = this.data(); // external access
}
}
1.6.6.3 函数修饰符
函数修饰符可用于改变函数的语义和行为。修饰符是可被继承的,也可被派生合约重载。下面实例展示了函数修饰符的用法。
pragma solidity ^0.4.22;
contract owned {
function owned() public { owner = msg.sender; }
address owner;
// 这个合约只定义了一个修饰符,但没有使用它。这个修饰符将在派生合约中使用。
// 在修饰符出现的带特殊字符‘_;’的地方,插入了该函数的函数体。
// 这意味着,如果owner调用该函数,函数会被执行,否则会抛出异常。
modifier onlyOwner {
require(
msg.sender == owner,
"Only owner can call this function."
);
_;
}
}
contract mortal is owned {
// 该合约自‘owned’继承了`onlyOwner` 修饰符,并用此修饰符修饰close函数。
// 这样仅当owner调用close时才会起作用。
function close() public 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) public { price = initialPrice; }
// 这里必须使用‘payable’关键字否则函数会自动拒绝发送给它的以太币
function register() public payable costs(price) {
registeredAddresses[msg.sender] = true;
}
function changePrice(uint _price) public onlyOwner {
price = _price;
}
}
contract Mutex {
bool locked;
modifier noReentrancy() {
require(
!locked,
"Reentrant call."
);
locked = true;
_;
locked = false;
}
/// 函数被mutex保护,这意味着‘msg.sender.call’执行的重入调用
/// 将不能再次调用函数‘f’。
/// `return 7`语句把7赋值给返回值,
/// 但仍然在修饰符中执行`locked = false`语句。
function f() public noReentrancy returns (uint) {
require(msg.sender.call());
return 7;
}
}
当多个修饰符用于修饰函数时,修饰符之间可用任何空白字符分隔,系统会依次解析这些修饰符。
当显式地从一个修饰符或函数体返回时,系统只会从当前修饰符或函数体返回。返回变量被赋值,系统继续执行上一个修饰符‘_’之后的语句。
修饰符的参数可以是任意表达式,所有函数可见的变量修饰符亦可见。但修饰符中定义的变量函数不可见。
1.6.6.4 常量状态变量(Constant State Variables)
状态变量可用关键字constant定义。如果用constant定义,则该状态变量必须用一个常量来赋值。任何读取storage存储,区块链数据(比如now,this.balance或block.number),执行数据(比如msg.value或gasleft())或调用外部合约的表达式都不能被用来给常量状态变量赋值。对内存分配有影响的表达式可以用来赋值,但对内存中对象可能有影响的表达式不能用于赋值。系统自带的函数比如keccak256, sha256, ripemd160, ecrecover, addmod 和 mulmod可以用于赋值。
编译器不会为常量状态变量分配storage存储空间。目前仅有值类型(value type)和字符串(string)支持常量状态变量,如下例所示:
pragma solidity ^0.4.0;
contract C {
uint constant x = 32**22 + 8;
string constant text = "abc";
bytes32 constant myHash = keccak256("abc");
}
1.6.6.5 函数
1)关键字view定义的函数
关键字view定义的函数,不能改变状态。
下列方式会改变状态:
- 写状态变量
- 触发事件
- 创建新合约实例
- 使用selfdestruct
- 通过调用发送以太币
- 调用非view和pure定义的函数
- 调用底层函数
- 调用包含特定操作码的内联汇编语句
注意在函数中使用关键字constant等同于使用关键字view,但在0.5.0版本中会被淘汰。Getter函数标记为view。
如果使用某些显式类型强制转换,也可以在一个带关键字view的函数中改变状态变量。为了禁止这种操作,用户可以使用pragma experimental "v0.5.0";指示编译器使用STATICCALL来调用函数,避免对以太坊虚拟机状态的改变。
编译器并不保证带view关键字的函数一定不被改变状态,如果这种情况下函数状态发生改变,编译器只会发出警告。
2)纯函数
函数可用关键字pure来定义,这时函数中的状态变量既无法读取也无法写入。下列情况视为对状态的读。
读取状态。
调用this.balance或<address>.balance。
调用成员block,tx,msg(msg.sig和msg.data除外)。
调用非pure函数。
执行包含操作码的内联汇编语句。
下例是个pure函数实例:
pragma solidity ^0.4.16;
contract C {
function f(uint a, uint b) public pure returns (uint) {
return a * (b + 42);
}
}
如果使用某些显式类型强制转换,也可以在一个带关键字pure的函数中改变状态变量。为了禁止这种操作,用户可以使用pragma experimental "v0.5.0";指示编译器使用STATICCALL来调用函数,避免对以太坊虚拟机状态的改变。
注意:无法杜绝在以太坊虚拟机中通过函数调用读取状态,只能杜绝函数对状态的写入(view可被强制生效,pure不行)。
3)回退函数(Fallback Function)
一个合约可以且仅可以有一个不命名的函数。这个函数可以不带任何参数,也可以不带返回值。当某个调用发现合约所有的命名函数都不匹配或者是不含任何参数的函数被调用时,这个不命名的函数就会被调用。
另外,当合约收到以太币,但没有收到任何输入参数时,不命名的函数也会被执行。为了让回退函数接受以太币,必须使用关键字payable,否则合约无法通过正常的交易接收以太币。
在最坏的情况下,回退函数只有2300gas可用,在这种情况下,由于gas的限制,合约执行其它操作(logging操作除外)的余地就很小了。下列操作所消耗的gas都超过2300:
对storage存储的写操作
创建新合约实例
调用一个会大量消耗gas的外部函数
发送以太币
注意:尽管回退函数不含任何输入参数,用户仍然可以用msg.data得到调用的有效载荷(payload)。如果合约直接接收到以太币(不通过send或transfer函数调用),但却没有定义回退函数,合约会抛出异常,并退回收到的币。因此想让合约正常收到以太币,必须定义一个回退函数。如果合约没有用payable定义的回退函数,也能收到以太币,但是是以coinbase交易(coinbase transaction)的方式收到或者是以selfdestruct的方式收到。这种情况下,合约无法拒绝这笔付款。这是以太坊虚拟机强制的机制。这意味着this.balance得到的余额可能比合约中用其它方式计算(比如在回退函数中有个计数器专门负责记录余额)得到的余额要多。
见下例所示:
pragma solidity ^0.4.0;
contract Test {
// 每当此合约接收到到外部消息调用时,这个函数就会被调用
// 给此合约发送以太币会抛出异常,因为其回退函数没有用‘payable’定义
function() public { x = 1; }
uint x;
}
// 此合约将保留所有发送给它的以太币,并且这些以太币无法被取回。
contract Sink {
function() public payable { }
}
contract Caller {
function callTest(Test test) public {
test.call(0xabcdef01); // 哈希值不存在,导致 test.x becoming == 1.
// 下面语句无法通过编译,因此就算有交易发送以太币给合约,交易也会失败,
// 并退回收到的以太币
//test.send(2 ether);
}
}
4)函数重载(Function Overloading)
在智能合约内可以对函数进行重载,但必须使用不同的参数,对派生类中的函数重载也是这样。下例显示了如何在合约A的作用域内重载函数f。
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语言中定义不同而外部类型却相同,将无法通过编译。如下例所示:
// This will not compile
pragma solidity ^0.4.16;
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 {
}
这两个重载函数尽管输入参数和返回值不同,但实际上最终都把地址类型作为输入参数,因此无法通过编译。
系统在调用函数时,会根据其输入参数(或经过隐式转换后的输入参数)来判断该选择哪个重载函数。如果找不到匹配的函数,则调用失败。
注意:返回值不作为重载函数的选择标准。见下例:
pragma solidity ^0.4.16;
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隐式转换后只能转换为uint256,不能转换为uint8。
1.6.6.6 事件(Events)
事件是以太坊虚拟机日志工具的接口。事件可在DAPP中用来调用Javascript的回调函数(Callback Function)。
事件是合约中可继承的成员。当事件被调用时,相关数据会被存储在交易的日志(区块链的一个特殊数据结构)中。这些日志和合约的地址相关联,并会被记录到区块链中。日志和事件无法从合约(甚至是创建事件的合约)中调用。
如果外部调用能提供SPV证明给某合约,就可以可查询其对应的日志是否存在区块链中。有三个参数可以用属性indexed来查询。所有非indexed的参数都会被存在日志的数据部分。
也可通过底层调用读取日志信息,比如:log0,log1,log2,log3和log4。函数logi的输入参数为i + 1个bytes32类型的数。其中第一个参数被作为日志的数据部分,而其余的参数会被作为日志的主题部分。如下例所示:
pragma solidity ^0.4.10;
contract C {
function f() public payable {
bytes32 _id = 0x420042;
log3(
bytes32(msg.value),
bytes32(0x50cb9fe53daa9737b786ab3646f04d0150dc50ef4e75f59509d83667ad5adb20),
bytes32(msg.sender),
_id
);
}
}
上例中一长串的16进制数等于事件的签名“keccak256("Deposit(address,bytes32,uint256)")”。
1.6.6.7 继承(Inheritance)
Solidity语言支持多重继承和多态。
在Solidity中所有的函数调用最后实际上都调用的是派生类的函数,除非在调用时被显式指明。
当一个合约继承了多个合约时,在区块链中实际只有一个合约被创建,所有基类合约的代码都会被拷贝到区块链所实际创建的继承类合约中。Solidity的继承机制非常类似Python,尤其在多重继承方面。详见下例所示。
pragma solidity ^0.4.22;
contract owned {
constructor() { owner = msg.sender; }
address owner;
}
// Use `is` to derive from another contract. Derived
// contracts can access all non-private members including
// internal functions and state variables. These cannot be
// accessed externally via `this`, though.
contract mortal is owned {
function kill() {
if (msg.sender == owner) selfdestruct(owner);
}
}
// These abstract contracts are only provided to make the
// interface known to the compiler. Note the function
// without body. If a contract does not implement all
// functions it can only be used as an interface.
contract Config {
function lookup(uint id) public returns (address adr);
}
contract NameReg {
function register(bytes32 name) public;
function unregister() public;
}
// Multiple inheritance is possible. Note that `owned` is
// also a base class of `mortal`, yet there is only a single
// instance of `owned` (as for virtual inheritance in C++).
contract named is owned, mortal {
constructor(bytes32 name) {
Config config = Config(0xD5f9D8D94886E70b06E474c3fB14Fd43E2f23970);
NameReg(config.lookup(1)).register(name);
}
// Functions can be overridden by another function with the same name and
// the same number/types of inputs. If the overriding function has different
// types of output parameters, that causes an error.
// Both local and message-based function calls take these overrides
// into account.
function kill() public {
if (msg.sender == owner) {
Config config = Config(0xD5f9D8D94886E70b06E474c3fB14Fd43E2f23970);
NameReg(config.lookup(1)).unregister();
// It is still possible to call a specific
// overridden function.
mortal.kill();
}
}
}
// If a constructor takes an argument, it needs to be
// provided in the header (or modifier-invocation-style at
// the constructor of the derived contract (see below)).
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;
}
注意,在上例中,mortal.kill()提前调用了析构函数,这种用法会有潜在风险。再看下一例。
pragma solidity ^0.4.22;
contract owned {
constructor() 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 { /* do cleanup 1 */ mortal.kill(); }
}
contract Base2 is mortal {
function kill() public { /* do cleanup 2 */ mortal.kill(); }
}
contract Final is Base1, Base2 {
}
Final.kill()将会调用Base2.kill,这个调用会绕过Base1.kill,如果不想绕过,则要用关键字super。见下例所示。
pragma solidity ^0.4.22;
contract owned {
constructor() 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 { /* do cleanup 1 */ super.kill(); }
}
contract Base2 is mortal {
function kill() public { /* do cleanup 2 */ super.kill(); }
}
contract Final is Base1, Base2 {
}
这时如果Base2调用用super定义的函数,不仅会调用其基类合约(base contract)还会调用其继承关系里的其它基类合约,也就是说还会调用Base1.kill()(注意,其继承关系如下:Final,Base2,Base1,mortal,owned)。
1)构造函数
构造函数是可选的,由关键字constructor定义。构造函数在合约创建时被调用执行。构造函数可以被定义为public或internal。如果合约没有构造函数,则系统自动创建一个构造函数:constructor() public{}。见下例所示:
pragma solidity ^0.4.22;
contract A {
uint public a;
constructor(uint _a) internal {
a = _a;
}
}
contract B is A(1) {
constructor() public {}
}
被定义为internal的构造函数会让其智能合约被系统标注为abstract。
2)基类构造函数的参数
所有基类合约的构造函数都要遵循线性化规则被调用。如果基类构造函数有输入参数,则其继承类合约的构造函数也必须定义这些输入参数。如下例所示:
pragma solidity ^0.4.22;
contract Base {
uint x;
constructor(uint _x) public { x = _x; }
}
contract Derived1 is Base(7) {
constructor(uint _y) public {}
}
contract Derived2 is Base {
constructor(uint _y) Base(_y * _y) public {}
}
在上例中,其中一个方法是在继承列表里如is Base(7)那样。另一个方式是在继承合约的构造函数中如Base(_y * _y)。如果构造函数的参数是个常数则第一个方法更简单,如果基类合约的构造函数参数依赖继承类构造函数的参数则必须用第二种方法。总之参数要么在继承列表中,要么在继承类构造函数中像修饰符那样被定义,但不用在两个地方同时定义。如果继承合约不对其基类合约函数的参数进行定义,则被视为abstract。
3)多重继承和线性化
在支持多重继承方面,Solidity和Python一样采用的是“C3 线性化”(“C3 Linearization”)规则。编写合约排列继承顺序时从最基础的合约到最后继承的合约。这个顺序和Python不同。在下例中,编译会报错。
// This will not compile
pragma solidity ^0.4.0;
contract X {}
contract A is X {}
contract C is A, X {}
在这个例子中,合约C要求X覆盖A,但A的定义又要求A覆盖X,两相冲突,无法通过编译。
4)合约的多个成员同名
当继承的合约出现函数和修饰符同名时,会被系统视为错误。同理如果事件和修饰符同名或者函数与事件同名也都会被视为错误。有一个例外,状态变量的getter函数可以同名重载一个public函数。
1.6.6.8 抽象合约(Abstract Contracts)
合约中当有至少一个函数没有实现时即被视为抽象合约。这一点类似其它面向对象编程语言比如C++。如下例所示:
pragma solidity ^0.4.0;
contract Feline {
function utterance() public returns (bytes32);
}
抽象合约不能通过编译,只能作为基类合约被继承。如下例所示:
pragma solidity ^0.4.0;
contract Feline {
function utterance() public returns (bytes32);
}
contract Cat is Feline {
function utterance() public returns (bytes32) { return "miaow"; }
}
如果一个合约继承自基类合约,但却没有实现基类合约中所有未实现的函数,则此合约也被视为一个基类合约。
1.6.6.9 接口(Interfaces)
接口类似于抽象合约,但接口中不允许函数实现,除此以外,还有以下限制:
不能继承其它合约或接口
不能定义构造函数
不能定义变量
不能定义结构体类型
不能定义枚举类型
在未来的版本中,某些限制可能被取消。接口由关键字interface定义,如下例所示:
pragma solidity ^0.4.11;
interface Token {
function transfer(address recipient, uint amount) public;
}
合约可以继承接口。
1.6.6.10 库(Libraries)
库类似合约,但库只在某个地址发布一次,库可以被DELEGATECALL重用,也就是说如果某个合约调用了库,那么库代码将在那个合约内被执行。库是独立于合约的代码,因此库代码只能访问合约中存储于storage存储的状态变量。如果库函数不改变任何状态,那么它必须被直接调用(不用DELEGATECALL),因为库被视作是没有状态的。
对于调用库函数的合约来说,库可被视为是它的隐式基类合约。虽然库不会出现在调用它的合约的继承列表中,但调用库函数实际上非常像显式调用基类合约的函数。另外库中用internal定义的函数对所有合约皆可见,这也很类似基类合约与继承合约之间的关系。下例显示了如何使用库:
pragma solidity ^0.4.22;
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)
public
returns (bool)
{
if (self.flags[value])
return false; // already there
self.flags[value] = true;
return true;
}
function remove(Data storage self, uint value)
public
returns (bool)
{
if (!self.flags[value])
return false; // not there
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 {
// The library functions can be called without a
// specific instance of the library, since the
// "instance" will be the current contract.
require(Set.insert(knownValues, value));
}
// In this contract, we can also directly access knownValues.flags, if we want.
}
这个例子只是使用库的参考,读者不必完全照搬。Set.contains,Set.insert,Set.remove在编译时都会被编译为(DELEGATECALL)调用一个外部合约/库。所以在调用库时,一定要注意实际上系统是进行了外部函数调用。但在这个过程中,msg.sender,msg.value和this会保持它们的值不变(在Homestead之前,它们的值会变)。
下例展示了如何使用库中的memory内存类型数据和内部函数来实现自定义类型。
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 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) {
// 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 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);
}
}
由于编译器不知道库会被部署在什么地址,这些地址必须在链接的时候填入字节码,如果不提供地址,编译器的十六进制码就会包含类似_set__(这里set是库的名称)的标识,然后手动地把地址填写进去,用库所在地址的十六进制码取代这些(40个符号)标识。
与合约相比,库有下列限制:
1)没有状态变量。
2)不能继承或被继承。
3)不能接收以太币。
如果调用的库函数不是pure或view,并且调用时用CALL而不是用DELEGATECALL或CALLCODE,状态会回退。
以太坊虚拟机没有办法让合约查到是否是被CALL调用的,但合约能用ADDRESS操作码查看当前运行的地址,然后把当前运行的地址和合约创建时的地址进行比较就能看出是被谁调用。
1.6.6.11 Using For 指令
指令using A for B;可以把库A的函数附在类型B上,这时库A的函数会知道对其调用的合约实例,并把该合约实例作为函数的第一个参数(类似Python中的self变量)。
如果使用指令using A for *;其效果就相当于把库A的函数附在任何类型上。
在使用这个指令时,系统会进行类型检查并且会进行函数重载的操作。
指令using A for B;的作用域为当前合约,在未来的Solidity版本中会被扩展为全局作用域。这样如果一个合约包含了一个模块,那么模块的数据及库函数就完全对合约可见。如下例所示:
pragma solidity ^0.4.16;
// This is the same code as before, just without comments
library Set {
struct Data { mapping(uint => bool) flags; }
function insert(Data storage self, uint value)
public
returns (bool)
{
if (self.flags[value])
return false; // already there
self.flags[value] = true;
return true;
}
function remove(Data storage self, uint value)
public
returns (bool)
{
if (!self.flags[value])
return false; // not there
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; // this is the crucial change
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)`
require(knownValues.insert(value));
}
}
也可以把基本数据类型进行扩展,如下例所示:
pragma solidity ^0.4.16;
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 {
// This performs the library function call
uint index = data.indexOf(_old);
if (index == uint(-1))
data.push(_new);
else
data[index] = _new;
}
}
注意:所有对库的调用实际上都是以太坊虚拟机的函数调用,这意味着如果调用时传送的是memory内存类型或值类型,会进行变量的值拷贝(包括self变量)。只有当传送storage存储类型的引用变量时,才不进行值拷贝。
Last updated