第四章:以太坊智能合约 - 合约
以太坊智能合约 - 合约
合约概述
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。不过表达式有一些限制。
- 不允许访问 storage。
- 不允许访问 区块链数据,如 now、 this.balance、 block.number。
- 不允许访问合约执行中的数据,如 msg.value 或 gasleft()。
- 不允许向外部合约发起调用 。
但内置的函数 keccak256、keccak256、ripemd160、ecrecover、addmod、mulmod 可以允许调用,即使它们调用的是外部合约。 编译器并不会为常量在 storage 上预留空间, 每个使用的常量都会被对应的 常量表达式所替换。
视图函数(View Function)
函数可以声明为 view,表示它不能修改状态。下面几种情况被认为是修改了状态。
- 写状态变量。
- 触发事件。
- 创建其他的合约。
- call 调用附加了以太币 。
- 调用了任何没有 view 或 pure 修饰的函数。
- 使用了低级别的调用( low-level call)。
- 使用 了包含特定操作符的内联汇编。
pragma solidity ^0.4.16;
contract C {
function f(uint a, uint b) public view returns (uint) {
return a * (b + 42) + now;
}
}
有几个地方需要注意一下:
- 声明为 view 和声明为 constant 是等价的, constant 是 view 的别名 。 constant 计划在 Solidity 0.5.0 版本之后被弃用 。
- 访问函数都被标记为 view。
纯函数(Pure Function)
函数可以声明为 view, 表示它既不能读取状态,也不能修改状态。
- 读状态变量。
- 访问了 this.balance 或.balance。
- 访问了 block, tx, msg 的成员 Cmsg.sig 和 msg.data 除外)。
- 调用了任何没有 pure 修饰的函数。
- 使用了包含特定操作符的内联汇编。
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)。
对比普通合约来说, 库有以下的限制。
- 无状态变量。
- 不能继承或被继承。
- 不能接收以太币。
- 不能销毁一个库。
不会修改状态变量(例如被声明 view 或 pure), 库函数只能通过直接调用 (不用 DELEGATECALL )。这是因为库被认为是与状态无关的。
库有许多使用场景。 其中两个主要的场景如下:
- 如果有许多合约, 它们有一些共同代码,则可以把共同代码部署成一个 库。这将节省 gas, 因为 gas 也依赖于合约的规模。因此, 可以把库想象成使用 其合约的父合约。
- 库可用于给数据类型添加成员函数。(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

以太坊智能合约 - 合约
浙公网安备 33010602011771号