solidity 合约详解4
发布日期:2021-06-28 22:10:29
浏览次数:2
分类:技术文章
本文共 14766 字,大约阅读时间需要 49 分钟。
1.合约 Solidity中合约有点类似面向对象语言中的类。合约中有用于数据持久化的状态变量(state variables)(即合约中的成员变量),和可以操作他们的函数。调用另一个合约实例的函数时,会执行一个EVM函数调用,这个操作会切换执行时的上下文,这样,前一个合约的状态变量(state variables)就不能访问了。 创建合约 合约可以通过Solidity,或不通过Solidity创建。 当合约创建时,一个和合约同名的函数(构造器函数)会调用一次,用于初始化。构造器函数是可选的。仅能有一个构造器,所以不支持重载。 如果不通过Solidity,我们可以通过web3.js,使用JavaScript的API来完成合约创建: // Need to specify some source including contract name for the data param below var source = "contract CONTRACT_NAME { function CONTRACT_NAME(unit a, uint b) {} }"; // The json abi array generated by the compiler var abiArray = [ { "inputs":[ {"name":"x","type":"uint256"}, {"name":"y","type":"uint256"} ], "type":"constructor" }, { "constant":true, "inputs":[], "name":"x", "outputs":[{"name":"","type":"bytes32"}], "type":"function" } ]; var MyContract_ = web3.eth.contract(source); MyContract = web3.eth.contract(MyContract_.CONTRACT_NAME.info.abiDefinition); // deploy new contract var contractInstance = MyContract.new( 10, 11, {from: myAccount, gas: 1000000} ); 具体内部实现里,构造器的参数是紧跟在合约代码的后面,但如果你使用web3.js,可以不用关心这样的细节。 如果一个合约要创建另一个合约,它必须要知道源码。这意味着循环创建依赖是不可能的。 pragma solidity ^0.4.0; contract OwnedToken { // TokenCreator is a contract type that is defined below. // It is fine to reference it as long as it is not used // to create a new contract. TokenCreator creator; address owner; bytes32 name; // This is the constructor which registers the // creator and the assigned name. function OwnedToken(bytes32 _name) { // State variables are accessed via their name // and not via e.g. this.owner. This also applies // to functions and especially in the constructors, // you can only call them like that ("internall"), // because the contract itself does not exist yet. owner = msg.sender; // We do an explicit type conversion from `address` // to `TokenCreator` and assume that the type of // the calling contract is TokenCreator, there is // no real way to check that. creator = TokenCreator(msg.sender); name = _name; } function changeName(bytes32 newName) { // Only the creator can alter the name -- // the comparison is possible since contracts // are implicitly convertible to addresses. if (msg.sender == address(creator)) name = newName; } function transfer(address newOwner) { // Only the current owner can transfer the token. if (msg.sender != owner) return; // We also want to ask the creator if the transfer // is fine. Note that this calls a function of the // contract defined below. If the call fails (e.g. // due to out-of-gas), the execution here stops // immediately. if (creator.isTokenTransferOK(owner, newOwner)) owner = newOwner; } } contract TokenCreator { function createToken(bytes32 name) returns (OwnedToken tokenAddress) { // Create a new Token contract and return its address. // From the JavaScript side, the return type is simply // "address", as this is the closest type available in // the ABI. return new OwnedToken(name); } function changeName(OwnedToken tokenAddress, bytes32 name) { // Again, the external type of "tokenAddress" is // simply "address". tokenAddress.changeName(name); } function isTokenTransferOK( address currentOwner, address newOwner ) returns (bool ok) { // Check some arbitrary condition. address tokenAddress = msg.sender; return (keccak256(newOwner) & 0xff) == (bytes20(tokenAddress) & 0xff); } } 2.可见性或权限控制(Visibility And Accessors) Solidity有两种函数调用方式,一种是内部调用,不会创建一个EVM调用(也叫做消息调用),另一种则是外部调用,会创建EVM调用(会发起消息调用)。Solidity对函数和状态变量提供了四种可见性。分别是external,public,internal,private。其中函数默认是public。状态变量默认的可见性是internal。 可见性 external: 外部函数是合约接口的一部分,所以我们可以从其它合约或通过交易来发起调用。 一个外部函数f,不能通过内部的方式来发起调用,(如f()不可以,但可以通过this.f())。外部函数在接收大的数组数据时更加有效。 public: 公开函数是合约接口的一部分,可以通过内部,或者消息来进行调用。对于public类型的状态变量,会自动创建一个访问器(详见下文)。 internal: 这样声明的函数和状态变量只能通过内部访问。如在当前合约中调用,或继承的合约里调用。需要注意的是不能加前缀this,前缀this是表示通过外部方式访问。 private: 私有函数和状态变量仅在当前合约中可以访问,在继承的合约内,不可访问。 备注 所有在合约内的东西对外部的观察者来说都是可见,将某些东西标记为private仅仅阻止了其它合约来进行访问和修改,但并不能阻止其它人看到相关的信息。 可见性的标识符的定义位置,对于state variable是在类型后面,函数是在参数列表和返回关键字中间。来看一个定义的例子: pragma solidity ^0.4.0; contract C { function f(uint a) private returns (uint b) { return a + 1; } function setData(uint a) internal { data = a; } uint public data; } 在下面的例子中,D可以调用c.getData()来访问data的值,但不能调用f。合约E继承自C,所以它可以访问compute函数。 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) { 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); // 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() { C c = new C(); uint val = compute(3, 5); // acces to internal member (from derivated to parent contract) } } 3.访问函数(Getter Functions) 编译器为自动为所有的public的状态变量创建访问函数。下面的合约例子中,编译器会生成一个名叫data的无参,返回值是uint的类型的值data。状态变量的初始化可以在定义时完成。 pragma solidity ^0.4.0; contract C{ uint public c = 10; } contract D{ C c = new C(); function getDataUsingAccessor() returns (uint){ return c.c(); } } 访问函数有外部(external)可见性。如果通过内部(internal)的方式访问,比如直接访问,你可以直接把它当一个变量进行使用,但如果使用外部(external)的方式来访问,如通过this.,那么它必须通过函数的方式来调用。 pragma solidity ^0.4.0; contract C{ uint public c = 10; function accessInternal() returns (uint){ return c; } function accessExternal() returns (uint){ return this.c(); } } 在acessExternal函数中,如果直接返回return this.c;,会出现报错Return argument type function () constant external returns (uint256) is not implicitly convertible to expected type (type of first return variable) uint256.。原因应该是通过外部(external)的方式只能访问到this.c作为函数的对象,所以它认为你是想把一个函数转为uint故而报错。 下面是一个更加复杂的例子: pragma solidity ^0.4.0; contract ComplexSimple{ struct Cat{ uint a; bytes3 b; mapping(uint => uint) map; } // mapping(uint => mapping(bool => Cat)) public content; function initial(){ content[0][true] = Cat(1, 1); content[0][true].map[0] = 10; } function get() returns (uint, bytes3, uint){ return (content[0][true].a, content[0][true].b, content[0][true].map[0]); } } contract Complex { struct Data { uint a; bytes3 b; mapping (uint => uint) map; } mapping (uint => mapping(bool => Data[])) public data; } 需要注意的是public的mapping默认访问参数是需要参数的,并不是之前说的访问函数都是无参的。 mapping类型的数据访问方式变为了data[arg1][arg2][arg3].a 结构体(struct)里的mapping初始化被省略了,因为并没有一个很好的方式来对键赋值。 4.常量(constant state variables) 状态变量可以被定义为constant,常量。这样的话,它必须在编译期间通过一个表达式赋值。赋值的表达式不允许:1)访问storage;2)区块链数据,如now,this.balance,block.number;3)合约执行的中间数据,如msg.gas;4)向外部合约发起调用。也许会造成内存分配副作用表达式是允许的,但不允许产生其它内存对象的副作用的表达式。内置的函数keccak256,keccak256,ripemd160,ecrecover,addmod,mulmod可以允许调用,即使它们是调用的外部合约。 允许内存分配,从而带来可能的副作用的原因是因为这将允许构建复杂的对象,比如,查找表。虽然当前的特性尚未完整支持。 编译器并不会为常量在storage上预留空间,每个使用的常量都会被对应的常量表达式所替换(也许优化器会直接替换为常量表达式的结果值)。 不是所有的类型都支持常量,当前支持的仅有值类型和字符串。 pragma solidity ^0.4.0; contract C { uint constant x = 32**22 + 8; string constant text = "abc"; bytes32 constant myHash = keccak256("abc"); } 常函数(Constant Functions)函数也可被声明为常量,这类函数将承诺自己不修改区块链上任何状态。 pragma solidity ^0.4.0; contract C { function f(uint a, uint b) constant returns (uint) { return a * (b + 42); } } 访问器(Accessor)方法默认被标记为constant。当前编译器并未强制一个constant的方法不能修改状态。但建议大家对于不会修改数据的标记为constant。 4。回退函数(fallback function) 每一个合约有且仅有一个没有名字的函数。这个函数无参数,也无返回值。如果调用合约时,没有匹配上任何一个函数(或者没有传哪怕一点数据),就会调用默认的回退函数。此外,当合约收到ether时(没有任何其它数据),这个函数也会被执行。在此时,一般仅有少量的gas剩余,用于执行这个函数(准确的说,还剩2300gas)。所以应该尽量保证回退函数使用少的gas。 下述提供给回退函数可执行的操作会比常规的花费得多一点。 写入到存储(storage) 创建一个合约 执行一个外部(external)函数调用,会花费非常多的gas 发送ether 请在部署合约到网络前,保证透彻的测试你的回退函数,来保证函数执行的花费控制在2300gas以内。 一个没有定义一个回退函数的合约。如果接收ether,会触发异常,并返还ether(solidity v0.4.0开始)。所以合约要接收ether,必须实现回退函数。下面来看个例子: pragma solidity ^0.4.0; contract Test { // This function is called for all messages sent to // this contract (there is no other function). // Sending Ether to this contract will cause an exception, // because the fallback function does not have the "payable" // modifier. function() { x = 1; } uint x; } // This contract keeps all Ether sent to it with no way // to get it back. contract Sink { function() payable { } } contract Caller { function callTest(Test test) { test.call(0xabcdef01); // hash does not exist // results in test.x becoming == 1. // The following call will fail, reject the // Ether and return false: test.send(2 ether); } } 在浏览器中跑的话,记得要先存ether。 5.事件(Events) 事件是使用EVM日志内置功能的方便工具,在DAPP的接口中,它可以反过来调用Javascript的监听事件的回调。 事件在合约中可被继承。当被调用时,会触发参数存储到交易的日志中(一种区块链上的特殊数据结构)。这些日志与合约的地址关联,并合并到区块链中,只要区块可以访问就一直存在(至少Frontier,Homestead是这样,但Serenity也许也是这样)。日志和事件在合约内不可直接被访问,即使是创建日志的合约。 日志的SPV(简单支付验证)是可能的,如果一个外部的实体提供了一个这样证明的合约,它可以证明日志在区块链是否存在。但需要留意的是,由于合约中仅能访问最近的256个区块哈希,所以还需要提供区块头信息。 可以最多有三个参数被设置为indexed,来设置是否被索引。设置为索引后,可以允许通过这个参数来查找日志,甚至可以按特定的值过滤。 如果数组(包括string和bytes)类型被标记为索引项,会用它对应的Keccak-256哈希值做为topic。 除非是匿名事件,否则事件签名(比如:Deposit(address,hash256,uint256))是其中一个topic,同时也意味着对于匿名事件无法通过名字来过滤。 所有未被索引的参数将被做为日志的一部分被保存起来。 被索引的参数将不会保存它们自己,你可以搜索他们的值,但不能检索值本身。 下面是一个简单的例子: pragma solidity ^0.4.0; contract ClientReceipt { event Deposit( address indexed _from, bytes32 indexed _id, uint _value ); function deposit(bytes32 _id) { // Any call to this function (even deeply nested) can // be detected from the JavaScript API by filtering // for `Deposit` to be called. Deposit(msg.sender, _id, msg.value); } } 下述是使用javascript来获取日志的例子。 var abi = /* abi as generated by the compiler */; var ClientReceipt = web3.eth.contract(abi); var clientReceipt = ClientReceipt.at(0x123 /* address */); var event = clientReceipt.Deposit(); // watch for changes event.watch(function(error, result){ // result will contain various information // including the argumets given to the Deposit // call. if (!error) console.log(result); }); // Or pass a callback to start watching immediately var event = clientReceipt.Deposit(function(error, result) { if (!error) console.log(result); }); 底层的日志接口(Low-level Interface to Logs) 通过函数log0,log1,log2,log3,log4,可以直接访问底层的日志组件。logi表示总共有带i + 1个参数(i表示的就是可带参数的数目,只是是从0开始计数的)。其中第一个参数会被用来做为日志的数据部分,其它的会做为主题(topics)。前面例子中的事件可改为如下: log3( msg.value, 0x50cb9fe53daa9737b786ab3646f04d0150dc50ef4e75f59509d83667ad5adb20, msg.sender, _id ); 其中的长16进制串是事件的签名,计算方式是keccak256("Deposit(address,hash256,uint256)") 6.继承(Inheritance) Solidity通过复制包括多态的代码来支持多重继承。所有函数调用是虚拟(virtual)的,这意味着最远的派生方式会被调用,除非明确指定了合约。当一个合约从多个其它合约那里继承,在区块链上仅会创建一个合约,在父合约里的代码会复制来形成继承合约。 基本的继承体系与python有些类似,特别是在处理多继承上面。 下面用一个例子来详细说明: pragma solidity ^0.4.0; contract owned { function owned() { 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) returns (address adr); } contract NameReg { function register(bytes32 name); function unregister(); } // 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 { function named(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() { 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) { if (msg.sender == owner) info = newInfo; } function get() constant returns(uint r) { return info; } uint info; } 上面的例子的named合约的kill()方法中,我们调用了motal.kill()调用父合约的销毁函数(destruction)。但这样可能什么引发一些小问题。 pragma solidity ^0.4.0; contract mortal is owned { function kill() { if (msg.sender == owner) selfdestruct(owner); } } contract Base1 is mortal { function kill() { /* do cleanup 1 */ mortal.kill(); } } contract Base2 is mortal { function kill() { /* do cleanup 2 */ mortal.kill(); } } contract Final is Base1, Base2 { } 对Final.kill()的调用只会调用Base2.kill(),因为派生重写,会跳过Base1.kill,因为它根本就不知道有Base1。一个变通方法是使用super。 pragma solidity ^0.4.0; contract mortal is owned { function kill() { if (msg.sender == owner) selfdestruct(owner); } } contract Base1 is mortal { function kill() { /* do cleanup 1 */ super.kill(); } } contract Base2 is mortal { function kill() { /* do cleanup 2 */ super.kill(); } } contract Final is Base2, Base1 { } 如果Base1调用了函数super,它不会简单的调用基类的合约函数,它还会调用继承关系图谱上的下一个基类合约,所以会调用Base2.kill()。需要注意的最终的继承图谱将会是:Final,Base1,Base2,mortal,owned。使用super时会调用的实际函数在使用它的类的上下文中是未知的,尽管它的类型是已知的。这类似于普通虚函数查找(ordinary virtual method lookup) 基类构造器的方法(Arguments for Base Constructors) 派生的合约需要提供所有父合约需要的所有参数,所以用两种方式来做,见下面的例子: pragma solidity ^0.4.0; contract Base { uint x; function Base(uint _x) { x = _x; } } contract Derived is Base(7) { function Derived(uint _y) Base(_y * _y) { } } 或者直接在继承列表中使用is Base(7),或像修改器(modifier)使用方式一样,做为派生构造器定义头的一部分Base(_y * _y)。第一种方式对于构造器是常量的情况比较方便,可以大概说明合约的行为。第二种方式适用于构造的参数值由派生合约的指定的情况。在上述两种都用的情况下,第二种方式优先(一般情况只用其中一种方式就好了)。 多继承与线性化(Multiple Inheritance and Linearization) 实现多继承的编程语言需要解决几个问题,其中之一是菱形继承问题又称钻石问题,如下。A
B C
D
Solidity的解决方案参考Python,使用C3_linearization来强制将基类合约转换一个有向无环图(DAG)的特定顺序。结果是我们希望的单调性,但却禁止了某些继承行为。特别是基类合约在is后的顺序非常重要。下面的代码,Solidity会报错Linearization of inheritance graph impossible。 pragma solidity ^0.4.0; contract X {} contract A is X {} contract C is A, X {} 原因是C会请求X来重写A(因为继承定义的顺序是A,X),但A自身又是重写X的,所以这是一个不可解决的矛盾。 一个简单的指定基类合约的继承顺序原则是从most base-like到most derived。 继承有相同名字的不同类型成员 当继承最终导致一个合约同时存在多个相同名字的修改器或函数,它将被视为一个错误。同新的如果事件与修改器重名,或者函数与事件重名都将产生错误。作为一个例外,状态变量的getter可以覆盖一个public的函数。 7.抽象(Abstract Contracts) 抽象函数是没有函数体的的函数。如下: pragma solidity ^0.4.0; contract Feline { function utterance() returns (bytes32); } 这样的合约不能通过编译,即使合约内也包含一些正常的函数。但它们可以做为基合约被继承。 pragma solidity ^0.4.0; contract Feline { function utterance() returns (bytes32); function getContractName() returns (string){ return "Feline"; } } contract Cat is Feline { function utterance() returns (bytes32) { return "miaow"; } } 如果一个合约从一个抽象合约里继承,但却没实现所有函数,那么它也是一个抽象合约。转载地址:https://blog.csdn.net/yhc166188/article/details/79829762 如侵犯您的版权,请留言回复原文章的地址,我们会给您删除此文章,给您带来不便请您谅解!
发表评论
最新留言
哈哈,博客排版真的漂亮呢~
[***.90.31.176]2024年04月14日 23时26分52秒
关于作者
喝酒易醉,品茶养心,人生如梦,品茶悟道,何以解忧?唯有杜康!
-- 愿君每日到此一游!
推荐文章
坑爹的小学数学题
2019-04-29
快速找出一个数组中的两个数字,让这两个数字之和等于一个给定的值
2019-04-29
[经典排序算法][集锦]
2019-04-29
无处不在的二分查找
2019-04-29
Java集合框架List,Map,Set等全面介绍
2019-04-29
Java 泛型(二) 泛型之中的通配符(Wildcards)使用
2019-04-29
7-36 复数四则运算 (15 分)
2019-04-29
基于powershell的渗透测试工具nishang
2019-04-29
pycharm创建django项目linux部署
2019-04-29
利用Android Studio快速搭建App
2019-04-29
CompletableFuture的多线程和异步监听实现
2019-04-29
HashMap jdk1.7和1.8概述
2019-04-29
springboot多环境加载yml和logback配置
2019-04-29
几道简单的算法题(来自leetcode)
2019-04-29
推一波JAVA学习公众号
2019-04-29
org.apache.ibatis.reflection.ReflectionException: Error instantiating class with invalid types
2019-04-29
HttpsURLConnection 返回 400
2019-04-29
python基础语法05
2019-04-29