solidity基础知识
Posted 凉面好好吃
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了solidity基础知识相关的知识,希望对你有一定的参考价值。
1、solidity是一种语法类似javascript的高级语言,它被设计成以编译的方式生成以太坊虚拟机代码。在后续的内容中你将会发现,使用它很容易创建用于投票、众筹、封闭拍卖、多重签名钱包等等的合约。
solidity在线IDE:https://ethereum.github.io/browser-solidity/#optimize=false&version=soljson-v0.4.23+commit.124ca40d.js
2、智能合约
智能合约(Smart Contract)是一种旨在以信息化方式传播、验证或执行合同的计算机协议。只能合约允许在没有第三方的情况下进行可信交易。这些交易可追踪不可逆转。智能合约概念于1994年由Nick Azabo首次指出。
智能合约的目的是提供优于传统合同方法的安全,病减少与合同相关的其他成本交易。
3、以太坊虚拟机
以太坊虚拟机(EVM)是以太坊中只能合约的运行环境。它不仅被沙箱封装起来,事实上它被完全隔离,也就是说EVM内部的代码不能寻呼机到网络、文件系统或者其他进程。甚至只能合约与其它只能合约只有有限的接触。
3.1、账户
以太坊有两类账户。他们共用同一个地址 空间。外部账户,该账户被公钥-私钥对控制(人类)。合约账户,该类账户被存储在账户中的代码控制。
外部账户由公钥决定;合约账户在创建合约时确定(由合约创建者地址+该地址发出过得交易数量计算。地址发出过的交易数量也被称为nonce);合约账户存储代码。
另外,每个账户都有一个以太币余额(单位wei),该账户余额可以通过向它发送带有以太币的交易来改变。
3.2、交易
交易是一条消息,从一个账户发送得到另一个账户(可能是相同的账户或者零账户)。交易可以包含二进制数据(payload)和以太币。
另外,如果目标账户零账户(账户地址是零),交易将创建一个新合约。即创建合约也是一个交易的过程,只是目标账户是0.
3.3、Gas
以太坊上的每笔交易都会收取一定数量的gas,gas的目的是限制执行交易所需的工作量,同时为执行支付费用。当EVM执行交易时,gas将按照特定规则被逐渐消耗。
gas price (以太币计)是由交易创建者设计的,发送账户需要预付的交易费用 = gas price * gas amount。如果执行结束还有剩余,这些gas将被返还给发送账户。
无论执行到什么位置,一旦gas被消耗尽,将会触发一个out-of-gas异常。当前调用帧所做的所有状态修改都将被回滚。
3.3 存储、主存和栈
每个账户有一块持久化区域被称为存储,其形式为key-value,key和value的长度均为256比特。在合约里,不能遍历账户的存储。相对于另外两种,存储的读操作开销较大,修改存储更甚。一个合约只能对它自己的存储进行读写。
第二内存区被称为主存。合约执行每次消息调用时,都有一块新的,被清除过得主存。主存可以以字节粒度寻址,但是读写粒度为32字节(256比特)。操作主存的开销随着其增长而变大(平方级别)。
EVM不是基于寄存器,而是基于栈的虚拟机,因此所有的计算都在一个被称为栈的区域执行。栈最大有1024个元素,每个元素256比特,对栈的访问只限于其顶端,方式为:允许拷贝最顶端的16个元素中的一个到栈顶,或者交换栈顶元素和下面16个元素中的一个。所有其他操作都只能取最顶的两个(或者一个、更多)元素,并把结果压在栈顶。当然可以把栈上的元素放到存储或者主存中,但是无法访问栈上指定深度的那个元素,在那之前必须要把指定深度之上的所有元素从栈中移除才行
3.4、指令集
EVM的指令集被刻意保持在最小规模,以尽可能避免可能导致共识问题的错误实现。所有的指令都是针对256比特这个基本的数据类型的操作。具备常用的算术,位,逻辑和比较操作。也可以做到条件和无条件跳转。此外,合约可以访问当前区块的相关属性,比如它的编号和时间戳。
3.5、消息调用
合约可以通过消息调用的方式来调用其它合约或者发送以太币到非合约账户。消息调用和交易非常类似,它们都有一个源,一个目标,数据负载,以太币,gas和返回数据。事实上每个交易都可以被认为是一个顶层消息调用,这个消息调用会依次产生更多的消息调用。
一个合约可以决定剩余gas的分配。比如内部消息调用时使用多少gas,或者期望保留多少gas。如果在内部消息调用时发生了out-of-gas异常(或者其他异常),合约将会得到通知,一个错误码被压在栈上。这种情况只是内部消息调用的gas耗尽。在solidity中,这种情况下发起调用的合约默认会触发一个人工异常。这个异常会打印出调用栈。就像之前说过的,被调用的合约(发起调用的合约也一样)会拥有崭新的主存并能够访问调用的负载。调用负载被存储在一个单独的被称为calldata的区域。调用执行结束后,返回数据将被存放在调用方预先分配好的一块内存中。
调用层数被限制为1024,因此对于更加复杂的操作,我们应该使用循环而不是递归。
3.6、代码调用和库
存在一种特殊类型的消息调用,被称为callcode。它跟消息调用几乎完全一样,只是加载目标地址的代码将在发起调用的合约上下文中运行。这意味着一个合约可以在运行时从另外一个地址动态加载代码。存储当前地址和余额都指向发起调用的合约,只有代码是被调用地址获取的。
这使得solidity可以实现库。可服用的库代码可以应用在一个合约存储上,可以用来实现复杂的数据结构。
3.7、日志
在区块层面,可以用一种特殊的可索引的数据界都来存储数据。这个特性被称为日志,solidity用它来实现事件。合约创建之后就无法访问日志数据,但是这些数据可以从区块链外高效的访问。因为部分日志数据被存储在布隆过滤器中,我们可以高效并且安全地搜索日志,所以那些没有下载整个区块链的网络节点(轻客户端)也可以找到这些日志。
3.8、创建
合约甚至可以通过一个特殊的指令来创建其他合约(不是简单的向零地址发起调用)。创建合约的调用跟普通的消息调用的区别在于,负载数据执行的结果被当做代码。调用者/创建者在栈上得到新的合约。
3.9、自毁
只有在某个地址的合约执行自毁操作时,合约代码才会从区块链上移除。合约地址上剩余的以太币会发送给指定目标,然后其存储和代码被移除。
4、语法
4.1、引入源文件
源文件包括任意数量的合约定义和include指令
solidity支持import语句。
在全局层次上,可以这样用:
(1)import "filename";将会从“filename”
(2)**import** ****** as symboName from "filename" 创立一个全局的符号名为sumbolName,其中的成员来自filename的所有符号
(3)import {symbol as alias,symbol2} from "filename";将创立一个新的全局变量别名:alias和symbol2,它将分别从filename引入symbol1和symbol2
(4)import "filename" as symbolName;//推荐使用 等价于 import * as symbolName from "filename"
路径:
(1)在上面,文件名总是用/作为目录分隔符,.是当前的目录,..是父目录,路径名称不用.开头的都将视为绝对路径。
(2)从同一个目录下import一个文件夹x作为当前文件,用import "./x" as x;如果使用import "x" as x;是不同的文件引用(在全局中使用“include directory”)
(3)import "x" as X 是不同的文件引用(在全局中使用“include directory”)。
(4)它将依赖于编译器(见后)来解析路径。通常,目录层次不必严格限定映射到你的本地文件系统,它也可以映射到ipfs,http或git上的其他资源
4.2、注释
//单行注释和多行注释(/../)
有种特别的注释叫做“natspec”(文档以后写出来),在函数声明或定义的右边用三个斜杠(///)或者用两个型号(/*.../).如果想要调用一个函数,可以使用doxygen-style标签里面文档功能,形式验证,并提供一个确认条件的文本注释显示给用户。
4.3、 类型
solidity是一种静态类型语言,意思是每个变量(声明和本地)在编译时刻都要定义(或者至少要知晓,参看后面的类型导出)。solidity提供几个基本类型组合成复杂类型。
(1)变量类型
以下类型被叫做值类型,因为这些类型的变量总是要被赋值,作为函数参数或者在赋值中,总需要拷贝。
布尔类型
布尔:bool 值是true和false
操作符:!(逻辑非)、&&(逻辑与)、||(逻辑或)、==(相等)、!=(不等)。
操作符||和&&可以应用常规短路规则,||如果一边为true,另一边不用计算,&&如果一边为false,另一边不用计算。
整型:int和uint,有符号和无符号的整数,关键字uint8到uint256步长8(从8到256位的无符号整数)。uint和int分别是uint256和int256的别名。
操作符:比较(<=,<,==,!=,>=,>计算布尔量),位操作符(&,|,^(位异或),~(位取反)),算术操作符(+,-,一元-,一元+,*,/,%(取余数),**(幂次方)).
地址成员:address,可以转账和查询余额(balance、send、transfer。
注解:如果x是合约地址,它的代码将和send调用一起执行(这是EVM的限制,不能修改),如果gas用完或者失败,ether转移将被回退,这种情况下,send返回false。
调用(call)和调用码(callcode):调用这两个函数将会返回true或false。callcode的目的是使用库代码存储在另一个合同。用户必须要确保存储在两个合约的布局适用于callcode。这两个函数是非常低级的函数,它可以作为打破solidity的类型安全的最后手段。
(2)引用类型
复杂类型,拷贝复杂类型可能相当耗费存储?时间,我们必须考虑把他们存储在内存(这不是持久化)或者存储器(状态变量存储的地方)
函数返回参数存储在内存,局部变量默认是存储在存储器里面。
(3)数据存储位置:内存、存储器、calldata
外部函数的参数(无返回):calldata
状态变量:存储器
默认数据存储位置
函数(有返回)的参数:内存
其他局部变量:存储器。
注意存储在不同位置的变量不能直接赋值。
内存与内存、存储器和存储器之间赋值是引用赋值 ,值会改变,其他是拷贝,不会改变值。
(4)数组
数组可以是固定大小的,也可以是动态的,对于存储器数组来说,成员类型可以是任意的(也可以是其他数组、映射或结构)。对于内存数组来说,成员类型不能是一个映射;如果是公开可见的函数参数,成员类型是必须是ABI类型的。
动态数组:string[] str;
定长数组:string[3] str;
bytesh和string是特殊类型的数组。bytes类似于byte[],但它是紧凑排列在calldata里的。string等于bytes,但不允许用长度或索引访问。同时如果想要访问字符串s的某个字节,要使用bytes(s).length/bytes(s)[7] = "x";记住,你正在访问的低级utf - 8字节表示,而不是单个字符!
成员:
length:数组数量。数组长度一旦确定,如果访问长度以外的数据,会报错,如果删除数组里面的数据,要手动调整长度,和数据位置。因为删除只是把值初始化,并没有删除。
push:在数组尾部添加数据。
注意:到目前为止还不可以在外部函数中使用数组的数组。由于EVM的局限,合约函数f contract C { function f() returns (uint[]) { ... } } 不可能从外部函数调用返回动态的数组(不能返回数组),使用web3.js调用,将有返回值,但是使用solidity调用,就没有返回值。
(5)结构体
struct shape{uint height;uint width;} 函数赋值 shape(1,2).结构体内部可以包含任何类型的数据,但是不能包含它自己。也就是不能把自己作为自己的成员。
(6)映射(mapping)
一种键值对的映射关系存储结构。定义方式为mapping(_KeyType => _KeyValue).键的类型允许除映射外的所有类型。值的类型无限制。映射可以视为一个哈希表,其中所有可能的键已被虚拟化的创建,被映射到一个默认值(二进制表示的零)。但在映射表中,我们并不存储键的数据,仅仅存储它的keccak256哈希值,用来查找时使用。所以映射没有长度。 mapping(address => uint) public balances;添加数据balance[msg.sender]=msg.value;
(7)删除delete(参考:http://baijiahao.baidu.com/s?id=1566265348199485&wfr=spider&for=pc)
delete用于释放空间,释放空间将会返还一些gas。
删除基本类型将他们的值设置为初始值。删除枚举时,会将其值重置为序号0的值。不能删除函数,删除结构体会将结构体里面的变量都设置为初始值,删除映射会报错,不过可以删除里面的某一项。删除十足试讲数组的所有元素设置为0.
删除本质是对一个变量赋初值。所以我们删除storage的引用时会报错,因为storage的引用并没有自己已分配的存储空间,所以不能对storage的引用直接赋初值。
(8)类型转换
隐式转换:如果一个操作符应用于不同类型,编译器就会试图隐式把操作数的类型,从一种类型转换到其他类型。一般来说,一个隐式的值类型之间的转换时可能的,如果予以敏感的话,信息不回丢失;int8可转成int16,int256等等,但是int8不能转成uint256,因为uint256范围不包含int8部分内容。
显示转换:int8 a = 2;uint b = uint(a);如果一个类型是显式地转换为一个更小的类型,高阶位将被移除.
(9)单位(参考:https://blog.csdn.net/wo541075754/article/details/79049425)
以太单位:wei、finney、szabo、ether,以太币数量默认缺省单位是wei。
1kwei(babbage) = 1e3wei (e就是乘以10的3次方)
1Mwei(lovelace) = 1e6wei; 1Gwei(shannon) = 1e9; 1microether(szabo) = 1e12; 1milliether(finney) = 1e15wei; 1ether = 1e18wei;
时间单位:now获取系统当前时间,类型是int,没有日期:1 seconds;minutes ,hours ,days,weeks,years等
如果你使用这些单位执行日历计算,要注意以下问题。 因为闰秒,所以每年不总是等于365天,甚至每天也不是都有24小时,。由于无法预测闰秒,一个精确的日历库必须由外部oracle更新。
(10)特殊的变量和函数
有特殊的变量和函数总是存在于全局命名空间,主要用于提供关于blockchain的信息。
块和交易属性:
- block.coinbase (address): :当前块的矿工的地址
- block.difficulty (uint):当前块的难度系数
- block.gaslimit (uint):当前块汽油限量
- block.number (uint):当前块编号
- block.blockhash (function(uint) returns (bytes32)):指定块的哈希值——最新的256个块的哈希值
- block.timestamp (uint):当前块的时间戳
- msg.data (bytes):完整的calldata
- msg.gas (uint):剩余的汽油
- msg.sender (address):消息的发送方(当前调用)
- msg.sig (bytes4):calldata的前四个字节(即函数标识符)
- msg.value (uint):所发送的消息中wei的数量
- now (uint):当前块时间戳(block.timestamp的别名)
- tx.gasprice (uint):交易的汽油价格
- tx.origin (address):交易发送方(完整的调用链)
由于所有块可伸缩性的原因,(所有)块的hash值就拿不到,你只能访问最近的256块的hash值,其他值为零。
(11)数学和加密功能
addmod(uint x,uint y,uint z) return (uint) 计算(X+y)%z
mulmod(uint x,uint y,uint z) return (uint) 计算(X*y)%z
加密数据
sha3(……) returns (byte32);
sha256(……) returns (byte32);
(12)自毁
selfdestruct(address);销毁当前合约,其资发送给指定的地址。此外,当前合同的所有函数可以被直接调用(包括当前函数)
4.4、表达式和控制结构
(1)控制结构:
除了 switch和goto,solidity的绝大多数控制结构均来自于C / JavaScript,if, else, while, for, break, continue, return, ? :, 的语义均和C / JavaScript一样。
条件语句中的括号不能省略,但在单条语句前后的花括号可以省略。
(2)函数调用
内部函数调用:直接调用函数;
外部函数调用:外外部调用函数,参数必须存储到内存中,特别注意的是合约构造函数中不能用this调用函数。因为当前合约还没有创建。如果直接调用合约名,而不实例化,那么合约并不执行构造函数,
具名参数调用:可以直接调用,按照顺序输入值,也可以({参数:值,})
函数返回可以返回多个类型不一样的值。支持返回元组类型。
(3)异常
有一些自动抛出异常的情况(见下文)。您可以使用throw 指令手动抛出一个异常。异常的影响是当前执行的调用被停止和恢复(即所有状态和余额的变化均没有发生)。另外, 异常也可以通过Solidity 函数 “冒出来”, (一旦“异常”发生, 就send "exceptions", call和callcode底层函数就返回false)。
捕获异常是不可能的。
目前,Solidity异常自动发生,有三种情况, :如果你访问数组超出其长度 (即x[i] where i >= x.length);如果一个通过消息调用的函数没有正确的执行结束(即gas用完,或本身抛出异常);如果一个库里不存在的函数被调用,或Ether被发送到一个函数库里。
异常还会通过solidity的函数调用向上冒泡(bubbled up)传递。(send,和底层的函数调用call,delegatecall,callcode是一个例外,当异常发生时,这些函数返回false)。
通过assert
判断内部条件是否达成,require
验证输入的有效性。这样的分析工具,可以假设正确的输入,减少错误。这样无效的操作码将永远不会出现。(返回值是false)
5、合约
合约是面向对象语言的类,他们持久存放在状态变量和函数中,可以修改这些变量。合约在创建时,构造函数将被调用。
(1)可见性和修饰符
函数调用分为内部调用和外部调用,内部调用不创建一个真实的EVM调用,也称为消息调用,外部调用需要创建一个真实的EVM调用。
函数修饰符有external、public、internal、private,缺省是public。对状态变量而言,external是不可能的,默认是internal。
external:外部函数是合约接口的一部分,这意味着他们可以从其他合约调用,也可以通过事务调用。如果在本合约调用external,要像外部合约调用这个函数一样,不能直接调用。external和public 修饰函数,外部函数都可以访问,但是external更省gas。
public:公共函数是合约接口的一部分,可以通过内部调用或通过消息调用。对公共状态变量而言,会有的自动访问修饰符的函数生成(如果一个状态变量用public修饰,合约将会自动给这个变量生成get函数,获取这个变量,需要调用get方法)
internal:这些函数和状态变量只能内部访问(即当前合约或由它派生的合约),而不使用(关键字)this
private:私有函数和状态变量仅仅在定义该合约中可见,在派生的合约中不可见。
注意:
在外部观察者中,合约的内部的各项均可见。用private仅仅防止其他合约来访问和修改(该合约中)信息。但它对blockchain之外的整个世界仍然可见。
可见性说明符是放在状态变量的类型之后,(也可以放在)参数列表和函数返回的参数列表之间。
(2)函数修饰符(modifier)
修饰符可以用来轻松改变函数的行为。列如,在执行的函数之前自动检查条件。他们是可继承合约的属性,也可被派生合约重写。(可以封装一些条件等等,或者封装重复使用的代码,一个函数可以使用多个修饰符,使用空格格开)
(3)常量(constant)
(4)回退函数(callback)
一个合约可以有一个匿名函数。若没有其他函数和给定法人函数标识符一致的话,该函数将没有参数,将执行一个合约的调用(如果没有提供数据)。
1)当合约接收一个普通的ether时,函数将被执行(没有数据)。在这样一个情况下,几乎没有gas用于函数调用,所以调用回退函数是非常廉价的,这点非常重要。
2)当合约收到ether时(没有任何其它数据),这个函数也会被执行。为了接收ether,回退功能必须标记为应付款。如果不存在此类功能,则合约无法通过正常交易接收ether。
3)一个没有定义一个回退函数的合约。如果接收ether,会触发异常,并返还ether(solidity v0.4.0开始)。所以合约要接收ether,必须实现回退函数。在部署合约到网络前,保证透彻的测试你的回退函数,来保证函数执行的花费控制在2300gas以内.
(5)事件
事件允许EVM写日志功能的方便使用,进而在dapp的用户接口中用javascript顺序调用,从而监听这些事件。
事件是合约可继承的成员。当他们调用时,会在导致一些参数在事务日志上的存储--在blockchain上的一种特殊的数据结构。这些日志和合约的地址相关联,将纳入blockchain中,存储在block里以便访问(在Frontier和Homestead里是永久存储,但在Serenity里有些变化)。在合约内部,日志和事件数据是不可访问的(从创建该日志的合约里)
事件的参数中,如果参数被设置为indexed(最多只有三个参数可以设置indexed),来设置是否索引,设置索引后,可以允许通过这个参数来查找日志,甚至可以按特定的值过滤。
如果参数增加indexed,存储到日志中的topic部分,如果事件参数对应的参数值只是单个值, 就是正常的存储(存储达到日志的topic部分),如果事件参数对应的参数值是一个数组类型,因为数组类型这种复杂类型的长度不确定,先转换成了哈希值。
如果参数不增加indexed,存储到日志的data部分。
日志中存储的不同的索引事件就叫不同的主题。比如,事件定义,event transfer(address indexed _from, address indexed _to, uint value)有三个主题,第一个主题为默认主题,即事件签名transfer(address,address,uint256),但如果是声明为anonymous的事件,则没有这个主题;另外两个indexed的参数也会分别形成两个主题,可以分别通过_from,_to主题来进行过滤。如果数组,包括字符串,字节数据做为索引参数,实际主题是对应值的Keccak-256哈希值。
(6)继承(is)
solidity支持多重继承,除非合约时显式给出的,所有的函数调用都是虚拟的,绝大多数派生函数可被调用。即使合约继承了多个其他的。
子类可以访问public、internal权限控制的变量和函数。在子类中可以访问状态变量,因为状态变量默是internal的。
1)继承支持传参,如果父类构造函数有参数,继承时 is Base(1)
方法二:
contract Base{ uint a; function Base(uint _a){ a = _a; } } contract InheritParaModifier is Base{ function InheritParaModifier(uint _a) Base(_a * _a){} function getBasePara() returns (uint){ return a; } }
如果要传入到基类的是简单的常量,第一种方式会更加简洁。但如果传入的参数与子类的输入参数有关,那么你应该使用第二种方式,以获取参数值。如果你同时使用了这两种方式,后一种方式将最终生效。
2)多继承会出现几个问题,如菱形继承问题(钻石问题),所以多继承要注意继承的顺序。同时如果继承的合约中拥有相同名字不同类型的方法或者变量,会视为错误。所以建议使用单继承。
(7)抽象(没有关键字,只是没有方法体的合约会视为抽象合约)
如果一个合约从一个抽象合约里继承,但却没实现所有函数,那么它也是一个抽象合约。
(8) 接口(interface)
接口与抽象合约类似,与之不同的是,接口内没有任何函数是已实现的,同时还有如下限制: .不能继承其它合约,或接口;不能定义构造器 ;.不能定义变量 ;不能定义结构体 ;.不能定义枚举类。
(9)库(library)
库与合约类似,但它的目的是在一个指定的地址,且仅部署一次,然后通过EVM的特性DELEGATECALL来复用代码。这意味着库函数调用时,它的代码是在调用合约的上下文中执行。使用this将会指向到调用合约,而且可以访问调用合约的存储(storage)。因为一个合约是一个独立的代码块,它仅可以访问调用合约明确提供的状态变量(state variables),否则除此之外,没有任何方法去知道这些状态变量。(调用同目录下本地库(./libraryName.sol))
对比普通合约来说,库的限制: 无状态变量(state variables)。 不能继承或被继承 不能接收ether。
库的常见―”坑” msg.sender 的值msg.sender 的值将是调用库函数的合约的值。 例如,如果 A 调用合约 B,B 内部调用库 C。在库 C 库的函数调用里,msg.sender 将是 合约 B 的地址。 表达式 LibraryName.functionName() 用 CALLCODE 完成外部函数调用, 它映射到一 个真正的EVM调用,就像otherContract.functionName() 或者 this.functionName()。 这种调用可以一级一级扩展调用深度(最多 1024 级),把 msg.sender 存储为当前的调 用者,然后执行库合约的代码,而不是执行当前的合约存储。这种执行方式是发生在一个完 全崭新的内存环境中,它的内存类型将被复制,并且不能绕过引用。 转移 Ether原则上使用 LibraryName.functionName.value(x)()来转移 Ether。但若使用 CALLCODE,Ether 会在当前合约里用完。
1)指令 using A for B; 可用于附加库函数(从库A)到任何类型(B)。这些函数将收到一个作为第一个参数的对象(像Python中self变量)。
using A for *;,是指函数从库A附加到任何类型。
在这两种情况下,所有的函数将被附加,(即使那些第一个参数的类型与对象的类型不匹配)。该被调用函数的入口类型将被检查,并进行函数重载解析。
6、修饰符
修饰符
-
constant for state variables: 不允许赋值(除了初始化),不占用存储块。
-
constant for functions:不允许改变状态- 这个目前不是强制的。
-
anonymous for events:不能将topic作为事件指纹进行存储。
- 函数可以声明view,在这种情况下他们保证不修改状态。(以下声明被视为修改状态: 1).写入状态变量。 2).发送事件。 3).创建其他合同。 4).使用selfdestruct。 5).通过电话发送以太。 6).调用任何未标记为view或pure的函数。 7).使用低级别呼叫。 8).使用包含某些操作码的内联汇编。.
) - 函数可以声明为pure,在这种情况下,它们无权读取或者修改状态。(除了上面解释的状态修改语句列表之外,以下内容被认为是从状态中读取的: 1).从状态变量读取 2).访问this.balance或<address> .balance。 3).访问块,tx,msg的任何成员(msg.sig和msg.data除外)。 4).调用任何未标记为pure的函数。 5).使用包含某些操作码的内联汇编。
)
以上是关于solidity基础知识的主要内容,如果未能解决你的问题,请参考以下文章