• 做一个幸福的人,读书,旅行,努力工作,关心身体和心情。
  • 不管有没有人爱,也要努力做一个可爱的人。不埋怨谁,不嘲笑谁,也不羡慕谁,阳光下灿烂,风雨中奔跑,做自己的梦,走自己的路。

智能合约Solidity编程语言

区块链 lcq 来源:智能合约Solidity编程语言 8个月前 (02-04) 1370次浏览 0个评论

本文全部来自《智能合约Solidity编程语言》,我只是做了搬运工搬过来方便我自己查看而已。

Solidity语言

Solidity是一种智能合约高级语言,运行在Ethereum虚拟机(EVM)之上。

Solidity与其它语言相关的特点?

它的语法接近于Javascript,是一种面向对象的语言。但作为一种真正意义上运行在网络上的去中心合约,它又有很多的不同,下面列举一些:

  • 以太坊底层是基于帐户,而非UTXO的,所以有一个特殊的 Address的类型。用于定位用户,定位合约,定位合约的代码(合约本身也是一个帐户)。
  • 由于语言内嵌框架是支持支付的,所以提供了一些关键字,如 payable,可以在语言层面直接支持支付,而且超级简单。
  • 存储是使用网络上的区块链,数据的每一个状态都可以永久存储,所以需要确定变量使用内存,还是区块链。
  • 运行环境是在去中心化的网络上,会比较强调合约或函数执行的调用的方式。因为原来一个简单的函数调用变为了一个网络上的节点中的代码执行,分布式的感觉。
  • 最后一个非常大的不同则是它的异常机制,一旦出现异常,所有的执行都将会被回撤,这主要是为了保证合约执行的原子性,以避免中间状态出现的数据不一致。

Hello Wolrd!

听起来高大上,其实入手玩起来也可以很简单:

通过读取参数输入的新值,并将之累加至合约的变量中,返回发送人的地址,和最终的累计值。

浏览器编译器Remix

使用无需安装的浏览器编译器 Remix 可以立即看到效果。打开后,如下图所示:

输入上述代码,点击Create按钮,就能在浏览器中创建能调用函数的按钮。在 update按钮旁输入入参,点击按钮,就能执行函数调用并打印出函数返回的结果了。

备注:如果出现错误,可以等待浏览器资源加载完成,或强制刷新后再试。

Solidity智能合约文件结构

版本申明

说明:

1 版本要高于0.4才可以编译

2 ^号表示高于0.5的版本则不可编译,第三位的版本号但可以变,留出来用做bug可以修复(如0.4.1的编译器有bug,可在0.4.2修复,现有合约不用改代码)。

引用其它源文件

  • 全局引入 *

  • 自定义命名空间引入 *

分别定义引入

非es6兼容的简写语法

等同于上述

关于路径

引入文件路径时要注意,非 .打头的路径会被认为是绝对路径,所以要引用同目录下的文件使用

也不要使用下述方式,这样会是在一个全局的目录下

为什么会有这个区别,是因为这取决于编译器,如果解析路径,通常来说目录层级结构并不与我们本地的文件一一对应,它非常有可能是通过ipfs,http,或git建立的一个网络上的虚拟目录。

编译器解析引用文件机制

各编译器提供了文件前缀映射机制。

1. 可以将一个域名下的文件映射到本地,从而从本地的某个文件中读取

2. 提供对同一实现的不同版本的支持(可能某版本的实现前后不兼容,需要区分)

3. 如果前缀相同,取最长,

4. 有一个”fallback-remapping”机制,空串会映射到”/usr/local/include/solidify”

solc编译器

命令行编译器,通过下述命令命名空间映射提供支持

上述的 context:=target是可选的。所有 context目录下的以 prefix开头的会被替换为 target
举例来说,如果你将 github.com/ethereum/dapp-bin拷到本地的 /usr/local/dapp-bin,并使用下述方式使用文件

要编译这个文件,使用下述命令:

另一个更复杂的例子,如果你使用一个更旧版本的dapp-bin,旧版本在/url/local/dapp-bin_old,那么,你可以使用下述命令编译

需要注意的是solc仅仅允许包含实际存在的文件。它必须存在于你重映射后目录里,或其子目录里。如果你想包含直接的绝对路径包含,那么可以将命名空间重映射为 =\
备注:如果有多个重映射指向了同一个文件,那么取最长的那个文件。

browser-solidity编译器:

browser-solidity编译器默认会自动映射到github上,然后会自动从网络上检索文件。例如:你可以通过下述方式引入一个迭代包:

备注:未来可能会支持其它的源码方式

代码注释

两种方式,单行( //),多行使用( /*…*/)

示例

文档注释

写文档用。三个斜杠 ////** … */,可使用 Doxygen语法,以支持生成对文档的说明,参数验证的注解,或者是在用户调用这个函数时,弹出来的确认内容。

示例

智能合约源文件的基本要素概览(Structure of a Contract)

  • 合约类似面向对象语言中的类。
  • 支持继承

每个合约中可包含 状态变量(State Variables)函数(Functions), 函数修饰符(Function Modifiers), 事件(Events), 结构类型(Structs Types)枚举类型(Enum Types)

状态变量(State Variables)

变量值会永久存储在合约的存储空间

详情见 类型(Types)章节,关于所有支持的类型和变量相关的可见性(Visibility and Accessors)。

函数(Functions)

智能合约中的一个可执行单元。

示例

上述示例展示了一个简单的加法函数。

函数调用可以设置为内部(Internal)的和外部(External)的。同时对于其它合同的不同级别的可见性和访问控制(Visibility and Accessors)。具体的情况详见后面类型中关于函数的章节。

函数修饰符(Function Modifiers)

函数修饰符用于增强语义。详情见函数修饰符相关章节。

事件(Events)

事件是以太坊虚拟机(EVM)日志基础设施提供的一个便利接口。用于获取当前发生的事件。

示例

关于事件如何声明和使用,详见后面事件相关章节。

结构体类型(Structs Types)

自定义的将几个变量组合在一起形成的类型。详见关于结构体相关章节。

示例

枚举类型

特殊的自定义类型,类型的所有值可枚举的情况。详情见后续相关章节。

示例

值类型与引用类型

由于Solidity是一个静态类型的语言,所以编译时需明确指定变量的类型(包括 本地变量状态变量), Solidity编程语言提供了一些 基本类型(elementary types)可以用来组合成复杂类型。

类型可以与不同运算符组合,支持表达式运算,你可以通过 表达式的执行顺序(Order of Evaluation of Expressions)来了解执行顺序。

值类型(Value Type)

值类型包含

  • 布尔(Booleans)
  • 整型(Integer)
  • 地址(Address)
  • 定长字节数组(fixed byte arrays)
  • 有理数和整型(Rational and Integer LiteralsString literals)
  • 枚举类型(Enums)
  • 函数(Function Types)

为什么会叫 值类型,是因为上述这些类型在传值时,总是值传递 1 。比如在函数传参数时,或进行变量赋值时。

引用类型(Reference Types)

复杂类型,占用空间较大的。在拷贝时占用空间较大。所以考虑通过引用传递。常见的引用类型有:

  • 不定长字节数组(bytes)
  • 字符串(string)
  • 数组(Array)
  • 结构体(Struts)

关于参数传递的相关方式的进一步了解: http://baike.baidu.com/item/参数传递

布尔(Booleans)

bool: 可能的取值为常量值 truefalse.

支持的运算符:

  • !逻辑非
  • && 逻辑与
  • || 逻辑或
  • == 等于
  • != 不等于

备注:运算符 &&||是短路运算符,如 f(x)||g(y),当 f(x)为真时,则不会继续执行 g(y)

整型(Integer)

int/uint:变长的有符号或无符号整型。变量支持的步长以 8递增,支持从 uint8uint256,以及 int8int256。需要注意的是, uintint默认代表的是 uint256int256

支持的运算符:

  • 比较: <=<==!=>=>,返回值为 bool类型。
  • 位运算符: &|,( ^异或),( ~非)。
  • 数学运算: +-,一元运算 +*/,( %求余),( **平方)。

整数除法总是截断的,但如果运算符是字面量,则不会截断(后面会进一步提到)。另外除 0会抛异常 ,我们来看看下面的这个例子:

整数字面量

整数字面量,由包含0-9的数字序列组成,默认被解释成十进制。在 Solidity中不支持八进制,前导 0会被默认忽略,如 0100,会被认为是 100

小数由 .组成,在他的左边或右边至少要包含一个数字。如 1..11.3均是有效的小数。

字面量本身支持任意精度,也就是可以不会运算溢出,或除法截断。但当它被转换成对应的非字面量类型,如整数或小数。或者将他们与非字面量进行运算,则不能保证精度了。

总之来说就是,字面量怎么都计算都行,但一旦转为对应的变量后,再计算就不保证精度啦。

地址(Address)

地址 1

地址: 以太坊地址的长度,大小20个字节,160位,所以可以用一个 uint160编码。地址是所有合约的基础,所有的合约都会继承地址对象,也可以随时将一个地址串,得到对应的代码进行调用。当然地址代表一个普通帐户时,就没有这么多丰富的功能啦。

支持的运算符

  • <=<==!=>=>

地址类型的成员

属性: balance

函数: send()call()delegatecall()callcode()

地址字面量

十六进制的字符串,凡是能通过地址合法性检查(address checksum test) 2 ,就会被认为是地址,如 0xdCad3a6d3569DF655070DEd06cb7A1b2Ccd1D3AF。需要注意的是39到41位长的没有通过地址合法性检查的,会提示一个警告,但会被视为普通的有理数字面量。

balance

通过它能得到一个地址的余额。

我们可以把上述代码放入 remix中,看看效果,参见下面的操作演示:

address_demo

演示中的一个核心要点是,编译后,我们能得到当前合约的地址,并将该地址复制到输入框中,记得录入地址项时要加英文的双引号,否则会报 Error encoding arguments: SyntaxError: JSON Parse error: Expected ']'

this

如果只是想得到当前合约的余额,其实可以这样写:

原因是对于合约来说,地址代表的就是合约本身,合约对象默认继承自地址对象,所以内部有地址的属性。

地址的方法 send()

用来向某个地址发送货币(货币单位是 wei)。

这个合约实现的是充值。 this.send(msg.value)意指向合约自身发送 msg.value量的以太币。 msg.value是合约调用方附带的以太币。

下面是操作演示:

address_demo_2

关于发送者的帐号,发送的以太币数量设置,需切换到Remix的小飞机图标的配置页。要在调用 deposit前在 Value输入项填入要发的以太币数量。在 getBalance时要记得将 value项内填的值去掉,因为 getBalance方法,并不是 payable的,不支持货币 3

send()方法执行时有一些风险

  1. 调用递归深度不能超1024。
  2. 如果 gas不够,执行会失败。
  3. 所以使用这个方法要检查成功与否。或为保险起见,货币操作时要使用一些最佳实践。

如果执行失败,将会回撤所有交易,所以务必留意返回结果。

call()callcode()delegatecall()

为了同一些不支持ABI协议的进行直接交互(一般的 web3.jssoldity都是支持的)。可以使用 call()函数,用来向另一个合约发送原始数据。参数支持任何类型任意数量。每个参数会按规则(规则是按ABI 4 )打包成32字节并一一拼接到一起。

call()方法支持ABI协议 4 定义的函数选择器。如果第一个参数恰好4个字节,在这种情况下,会被认为根据ABI协议定义的函数器指定的函数签名 4 。所以如果你只是想发送消息体,需要避免第一个参数是4个字节。

call方法返回一个 bool值,以表明执行成功还是失败。正常结束返回 true,异常终止返回 false。我们无法解析返回结果,因为这样我们得事前知道返回的数据的编码和数据大小(这里的潜在假设是不知道对方使用的协议格式,所以也不会知道返回的结果如何解析,有点祼协议测试的感觉)。

同样我们也可以使用 delegatecall(),它与 call方法的区别在于,仅仅是代码会执行,而其它方面,如(存储,余额等)都是用的当前的合约的数据。 delegatecall()方法的目的是用来执行另一个合约中的工具库。所以开发者需要保证两个合约中的存储变量能兼容,来保证 delegatecall()能顺利执行。

在homestead阶段之前,仅有一个受限的多样的 callcode()方法可用,但并未提供对 msg.sendermsg.value的访问权限。

上面的这三个方法 call()delegatecall()callcode()都是底层的消息传递调用,最好仅在万不得已才进行使用,因为他们破坏了Solidity的类型安全。

关于 call()函数究竟发的什么消息体,函数选择器究竟怎么用,参见 这个文章的挖掘。


  1. 如果你想了解更多关于地址的由来,UTXO等,可以参考: http://me.tryblockchain.org/Solidity%E7%9A%84%E5%9C%B0%E5%9D%80%E7%B1%BB%E5%9E%8B.html
  2. 为防止录入地址有误,一种格式化地址后来确认地址有效性的方案, https://github.com/ethereum/EIPs/issues/55
  3. 原因详见实现以太币支付的文章, http://me.tryblockchain.org/%E6%94%AF%E4%BB%98%E7%9B%B8%E5%85%B3.html
  4. 关于ABI协议的详细说明: http://me.tryblockchain.org/Solidity-abi-abstraction.html

字节数组(byte arrays)

定长字节数组(Fixed-size byte arrays)

bytes1, … , bytes32,允许值以步长 1递增。 byte默认表示 byte1

运算符

比较: <=<==!=>=>,返回值为 bool类型。

位运算符: &|^(异或), ~

支持序号的访问,与大多数语言一样,取值范围[0, n),其中 n表示长度。

成员变量

.length表示这个字节数组的长度(只读)。

动态大小的字节数组

bytes: 动态长度的字节数组,参见 数组(Arrays)。非值类型 1

string: 动态长度的UTF-8编码的字符类型,参见 数组(Arrays)。非值类型 1

一个好的使用原则是:

  • bytes用来存储任意长度的字节数据, string用来存储任意长度的 UTF-8编码的字符串数据。
  • 如果长度可以确定,尽量使用定长的如 byte1byte32中的一个,因为这样更省空间。

小数

定点数

//文档上称,暂不支持

小数字面量

如果字面量计算的结果不是一个整数,那么将会转换为一个对应的 ufixed,或 fixed类型。 Solidity会选择合适的大小,以能尽量包含小数部分。

例,在 var x = 1 / 4中, x的实际类型是 ufixed0x8。而在 var x = 1/ 3中,类型会是 ufixedox256,因为这个结果表示是无限的,所以他只能是无限接近。

支持的运算符

适用于整型的操作符,同时适用于数字的字面量运算表达式,当操作结果是整数时。如果有任何一方是有理数,将不允许使用位操作符。如果指数是小数,还将不能进行取幂运算。

数字字面量

Solidity对每一个有理数都有一个数值字面量类型。整数字面量和有理数字面量从属于数字面量。所有的数字字面表式的结果都属于数字字面类型。所以 1 + 22 + 1都属于同样的有理数的数字字面类型 3

二进制表示

大多数含小数的十进制,均不可被二进制准确表达,比如 5.3743的类型可能是 ufixed8*248。如果你想使用这样的值,需要明确指定精度 x + ufixed(5.3743),否则会报类型转换错误。

字面量截断

整数上的字面量除法,在早期的版本中是被截断的,但现在可以被转为有理数了,如 5 /2的值为 2.5

字面量转换

数字的字面量表达式,一旦其中含有非字面量表达式,它就会被转为一个非字面量类型。下面代码中表达式的结果将会被认为是一个有理数:

虽然我们知道上述表达式运算的结果将是一个整型,但最终被编译器认为是小数型,所以上述代码编译不能通过。

字符串(String literal)

字符串字面量

字符串字面量是指由单引号,或双引号引起来的字符串。字符串并不像C语言,包含结束符, foo这个字符串大小仅为三个字节。

定长字节数组

正如整数一样,字符串的长度类型可以是变长的。特殊之处在于,可以隐式的转换为 byte1,… byte32。下面来看看这个特性:

上述的字符串字面量,会隐式转换为 bytes3。但这样不是理解成 bytes3的字面量方式一个意思。

转义字符

字符串字面量支持转义字符,比如 \n\xNN\uNNNN。其中 \xNN表式16进制值,最终录入合适的字节。而 \uNNNN表示 Unicode码点值,最终会转换为 UTF8的序列。

十六进制字面量

十六进制字面量,以关键字 hex打头,后面紧跟用单或双引号包裹的字符串。如 hex"001122ff"。在内部会被表示为二进制流。通过下面的例子来理解下是什么意思:

由于一个字节是8位,所以一个 hex是由两个 [0-9a-z]字符组成的。所以 var b = hex"A";不是成双的字符串是会报错的。

转换

十六进制的字面量与字符串可以进行同样的类似操作:

可以发现,它可以隐式的转为 bytes,上述代码的执行结果如下:

枚举

枚举类型是在Solidity中的一种用户自定义类型。他可以显示的转换与整数进行转换,但不能进行隐式转换。显示的转换会在运行时检查数值范围,如果不匹配,将会引起异常。枚举类型应至少有一名成员。我们来看看下面的例子吧。

函数(Function Types)

函数类型 1 即是函数这种特殊的类型。

  • 可以将一个函数赋值给一个变量,一个函数类型的变量。
  • 还可以将一个函数作为参数进行传递。
  • 也可以在函数调用中返回一个函数。

函数类型有两类;可分为 internalexternal函数。

内部函数(internal)

因为不能在当前合约的上下文环境以外的地方执行,内部函数只能在当前合约内被使用。如在当前的代码块内,包括内部库函数,和继承的函数中。

外部函数(External)

外部函数由地址和函数方法签名两部分组成。可作为 外部函数调用的参数,或者由 外部函数调用返回。

函数的定义

完整的函数的定义如下:

若不写类型,默认的函数类型是 internal的。如果函数没有返回结果,则必须省略 returns关键字。下面我们通过一个例子来了解一下。

如果一个函数变量没有初始化,直接调用它将会产生异常。如果 delete了一个函数后调用,也会发生同样的异常。

如果 外部函数类型在 Solidity的上下文环境以外的地方使用,他们会被视为 function类型。编码为20字节的函数所在地址,紧跟4字节的函数方法签名 2 的共占24字节的 bytes24类型。

合约中的 public的函数,可以使用 internalexternal两种方式来调用。下面来看看,两种方式的不同之处。

函数的 internalexternal

调用一个函数 f()时,我们可以直接调用 f(),或者使用 this.f()。但两者有一个区别。前者是通过 internal的方式在调用,而后者是通过 external的方式在调用。请注意,这里关于 this的使用与大多数语言相背。下面通过一个例子来了解他们的不同:

函数例子(官方)

Question?

  • library是什么呢。
  • library引入时为什么使用 using,这和文件引入的 import有何区别。
  • library内的函数全是 internal的。
  • library内的函数,他的参数函数为什么是 internal的,不应该是 external的?
  • uint[]是什么类型,不能写做 []uint
  • memory又是什么呢,为什么 map函数明明是两个参数,但只需要传一个呢。
  • http://solidity.readthedocs.io/en/develop/types.html#function-types
  • 函数签名的编码方式可查看函数选择器相关章节, 【文档翻译系列】ABI详解

引用类型(Reference Types)

复杂类型。不同于之前值类型,复杂类型占的空间更大,超过256字节,因为拷贝它们占用更多的空间。由此我们需要考虑将它们存储在什么位置 内存(memory,数据不是永久存在的)存储(storage,值类型中的状态变量)

后面我们会讲到常见的引用类型。

数据位置(Data location)

复杂类型,如 数组(arrays)数据结构(struct)在Solidity中有一个额外的属性,数据的存储位置。可选为 memorystorage

memory存储位置同我们普通程序的内存一致。即分配,即使用,越过作用域即不可被访问,等待被回收。而在区块链上,由于底层实现了图灵完备,故而会有非常多的状态需要永久记录下来。比如,参与众筹的所有参与者。那么我们就要使用 storage这种类型了,一旦使用这个类型,数据将永远存在。

基于程序的上下文,大多数时候这样的选择是默认的,我们可以通过指定关键字 storagememory修改它。

默认的函数参数,包括返回的参数,他们是 memory。默认的局部变量是 storage 1 。而默认的状态变量(合约声明的公有变量)是 storage

另外还有第三个存储位置 calldata。它存储的是函数参数,是只读的,不会永久存储的一个数据位置。 外部函数的参数(不包括返回参数)被强制指定为 calldata。效果与 memory差不多。

数据位置指定非常重要,因为不同数据位置变量赋值产生的结果也不同。在 memorystorage之间,以及它们和 状态变量(即便从另一个状态变量)中相互赋值,总是会创建一个完全不相关的拷贝。

将一个 storage的状态变量,赋值给一个 storage的局部变量,是通过引用传递。所以对于局部变量的修改,同时修改关联的状态变量。但另一方面,将一个 memory的引用类型赋值给另一个 memory的引用,不会创建另一个拷贝。

下面来看下官方的例子说明:

总结

强制的数据位置(Forced data location)

  • 外部函数(External function)的参数(不包括返回参数)强制为: calldata
  • 状态变量(State variables)强制为: storage

默认数据位置(Default data location)

  • 函数参数(括返回参数: memory
  • 所有其它的局部变量: storage

更多请查看关于数据位置的进一步挖掘: http://me.tryblockchain.org/solidity-data-location.html

参考资料

本地变量但是为什么是 stroage的,没有太想明白。 http://ethereum.stackexchange.com/questions/9781/solidity-default-data-location-for-local-vars

数组

数组可以声明时指定长度,或者是变长的。对 storage 1 的数组来说,元素类型可以是任意的,类型可以是数组,映射类型,数据结构等。但对于 memory 1 的数组来说。如果函数是对外可见的 2 ,那么函数参数不能是映射类型的数组,只能是支持ABI的类型 3

一个类型为T,长度为k的数组,可以声明为 T[k],而一个变长的数组则声明为 T[]
你还可以声明一个多维数据,如一个类型为 uint的数组长度为5的变长数组,可以声明为 uint[][5] x。需要留心的是,相比非区块链语言,多维数组的长度声明是反的。

要访问第三个动态数据的,第二个元素,使用 x[2][1]。数组的序号是从0开始的,序号顺序与定义相反。

bytesstring是一种特殊的数组。 bytes类似 byte[],但在外部函数作为参数调用中,会进行压缩打包,更省空间,所以应该尽量使用 bytes 4 string类似 bytes,但不提供长度和按序号的访问方式。

由于 bytesstring,可以自由转换,你可以将字符串 s通过 bytes(s)转为一个 bytes。但需要注意的是通过这种方式访问到的是UTF-8编码的码流,并不是独立的一个个字符。比如中文编码是多字节,变长的,所以你访问到的很有可能只是其中的一个代码点。

类型为数组的状态变量,可以标记为 public类型,从而让 Solidity创建一个访问器,如果要访问数组的某个元素,指定数字下标就好了。

创建一个数组

可使用 new关键字创建一个 memory的数组。与 stroage数组不同的是,你不能通过 .length的长度来修改数组大小属性。我们来看看下面的例子:

在上面的代码中, f()方法尝试调整数组 a的长度,编译器报错 Error: Expression has to be an lvalue.。但在 g()方法中我们看到可以修改 5

字面量及内联数组

数组字面量,是指以表达式方式隐式声明一个数组,并作为一个数组变量使用的方式。下面是一个简单的例子:

通过数组字面量,创建的数组是 memory的,同时还是定长的。元素类型则是使用刚好能存储的元素的能用类型,比如代码里的 [1, 2, 3],只需要 uint8即可存储。由于 g()方法的参数需要的是 uint(默认的 uint表示的其实是 uint256),所以要使用 uint(1)来进行类型转换。

还需注意的一点是,定长数组,不能与变长数组相互赋值,我们来看下面的代码:

限制的主要原因是,ABI不能很好的支持数组,已经计划在未来移除这样的限制。(当前的ABI接口,不是已经能支持数组了?)

数组的属性和方法

length属性

数组有一个 .length属性,表示当前的数组长度。 storage的变长数组,可以通过给 .length赋值调整数组长度。 memory的变长数组不支持。

不能通过访问超出当前数组的长度的方式,来自动实现上面说的这种情况。 memory数组虽然可以通过参数,灵活指定大小,但一旦创建,大小不可调整,对于变长数组,可以通过参数在编译期指定数组大小。

push方法

storage的变长数组和 bytes都有一个 push(),用于附加新元素到数据末端,返回值为新的长度。

限制的情况

当前在外部函数中,不能使用多维数组。

另外,基于EVM的限制,不能通过外部函数返回动态的内容。

在上面的例子中,通过web.js调用能返回数据,但在Solidity中不能返回数据。一种临时的解决办法,是使用一个非常大的静态数组。

更多请查看这里的重新梳理: http://me.tryblockchain.org/solidity-array.html

参考资料

变量声明时定义的数据位置属性。可以是memory的,也可以为storage的,详见: http://me.tryblockchain.org/data-location.org

函数的可见性说明,详见这里: http://me.tryblockchain.org/solidity-function-advanced1.html

了解更多关于ABI的信息, https://github.com/ethereum/wiki/wiki/Ethereum-Contract-ABI#types

因为字节数组在使用时会padding,所以不节省空间。 https://github.com/chriseth/solidity/blob/48588b5c4f21c8bb7c5d4319b05a93ee995b457d/_docs/faq_basic.md#what-is-the-difference-between-bytes-and-byte

http://stackoverflow.com/questions/33839122/in-ethereum-solidity-when-changing-an-array-length-i-get-value-must-be-an-lva

结构体(struct)

Solidity提供 struct来定义自定义类型。我们来看看下面的例子:

上面的代码向我们展示的一个简化版的众筹项目,其实包含了一些 struct的使用。 struct可以用于映射和数组中作为元素。其本身也可以包含映射和数组等类型。

我们不能声明一个 struct同时将这个 struct作为这个struct的一个成员。这个限制是基于结构体的大小必须是有限的。

虽然数据结构能作为一个 mapping的值,但数据类型不能包含它自身类型的成员,因为数据结构的大小必须是有限的。

需要注意的是在函数中,将一个 struct赋值给一个局部变量(默认是storage类型),实际是拷贝的引用,所以修改局部变量值时,会影响到原变量。

当然,你也可以直接通过访问成员修改值,而不用一定赋值给一个局部变量,如 campaigns[comapingnId].amount = 0

映射/字典(mappings)

映射或字典类型,一种键值对的映射关系存储结构。定义方式为 mapping(_KeyType => _KeyValue)。键的类型允许除 映射外的所有类型,如数组,合约,枚举,结构体。值的类型无限制。

映射可以被视作为一个哈希表,其中所有可能的键已被虚拟化的创建,被映射到一个默认值(二进制表示的零)。但在映射表中,我们并不存储键的数据,仅仅存储它的 keccak256哈希值,用来查找值时使用。

因此, 映射并没有长度,键集合(或列表),值集合(或列表)这样的概念。

映射类型,仅能用来定义 状态变量,或者是在内部函数中作为 storage类型的引用。引用是指你可以声明一个,如 var storage mappVal的用于存储状态变量的引用的对象,但你没办法使用非状态变量来初始化这个引用。

可以通过将 映射标记为 public,来让Solidity创建一个访问器。要想访问这样的 映射,需要提供一个键值做为参数。如果 映射的值类型也是 映射,使用访问器访问时,要提供这个 映射值所对应的键,不断重复这个过程。下面来看一个例子:

由于调试时,你不一定方便知道自己的发起地址,所以把这个函数,略微调整了一下,以在调用时,返回调用者的地址。编译上述合同后,可以先调用 update(),执行成功后,查看调用信息,能看到你更新的地址,这样再查一下这个地址的在映射里存的值。

如果你想通过合约进行上述调用。

映射并未提供迭代输出的方法,可以自行实现一个数据结构。参见 iterable mapping

左值的相关运算符

左值 1 ,是指位于表达式左边的变量,可以是与操作符直接结合的形成的,如自增,自减;也可以是赋值,位运算。

可以支持操作符有: -=, +=, *=, %=, |=, &=, ^=, ++, --

特殊的运算符delete

delete运算符,用于将某个变量重置为初始值。对于整数,运算符的效果等同于 a = 0。而对于定长数组,则是把数组中的每个元素置为初始值,变长数组则是将长度置为0。对于结构体,也是类似,是将所有的成员均重置为初始值。

delete对于映射类型几乎无影响,因为键可能是任意的,且往往不可知。所以如果你删除一个结构体,它会递归删除所有非mapping的成员。当然,你是可以单独删除映射里的某个键,以及这个键映射的某个值。

需要强调的是 delete a的行为更像赋值,为 a赋予一个新对象。我们来看看下文的示例:

通过上面的代码,我们可以看出,对于值类型,是值传递,删除 x不会影响到 data,同样的删除 data也不会影响到 x。因为他们都存了一份原值的拷贝。

而对于复杂类型略有不同,复杂类型在赋值时使用的是引用传递。删除会影响所有相关变量。比如上述代码中,删除 dataArray同样会影响到 y

由于 delete的行为更像是赋值操作,所以不能在上述代码中执行 delete y,因为不能对一个storage的引用赋值 2

参考资料

http://solidity.readthedocs.io/en/develop/types.html#operators-involving-lvalues

strorage的引用不能赋值原因,可以看看关于数据位置的这篇文章: http://me.tryblockchain.org/solidity-data-location.html。

基本类型间的转换

语言中经常会出现类型转换 1 。如将一个数字字符串转为整型,或浮点数。这种转换常常分为,隐式转换和显式转换。

隐式转换

如果运算符支持两边不同的类型,编译器会尝试隐式转换类型,同理,赋值时也是类似。通常,隐式转换需要能保证不会丢失数据,且语义可通。如 uint8可以转化为 uint16, uint256。但 int8不能转为 uint256,因为 uint256不能表示 -1

此外,任何无符号整数,可以转换为相同或更大大小的字节值。比如,任何可以转换为 uint160的,也可以转换为 address

显式转换

如果编译器不允许隐式的自动转换,但你知道转换没有问题时,可以进行强转。需要注意的是,不正确的转换会带来错误,所以你要进行谨慎的测试。

如果转换为一个更小的类型,高位将被截断。

参考资料

http://solidity.readthedocs.io/en/develop/types.html#conversions-between-elementary-types

类型推断(Type Deduction)

为了方便,并不总是需要明确指定一个变量的类型,编译器会通过第一个向这个对象赋予的值的类型来进行推断 1

函数的参数,包括返回参数,不可以使用 var这种不指定类型的方式。

需要特别注意的是,由于类型推断是根据第一个变量进行的赋值。所以代码 for (var i = 0; i < 2000; i++) {}将是一个无限循环,因为一个 uint8i的将小于 2000

参考资料

http://solidity.readthedocs.io/en/develop/types.html#type-deduction

货币单位(Ether Units)

一个字面量的数字,可以使用后缀 wei, finney, szaboether来在不同面额中转换。不含任何后缀的默认单位是 wei。如 2 ether == 2000 finney的结果是 true

时间单位(Time Units)

seconds, minutes, hours, days, weeks, years均可做为后缀,并进行相互转换,默认是 seconds为单位。

默认规则如下:

  • 1 == 1 seconds
  • 1 minutes == 60 seconds
  • 1 hours == 60 minutes
  • 1 days == 24 hours
  • 1 weeks = 7 days
  • 1 years = 365 days

如果你需要进行使用这些单位进行日期计算,需要特别小心,因为不是每年都是365天,且并不是每天都有24小时,因为还有闰秒。由于无法预测闰秒,必须由外部的oracle来更新从而得到一个精确的日历库(内部实现一个日期库也是消耗gas的)。

后缀不能用于变量。如果你想对输入的变量说明其不同的单位,可以使用下面的方式。

特殊变量及函数(Special Variables and Functions)

有一些变量和函数存在于¥全局上下文中。主要用来提供一些区块链当前的信息。

区块和交易的属性(Block And Transaction Properties)

  • block.blockhash(uint blockNumber) returns (bytes32),给定区块号的哈希值,只支持最近256个区块,且不包含当前区块。
  • block.coinbase (address) 当前块矿工的地址。
  • block.difficulty (uint)当前块的难度。
  • block.gaslimit (uint)当前块的 gaslimit
  • block.number (uint)当前区块的块号。
  • block.timestamp (uint)当前块的时间戳。
  • msg.data (bytes)完整的调用数据(calldata)。
  • msg.gas (uint)当前还剩的 gas
  • msg.sender (address)当前调用发起人的地址。
  • msg.sig (bytes4)调用数据的前四个字节(函数标识符)。
  • msg.value (uint)这个消息所附带的货币量,单位为 wei
  • now (uint)当前块的时间戳,等同于 block.timestamp
  • tx.gasprice (uint) 交易的 gas价格。
  • tx.origin (address)交易的发送者(完整的调用链)

msg的所有成员值,如 msg.sender, msg.value的值可以因为每一次外部函数调用,或库函数调用发生变化(因为 msg就是和调用相关的全局变量)。

如果你想在库函数中,用 msg.sender实现访问控制,你需要将 msg.sender做为参数(就是说不能使用默认的 msg.value,因为它可能被更改)。

为了可扩展性的原因,你只能查最近256个块,所有其它的将返回0.

数学和加密函数(Mathematical and Cryptographic Functions)

asser(bool condition):

如果条件不满足,抛出异常。

addmod(uint x, uint y, uint k) returns (uint):

计算 (x + y) % k。加法支持任意的精度。但不超过(wrap around?) 2**256

mulmod(uint x, uint y, uint k) returns (uint):

计算 (x * y) % k。乘法支持任意精度,但不超过(wrap around?) 2**256

keccak256(...) returns (bytes32):

使用以太坊的(Keccak-256)计算HASH值。紧密打包。

sha3(...) returns (bytes32):

等同于 keccak256()。紧密打包。

sha256(...) returns (bytes32):

使用SHA-256计算HASH值。紧密打包。

ripemd160(...) returns (bytes20):

使用RIPEMD-160计算HASH值。紧密打包。

ecrecover(bytes32 hash, uint8 v, bytes32 r, bytes32 s) returns (address):

通过签名信息恢复非对称加密算法公匙地址。如果出错会返回0,附录提供了一个例子 1 .

revert()

取消执行,并回撤状态变化。

需要注意的是参数是”紧密打包(tightly packed)”的,意思是说参数不会补位,就直接连接在一起的。下面来看一个例子:

上述例子中,三种表达方式都是一致的。

如果需要补位,需要明确的类型转换,如 keccak256("\x00\x12")等同于 keccak256(uint16(0x12))

需要注意的是字面量会用,尽可能小的空间来存储它们。比如, keccak256(0) == keccak256(uint8(0))keccak256(0x12345678) == keccak256(uint32(0x12345678))

私链(private blockchain)上运行 sha256, ripemd160ecrecover可能会出现 Out-Of-Gas报错。因为它们实现了一种预编译的机制,但合约要在收到第一个消息后才会存在。向一个不存在的合约发送消息,非常昂贵,所以才会导致 Out-Of-Gas的问题。一种解决办法是每个在你真正使用它们前,先发送 1 wei到这些合约上来完成初始化。在官方和测试链上没有这个问题。

参考资料

使用ecrecover实现签名检验的例子。 http://me.tryblockchain.org/web3js-sign-ecrecover-decode.html

地址相关(Address Related)

<address>.balance (uint256):Address的余额,以 wei为单位。

<address>.transfer(uint256 amount):发送给定数量的 ether,以 wei为单位,到某个地址。失败时抛出异常。

<address>.send(uint256 amount) returns (bool):发送给定数量的 ether,以 wei为单位,到某个地址。失败时返回 false

<address>.call(…) returns (bool):发起底层的 call调用。失败时返回 false

<address>.callcode(…) returns (bool):发起底层的 callcode调用,失败时返回 false

<address>.delegatecall(…) returns (bool):发起底层的 delegatecall调用,失败时返回 false

更多信息参加Address章节 1

使用 send方法需要注意,调用栈深不能超过1024,或gas不足,都将导致发送失败。使用为了保证你的 ether安全,要始终检查返回结果。当用户取款时,使用 transfer或使用最佳实践的模式 2

合约相关

this(当前合约的类型)

当前合约的类型,可以显式的转换为 Address

selfdestruct(address recipt):

销毁当前合约,并把它所有资金发送到给定的地址。

另外,当前合约里的所有函数均可支持调用,包括当前函数本身。

参考资料

更新关于地址的call,callcode,delegateCall的介绍。 http://me.tryblockchain.org/Solidity-call-callcode-delegatecall.html

http://solidity.readthedocs.io/en/develop/common-patterns.html#withdrawal-from-contracts

入参和出参(Input Parameters and Output Parameters)

javascript一样,函数有输入参数,但与之不同的是,函数可能有任意数量的返回参数。

入参(Input Parameters)

入参(Input Parameter)与变量的定义方式一致,稍微不同的是,不会用到的参数可以省略变量名称。一种可接受两个整型参数的函数如下:

出参(Output Parameters)

出参(Output Paramets)returns关键字后定义,语法类似变量的定义方式。下面的例子展示的是,返回两个输入参数的求和,乘积的实现:

出参的的名字可以省略。返回的值,同样可以通过 return关键字来指定。 return也可以同时返回多个值,参见 Returning Multiple Values。出参的默认值为0,如果没有明确被修改,它将一直是0。

入参和出参也可在函数体内用做表达式。它们也可被赋值。

返回多个值(Returning Multiple Values)

当返回多个参数时,使用 return (v0, v1, ..., vn)。返回结果的数量需要与定义的一致。

控制结构

不支持 switchgoto,支持 ifelsewhiledoforbreakcontinuereturn?:

条件判断中的括号不可省略,但在单行语句中的大括号可以省略。

需要注意的是,这里没有像C语言,和 javascript里的非Boolean类型的自动转换,比如 if(1){...}在Solidity中是无效的。

函数调用(Function Calls)

内部函数调用(Internal Function Calls)

在当前的合约中,函数可以直接调用(内部调用方式),包括也可递归调用,来看一个简单的示例:

这些函数调用在EVM中被翻译成简单的跳转指令。这样带来的一个好处是,当前的内存不会被回收。所以在一个内部调用时传递一个内存型引用效率将非常高。当然,仅仅是同一个合约的函数之间才可通过内部的方式进行调用。

外部函数调用(External Function Calls)

表达式 this.g(8);c.g(2)(这里的 c是一个合约实例)是外部调用函数的方式。实现上是通过一个消息调用,而不是直接通过EVM的指令跳转。需要注意的是,在合约的构造器中,不能使用 this调用函数,因为当前合约还没有创建完成。

其它合约的函数必须通过外部的方式调用。对于一个外部调用,所有函数的参数必须要拷贝到内存中。

当调用其它合约的函数时,可以通过选项 .value(),和 .gas()来分别指定,要发送的 ether量(以 wei为单位),和 gas值。

上面的代码中,我们首先调用 deposit()Consumer合约存入一定量的 ether。然后调用 callFeed()通过 value(1)的方式,向 InfoFeed合约的 info()函数发送1 ether。需要注意的是,如果不先充值,由于合约余额为0,余额不足会报错 Invalid opcode 1

InfoFeed.info()函数,必须使用 payable关键字,否则不能通过 value()选项来接收 ether

代码 InfoFeed(addr)进行了一个显示的类型转换,声明了我们确定知道给定的地址是 InfoFeed类型。所以这里并不会执行构造器的初始化。显示的类型强制转换,需要极度小心,不要尝试调用一个你不知道类型的合约。

我们也可以使用 function setFeed(InfoFeed _feed) { feed = _feed; }来直接进行赋值。 .info.value(1).gas(8000)只是本地设置发送的数额和gas值,真正执行调用的是其后的括号 .info.value(1).gas(8000)()

如果被调用的合约不存在,或者是不包代码的帐户,或调用的合约产生了异常,或者gas不足,均会造成函数调用发生异常。

如果被调用的合约源码并不事前知道,和它们交互会有潜在的风险。当前合约会将自己的控制权交给被调用的合约,而对方几乎可以做任何事。即使被调用的合约是继承自一个已知的父合约,但继承的子合约仅仅被要求正确实现了接口。合约的实现,可以是任意的内容,由此会有风险。另外,准备好处理调用你自己系统中的其它合约,可能在第一调用结果未返回之前就返回了调用的合约。某种程度上意味着,被调用的合约可以改变调用合约的 状态变量(state variable)来标记当前的状态。如,写一个函数,只有当 状态变量(state variables)的值有对应的改变时,才调用外部函数,这样你的合约就不会有可重入性漏洞。

命名参数调用和匿名函数参数(Named Calls and Anonymous Function Paramters)

函数调用的参数,可以通过指定名字的方式调用,但可以以任意的顺序,使用方式是 {}包含。但参数的类型和数量要与定义一致。

省略函数名称(Omitted Function Parameter Names)

没有使用的参数名可以省略(一般常见于返回值)。这些名字在栈(stack)上存在,但不可访问。

参考资料

如何使用 Remix向合约发送ether参见这里。 http://me.tryblockchain.org/%E6%94%AF%E4%BB%98%E7%9B%B8%E5%85%B3.html

创建合约实例(Creating Contracts via new)

一个合约可以通过 new关键字来创建一个合约。要创建合约的完整代码,必须提前知道,所以递归创建依赖是不可能的。

从上面的例子可以看出来,可以在创建合约中,发送 ether,但不能限制gas的使用。如果创建因为 out-of-stack,或无足够的余额以及其它任何问题,会抛出一个异常。

表达式的执行顺序(Order of Evaluation of Expressions)

表达式的求值顺序并不是确定的,更正式的说法是,表达式树一个节点的某个节点在求值时的顺序是不确定的,但是它肯定会比节点本身先执行。我们仅仅保证语句(statements)按顺序执行和布尔表达式的短路运算的支持。查看 运算符的执行顺序了解更多。

赋值(Assignment)

解构赋值和返回多个结果(Destructing Assignments and Returning Multip Values)

Solidity内置支持 元组(tuple),也就是说支持一个可能的完全不同类型组成的一个列表,数量上是固定的(Tuple一般指两个,还有个Triple一般指三个)。

这种内置结构可以同时返回多个结果,也可用于同时赋值给多个变量。

数组和自定义结构体的复杂性(Complication for Arrays and Struts)

对于非值类型,比如数组和数组,赋值的语法有一些复杂。

  • 赋值给一个状态变量总是创建一个完全无关的拷贝。
  • 赋值给一个局部变量,仅对基本类型,如那些32字节以内的 静态类型(static types),创建一份完全无关拷贝。
  • 如果是数据结构或者数组(包括 bytesstring)类型,由状态变量赋值为一个局部变量,局部变量只是持有原始状态变量的一个引用。对这个局部变量再次赋值,并不会修改这个状态变量,只是修改了引用。但修改这个本地引用变量的成员值,会改变状态变量的值。

作用范围和声明(Scoping And Decarations)

一个变量在声明后都有初始值为字节表示的全0值。也就是所有类型的默认值是典型的 零态(zero-state)。举例来说,默认的 bool的值为 false, uintint的默认值为 0

对从 byte1byte32定长的字节数组,每个元素都被初始化为对应类型的初始值(一个字节的是一个字节长的全0值,多个字节长的是多个字节长的全零值)。对于变长的数组 bytesstring,默认值则为空数组和空字符串。

函数内定义的变量,在整个函数中均可用,无论它在哪里定义)。因为Solidity使用了 javascript的变量作用范围的规则。与常规语言语法从定义处开始,到当前块结束为止不同。由此,下述代码编译时会抛出一个异常, Identifier already declared

另外的,如果一个变量被声明了,它会在函数开始前被初始化为默认值。所以下述例子是合法的。

异常(Excepions)

有一些情况下,异常是自动抛出来的(见下),你也可以使用 throw来手动抛出一个异常。抛出异常的效果是当前的执行被终止且被撤销(值的改变和帐户余额的变化都会被回退)。异常还会通过Solidity的函数调用向上冒泡(bubbled up)传递。( send,和底层的函数调用 call, delegatecallcallcode是一个例外,当发生异常时,这些函数返回 false)。

捕捉异常是不可能的(或许因为异常时,需要强制回退的机制)。

在下面的例子中,我们将如展示如何使用 throw来回退转帐,以及演示如何检查 send的返回值。

当前,Solidity在下述场景中自动产生运行时异常。

  1. 如果越界,或是负的序号值访问数组。
  2. 如果访问一个定长的 bytesN,序号越界,或是负的序号值。
  3. 如果你通过消息调用一个函数,但在调用的过程中,并没有正确结束(gas不足,没有匹配到对应的函数,或他自己出现异常)。底层操作如 call, send, delegatecallcallcode除外,它们不会抛出异常,但它们会通过返回 false来表示失败。
  4. 如果在使用 new创建一个新合约时,但合约的初化化由于类似3中的原因没有正常完成。
  5. 被除数为0。
  6. 对一个二进制移动一个负的值。
  7. 使用枚举时,将过大值,负值转为枚举类型。
  8. 使用外部函数调用时,被调用的对象并不包含代码。
  9. 如果你的 public的函数在没有 payable关键字时,却尝试在接收 ether(包括构造函数,和回退函数)。
  10. 合约通过一个 publicgetter函数(public getter funciton)接收 ether
  11. 调用一个未初始化的内部函数。
  12. .transfer()执行失败
  13. assert返回 false

当一个用户通过下述方式触发一个异常:

  1. 调用 throw
  2. 调用 require,但参数值为false。

当上述情况发生时,在Solidity会执行一个回退操作(指令 0xfd)。与之相对的是,如果发生运行时异常,或 assert失败时,将执行无效操作(指令 0xfe)。在上述的情况下,由此促使EVM撤回所有的状态改变。这样做的原因是,没有办法继续安全执行了,因为想要发生的事件并未发生。因为我们想保持交易的原子性(一致性),所以撤销所有操作,让整个交易没有任何影响。

通过 assert判断内部条件是否达成, require验证输入的有效性。这样的分析工具,可以假设正确的输入,减少错误。这样无效的操作码将永远不会出现。

内联汇编(Inline Assembly)

为了增强对语言的细粒度的控制,特别是在写通用库时,可以在一个语言中交错使用Solidity的语句来接近其中一个虚拟机。但由于EVM是基于栈执行的,所以有时很难定位到正确的栈槽位,从而提供正确的的参数或操作码。Solidit的内联汇编尝试解决这个问题,但也引入了其它的问题,当你通过下述特性进行手动的汇编时:

  • 函数式的操作码: mul(1, add(2, 3))代替 push1 3 push1 2 add push1 1 mul
  • 本地汇编变量: let x := add(2, 3) let y := mload(0x40) x := add(x, y)
  • 访问外部变量: function f(uint x){ assembly { x := sub(x,1)}}
  • 标签支持: let x := 10 repeat := sub(x, 1) jumpi(repeat, eq(x, 0))

Solidity Assembly是对内联汇编的详细介绍。

合约

Solidity中合约有点类似面向对象语言中的类。合约中有用于数据持久化的 状态变量(state variables),和可以操作他们的函数。调用另一个合约实例的函数时,会执行一个EVM函数调用,这个操作会切换执行时的上下文,这样,前一个合约的 状态变量(state variables)就不能访问了。

创建合约

合约可以通过Solidity,或不通过Solidity创建。当合约创建时,一个和合约同名的函数(构造器函数)会调用一次,用于初始化。

构造器函数是可选的。仅能有一个构造器,所以不支持重载。

如果不通过Solidity,我们可以通过 web3.js,使用JavaScript的API来完成合约创建:

具体内部实现里,构造器的参数是紧跟在合约代码的后面,但如果你使用 web3.js,可以不用关心这样的细节。

如果一个合约要创建另一个合约,它必须要知道源码。这意味着循环创建依赖是不可能的。

可见性或权限控制(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是在类型后面,函数是在参数列表和返回关键字中间。来看一个定义的例子:

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

访问函数(Getter Functions)

编译器为自动为所有的 public的状态变量创建访问函数。下面的合约例子中,编译器会生成一个名叫 data的无参,返回值是 uint的类型的值 data。状态变量的初始化可以在定义时完成。

访问函数有外部(external)可见性。如果通过内部(internal)的方式访问,比如直接访问,你可以直接把它当一个变量进行使用,但如果使用外部(external)的方式来访问,如通过 this.,那么它必须通过函数的方式来调用。

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故而报错。

下面是一个更加复杂的例子:

文档中自带的的这个Demo始终跑不通,数组类型这里不知为何会抛 invalid jump。把这块简化了写了一个 ComplextSimple供参考。

需要注意的是 publicmapping默认访问参数是需要参数的,并不是之前说的访问函数都是无参的。

mapping类型的数据访问方式变为了 data[arg1][arg2][arg3].a

结构体(struct)里的 mapping初始化被省略了,因为并没有一个很好的方式来对键赋值。

函数修改器(Function Modifiers)

修改器(Modifiers)可以用来轻易的改变一个函数的行为。比如用于在函数执行前检查某种前置条件。修改器是一种合约属性,可被继承,同时还可被派生的合约 重写(override)。下面我们来看一段示例代码:

修改器可以被继承,使用将 modifier置于参数后,返回值前即可。

特殊 _表示使用修改符的函数体的替换位置。

从合约 Register可以看出全约可以多继承,通过 ,号分隔两个被继承的对象。

修改器也是可以接收参数的,如 pricedcosts

使用修改器实现的一个防重复进入的例子。

例子中,由于 call()方法有可能会调回当前方法,修改器实现了防重入的检查。

如果同一个函数有多个修改器,他们之间以空格隔开,修饰器会依次检查执行。

在修改器中和函数体内的显式的 return语句,仅仅跳出当前的修改器和函数体。返回的变量会被赋值,但整个执行逻辑会在前一个修改器后面定义的”_”后继续执行。

修改器的参数可以是任意表达式。在对应的上下文中,所有的函数中引入的符号,在修改器中均可见。但修改器中引入的符号在函数中不可见,因为它们有可能被重写。

常量(constant state variables)

状态变量可以被定义为 constant,常量。这样的话,它必须在编译期间通过一个表达式赋值。赋值的表达式不允许:

1)访问 storage

2)区块链数据,如 nowthis.balanceblock.number

3)合约执行的中间数据,如 msg.gas

4)向外部合约发起调用。也许会造成内存分配副作用表达式是允许的,但不允许产生其它内存对象的副作用的表达式。内置的函数 keccak256keccak256ripemd160ecrecoveraddmodmulmod可以允许调用,即使它们是调用的外部合约。

允许内存分配,从而带来可能的副作用的原因是因为这将允许构建复杂的对象,比如,查找表。虽然当前的特性尚未完整支持。

编译器并不会为常量在 storage上预留空间,每个使用的常量都会被对应的常量表达式所替换(也许优化器会直接替换为常量表达式的结果值)。

不是所有的类型都支持常量,当前支持的仅有值类型和字符串。

常函数(Constant Functions)

函数也可被声明为常量,这类函数将承诺自己不修改区块链上任何状态。

访问器(Accessor)方法默认被标记为 constant。当前编译器并未强制一个 constant的方法不能修改状态。但建议大家对于不会修改数据的标记为 constant

回退函数(fallback function)

每一个合约有且仅有一个没有名字的函数。这个函数无参数,也无返回值。如果调用合约时,没有匹配上任何一个函数(或者没有传哪怕一点数据),就会调用默认的回退函数。

此外,当合约收到 ether时(没有任何其它数据),这个函数也会被执行。在此时,一般仅有少量的gas剩余,用于执行这个函数(准确的说,还剩2300gas)。所以应该尽量保证回退函数使用少的gas。

下述提供给回退函数可执行的操作会比常规的花费得多一点。

  • 写入到存储(storage)
  • 创建一个合约
  • 执行一个外部(external)函数调用,会花费非常多的gas
  • 发送 ether

请在部署合约到网络前,保证透彻的测试你的回退函数,来保证函数执行的花费控制在2300gas以内。

一个没有定义一个回退函数的合约。如果接收ether,会触发异常,并返还ether(solidity v0.4.0开始)。所以合约要接收ether,必须实现回退函数。下面来看个例子:

在浏览器中跑的话,记得要先存ether。

事件(Events)

事件是使用EVM日志内置功能的方便工具,在DAPP的接口中,它可以反过来调用Javascript的监听事件的回调。

事件在合约中可被继承。当被调用时,会触发参数存储到交易的日志中(一种区块链上的特殊数据结构)。这些日志与合约的地址关联,并合并到区块链中,只要区块可以访问就一直存在(至少Frontier,Homestead是这样,但Serenity也许也是这样)。日志和事件在合约内不可直接被访问,即使是创建日志的合约。

日志的SPV(简单支付验证)是可能的,如果一个外部的实体提供了一个这样证明的合约,它可以证明日志在区块链是否存在。但需要留意的是,由于合约中仅能访问最近的256个区块哈希,所以还需要提供区块头信息。

可以最多有三个参数被设置为 indexed,来设置是否被索引。设置为索引后,可以允许通过这个参数来查找日志,甚至可以按特定的值过滤。

如果数组(包括 stringbytes)类型被标记为索引项,会用它对应的 Keccak-256哈希值做为 topic

除非是匿名事件,否则事件签名(比如: Deposit(address,hash256,uint256))是其中一个 topic,同时也意味着对于匿名事件无法通过名字来过滤。

所有未被索引的参数将被做为日志的一部分被保存起来。

被索引的参数将不会保存它们自己,你可以搜索他们的值,但不能检索值本身。

下面是一个简单的例子:

下述是使用javascript来获取日志的例子。

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

通过函数 log0log1log2log3log4,可以直接访问底层的日志组件。 logi表示总共有带 i + 1个参数( i表示的就是可带参数的数目,只是是从0开始计数的)。其中第一个参数会被用来做为日志的数据部分,其它的会做为主题(topics)。前面例子中的事件可改为如下:

其中的长16进制串是事件的签名,计算方式是 keccak256("Deposit(address,hash256,uint256)")

更多的理解事件的资源

继承(Inheritance)

Solidity通过复制包括多态的代码来支持多重继承。

所有函数调用是 虚拟(virtual)的,这意味着最远的派生方式会被调用,除非明确指定了合约。

当一个合约从多个其它合约那里继承,在区块链上仅会创建一个合约,在父合约里的代码会复制来形成继承合约。

基本的继承体系与 python有些类似,特别是在处理多继承上面。

下面用一个例子来详细说明:

上面的例子的 named合约的 kill()方法中,我们调用了 motal.kill()调用父合约的 销毁函数(destruction)。但这样可能什么引发一些小问题。

Final.kill()的调用只会调用 Base2.kill(),因为派生重写,会跳过 Base1.kill,因为它根本就不知道有 Base1。一个变通方法是使用 super

如果 Base1调用了函数 super,它不会简单的调用基类的合约函数,它还会调用继承关系图谱上的下一个基类合约,所以会调用 Base2.kill()。需要注意的最终的继承图谱将会是:Final,Base1,Base2,mortal,owned。使用super时会调用的实际函数在使用它的类的上下文中是未知的,尽管它的类型是已知的。这类似于普通虚函数查找 (ordinary virtual method lookup)

基类构造器的方法(Arguments for Base Constructors)

派生的合约需要提供所有父合约需要的所有参数,所以用两种方式来做,见下面的例子:

或者直接在继承列表中使用 is Base(7),或像 修改器(modifier)使用方式一样,做为派生构造器定义头的一部分 Base(_y * _y)。第一种方式对于构造器是常量的情况比较方便,可以大概说明合约的行为。第二种方式适用于构造的参数值由派生合约的指定的情况。在上述两种都用的情况下,第二种方式优先(一般情况只用其中一种方式就好了)。

多继承与线性化(Multiple Inheritance and Linearization)

实现多继承的编程语言需要解决几个问题,其中之一是 菱形继承问题又称 钻石问题,如下图。

Solidity的解决方案参考 Python,使用 C3_linearization来强制将基类合约转换一个有向无环图(DAG)的特定顺序。结果是我们希望的单调性,但却禁止了某些继承行为。特别是基类合约在 is后的顺序非常重要。下面的代码,Solidity会报错 Linearization of inheritance graph impossible

原因是 C会请求 X来重写 A(因为继承定义的顺序是 A,X),但 A自身又是重写 X的,所以这是一个不可解决的矛盾。

一个简单的指定基类合约的继承顺序原则是从 most base-likemost derived

继承有相同名字的不同类型成员

当继承最终导致一个合约同时存在多个相同名字的修改器或函数,它将被视为一个错误。同新的如果事件与修改器重名,或者函数与事件重名都将产生错误。作为一个例外,状态变量的 getter可以覆盖一个 public的函数。

抽象(Abstract Contracts)

抽象函数是没有函数体的的函数。如下:

这样的合约不能通过编译,即使合约内也包含一些正常的函数。但它们可以做为基合约被继承。

如果一个合约从一个 抽象合约里继承,但却没实现所有函数,那么它也是一个 抽象合约

接口

接口与抽象合约类似,与之不同的是,接口内没有任何函数是已实现的,同时还有如下限制:

  1. 不能继承其它合约,或接口。
  2. 不能定义构造器
  3. 不能定义变量
  4. 不能定义结构体
  5. 不能定义枚举类

其中的一些限制可能在未来放开。

接口基本上限制为合约ABI定义可以表示的内容,ABI和接口定义之间的转换应该是可能的,不会有任何信息丢失。

接口用自己的关键词表示:

合约可以继承于接口,因为他们可以继承于其它的合约。

库(Libraries)

库与合约类似,但它的目的是在一个指定的地址,且仅部署一次,然后通过EVM的特性 DELEGATECALL(Homestead之前是用 CALLCODE)来复用代码。这意味着库函数调用时,它的代码是在调用合约的上下文中执行。使用 this将会指向到调用合约,而且可以访问调用合约的 存储(storage)。因为一个合约是一个独立的代码块,它仅可以访问调用合约明确提供的 状态变量(state variables),否则除此之外,没有任何方法去知道这些状态变量。

使用库合约的合约,可以将库合约视为隐式的 父合约(base contracts),当然它们不会显式的出现在继承关系中。但调用库函数的方式非常类似,如库 L有函数 f(),使用 L.f()即可访问。此外, internal的库函数对所有合约可见,如果把库想像成一个父合约就能说得通了。当然调用内部函数使用的是 internal的调用惯例,这意味着所有 internal类型可以传进去, memory类型则通过引用传递,而不是拷贝的方式。为了在EVM中实现这一点, internal的库函数的代码和从其中调用的所有函数将被 拉取(pull into)到调用合约中,然后执行一个普通的 JUMP来代替 DELEGATECALL

下面的例子展示了如何使用库(后续在 using for章节有一个更适合的实现 Set的例子)。

上面的例子中:

  • Library定义了一个数据结构体,用来在调用的合约中使用(库本身并未实际存储的数据)。如果函数需要操作数据,这个数据一般是通过库函数的第一个参数传入,按惯例会把参数名定为 self
  • 另外一个需要留意的是上例中 self的类型是 storage,那么意味着传入的会是一个引用,而不是拷贝的值,那么修改它的值,会同步影响到其它地方,俗称引用传递,非值传递。
  • 库函数的使用不需要实例化, c.register中可以看出是直接使用 Set.insert。但实际上当前的这个合约本身就是它的一个实例。
  • 这个例子中, c可以直接访问, knownValues。虽然这个值主要是被库函数使用的。

当然,你完全可以不按上面的方式来使用库函数,可以不需要定义结构体,不需要使用 storage类型的参数,还可以在任何位置有多个 storage的引用类型的参数。

调用 Set.containsSet.removeSet.insert都会编译为以 DELEGATECALL的方式调用 external的合约和库。如果使用库,需要注意的是一个实实在在的外部函数调用发生了。尽管 msg.sendermsg.valuethis还会保持它们在此调用中的值(在 Homestead之前,由于实际使用的是 CALLCODEmsg.sendermsg.value会变化)。

下面的例子演示了如何使用 memory类型和 内部函数(inernal function),来实现一个自定义类型,但不会用到 外部函数调用(external function)

因为编译器并不知道库最终部署的地址。这些地址须由 linker填进最终的字节码中(使用 命令行编译器来进行联接)。如果地址没有以参数的方式正确给到编译器,编译后的字节码将会仍包含一个这样格式的占们符 _Set___(其中 Set是库的名称)。可以通过手动将所有的40个符号替换为库的十六进制地址。

对比普通合约来说,库的限制:

  • 状态变量(state variables)
  • 不能继承或被继承
  • 不能接收 ether

这些限制将来也可能被解除!

附着库(Using for)

指令 using A for B;用来附着库里定义的函数(从库 A)到任意类型 B。这些函数将会默认接收调用函数对象的实例作为第一个参数。语法类似, python中的 self变量一样。

using A for *的效果是,库 A中的函数被附着在做任意的类型上。

在这两种情形中,所有函数,即使那些第一个参数的类型与调用函数的对象类型不匹配的,也被附着上了。类型检查是在函数被真正调用时,函数重载检查也会执行。

using A for B;指令仅在当前的作用域有效,且暂时仅仅支持当前的合约这个作用域,后续也非常有可能解除这个限制,允许作用到全局范围。如果能作用到全局范围,通过引入一些模块(module),数据类型将能通过库函数扩展功能,而不需要每个地方都得写一遍类似的代码了。

下面我们来换个方式重写 set的例子。

我们也可以通过这种方式来扩展 基本类型(elementary types)

需要注意的是所有库调用都实际上是EVM函数调用。这意味着,如果你传的是 memory类型的,或者是 值类型(vaue types),那么仅会传一份拷贝,即使是 self变量。变通之法就是使用 存储(storage)类型的变量,这样就不会拷贝内容。

Solidity Assembly

Solidity定义了一个汇编语言,可以不同Solidity一起使用。这个汇编语言还可以嵌入到Solidity源码中,以内联汇编的方式使用。下面我们将从内联汇编如何使用着手,介绍其与独立使用的汇编语言的不同,最后再介绍这门汇编语言。

文档尚待完善的补充的地方:待补充内联汇编的变量作用域的不同,尤其是使用含 internal的函数的库时所引入的复杂度。另外,还需补充,编译器定义的符号(symbols)。

内联汇编

通常我们通过库代码,来增强语言我,实现一些精细化的控制,Solidity为我们提供了一种接近于EVM底层的语言,内联汇编,允许与Solidity结合使用。由于EVM是栈式的,所以有时定位栈比较麻烦,Solidty的内联汇编为我们提供了下述的特性,来解决手写底层代码带来的各种问题:

  • 允许函数风格的操作码: mul(1, add(2, 3))等同于 push1 3 push1 2 add push1 1 mul
  • 内联局部变量: let x := add(2, 3) let y := mload(0x40) x := add(x, y)
  • 可访问外部变量: function f(uint x) { assembly { x := sub(x, 1) } }
  • 标签: let x := 10 repeat: x := sub(x, 1) jumpi(repeat, eq(x, 0))
  • 循环: for { let i := 0 } lt(i, x) { i := add(i, 1) } { y := mul(2, y) }
  • switch语句: switch x case 0 { y := mul(x, 2) } default { y := 0 }
  • 函数调用: function f(x) -> y { switch x case 0 { y := 1 } default { y := mul(x, f(sub(x, 1))) } }

下面将详细介绍内联编译(inline assembly)语言。

示例

下面的例子提供了一个库函数来访问另一个合约,并把它写入到一个 bytes变量中。有一些不能通过常规的Solidity语言完成,内联库可以用来在某些方面增强语言的能力。

内联编译在当编译器没办法得到有效率的代码时非常有用。但需要留意的是内联编译语言写起来是比较难的,因为编译器不会进行一些检查,所以你应该只在复杂的,且你知道你在做什么的事情上使用它。

语法

内联编译语言也会像Solidity一样解析注释,字面量和标识符。所以你可以使用 ///**/的方式注释。内联编译的在Solidity中的语法是包裹在 assembly { ... },下面是可用的语法,后续有更详细的内容。

  • 字面量。如 0x12342abc(字符串最多是32个字符)
  • 操作码(指令的方式),如 mload sload dup1 sstore,后面有可支持的指令列表
  • 函数风格的操作码,如 add(1, mlod(0)
  • 标签,如 name:
  • 变量定义,如 let x := 7let x := add(y, 3)
  • 标识符(标签或内联局部变量或外部),如 jump(name)3 x add
  • 赋值(指令风格),如, 3 =: x
  • 函数风格的赋值,如 x := add(y, 3)
  • 支持块级的局部变量,如 { let x := 3 { let y := add(x, 1) } }

操作码

这个文档不想介绍EVM虚拟机的完整描述,但后面的列表可以做为EVM虚拟机的指令码的一个参考。

如果一个操作码有参数(通过在栈顶),那么他们会放在括号。需要注意的是参数的顺序可以颠倒(非函数风格,后面会详细说明)。用 -标记的操作码不会将一个参数推到栈顶,而标记为 *的是非常特殊的,所有其它的将且只将一个推到栈顶。

在后面的例子中, mem[a...b)表示成位置 a到位置 b(不包含)的 memory字节内容, storage[p]表示在位置 pstrorage内容。

操作码 pushijumpdest不能被直接使用。

在语法中,操作码被表示为预先定义的标识符。

操作码 说明
stop stop execution, identical to return(0,0)
add(x, y)   x + y
sub(x, y)   x – y
mul(x, y)   x * y
div(x, y)   x / y
sdiv(x, y)   x / y, for signed numbers in two’s complement
mod(x, y)   x % y
smod(x, y)   x % y, for signed numbers in two’s complement
exp(x, y)   x to the power of y
not(x)   ~x, every bit of x is negated
lt(x, y)   1 if x < y, 0 otherwise
gt(x, y)   1 if x > y, 0 otherwise
slt(x, y)   1 if x < y, 0 otherwise, for signed numbers in two’s complement
sgt(x, y)   1 if x > y, 0 otherwise, for signed numbers in two’s complement
eq(x, y)   1 if x == y, 0 otherwise
iszero(x)   1 if x == 0, 0 otherwise
and(x, y)   bitwise and of x and y
or(x, y)   bitwise or of x and y
xor(x, y)   bitwise xor of x and y
byte(n, x)   nth byte of x, where the most significant byte is the 0th byte
addmod(x, y, m)   (x + y) % m with arbitrary precision arithmetics
mulmod(x, y, m)   (x * y) % m with arbitrary precision arithmetics
signextend(i, x)   sign extend from (i*8+7)th bit counting from least significant
keccak256(p, n)   keccak(mem[p…(p+n)))
sha3(p, n)   keccak(mem[p…(p+n)))
jump(label) jump to label / code position
jumpi(label, cond) jump to label if cond is nonzero
pc   current position in code
pop(x) remove the element pushed by x
dup1 … dup16   copy ith stack slot to the top (counting from top)
swap1 … swap16 * swap topmost and ith stack slot below it
mload(p)   mem[p..(p+32))
mstore(p, v) mem[p..(p+32)) := v
mstore8(p, v) mem[p] := v & 0xff – only modifies a single byte
sload(p)   storage[p]
sstore(p, v) storage[p] := v
msize   size of memory, i.e. largest accessed memory index
gas   gas still available to execution
address   address of the current contract / execution context
balance(a)   wei balance at address a
caller   call sender (excluding delegatecall)
callvalue   wei sent together with the current call
calldataload(p)   call data starting from position p (32 bytes)
calldatasize   size of call data in bytes
calldatacopy(t, f, s) copy s bytes from calldata at position f to mem at position t
codesize   size of the code of the current contract / execution context
codecopy(t, f, s) copy s bytes from code at position f to mem at position t
extcodesize(a)   size of the code at address a
extcodecopy(a, t, f, s) like codecopy(t, f, s) but take code at address a
returndatasize   size of the last returndata
returndatacopy(t, f, s) copy s bytes from returndata at position f to mem at position t
create(v, p, s)   create new contract with code mem[p..(p+s)) and send v wei and return the new address
create2(v, n, p, s)   create new contract with code mem[p..(p+s)) at address keccak256(

. n . keccak256(mem[p..(p+s))) and send v wei and return the new address
call(g, a, v, in, insize, out, outsize)   call contract at address a with input mem[in..(in+insize)) providing g gas and v wei and output area mem[out..(out+outsize)) returning 0 on error (eg. out of gas) and 1 on success
callcode(g, a, v, in, insize, out, outsize)   identical to call but only use the code from a and stay in the context of the current contract otherwise
delegatecall(g, a, in, insize, out, outsize)   identical to callcode but also keep caller and callvalue
staticcall(g, a, in, insize, out, outsize)   identical to call(g, a, 0, in, insize, out, outsize) but do not allow state modifications
return(p, s) end execution, return data mem[p..(p+s))
revert(p, s) end execution, revert state changes, return data mem[p..(p+s))
selfdestruct(a) end execution, destroy current contract and send funds to a
invalid end execution with invalid instruction
log0(p, s) log without topics and data mem[p..(p+s))
log1(p, s, t1) log with topic t1 and data mem[p..(p+s))
log2(p, s, t1, t2) log with topics t1, t2 and data mem[p..(p+s))
log3(p, s, t1, t2, t3) log with topics t1, t2, t3 and data mem[p..(p+s))
log4(p, s, t1, t2, t3, t4) log with topics t1, t2, t3, t4 and data mem[p..(p+s))
origin   transaction sender
gasprice   gas price of the transaction
blockhash(b)   hash of block nr b – only for last 256 blocks excluding current
coinbase   current mining beneficiary
timestamp   timestamp of the current block in seconds since the epoch
number   current block number
difficulty   difficulty of the current block
gaslimit   block gas limit of the current block

字面量

你可以使用整数常量,通过直接以十进制或16进制的表示方式,将会自动生成恰当的 pushi指令。

上面的例子中,将会先加2,3得到5,然后再与字符串 abc进行与运算。字符串按左对齐存储,且不能超过32字节。

函数风格

你可以在操作码后接着输入操作码,它们最终都会生成正确的字节码。比如:

下面将会添加 3memory中位置 0x80的值。

由于经常很难直观的看到某个操作码真正的参数,Solidity内联编译提供了一个函数风格的表达式,上面的代码与下述等同:

函数风格的表达式不能在内部使用指令风格,如 1 2 mstore(0x80, add)将不是合法的,必须被写为 mstore(0x80, add(2, 1))。那些不带参数的操作码,括号可以忽略。

需要注意的是函数风格的参数与指令风格的参数是反的。如果使用函数风格,第一个参数将会出现在栈顶。

访问外部函数与变量

Solidity中的变量和其它标识符,可以简单的通过名称引用。对于 memory变量,这将会把地址而不是值推到栈上。 Storage的则有所不同,由于对应的值不一定会占满整个 storage槽位,所以它的地址由槽和实际存储位置相对起始字节偏移。要搜索变量 x指向的槽位,使用 x_slot,得到变量相对槽位起始位置的偏移使用 x_offset

在赋值中(见下文),我们甚至可以直接向Solidity变量赋值。

还可以访问内联编译的外部函数:内联编译会推入整个的入口的 label(应用虚函数解析的方式)。Solidity中的调用语义如下:

  • 调用者推入返回的 label,arg1,arg2, … argn
  • 调用返回ret1,ret2,…, retm

这个功能使用起来还是有点麻烦,因为堆栈偏移量在调用过程中基本上有变化,因此对局部变量的引用将是错误的。

标签

另一个在EVM的汇编的问题是 jumpjumpi使用了绝对地址,可以很容易的变化。Solidity内联汇编提供了标签来让 jump跳转更加容易。需要注意的是标签是非常底层的特性,尽量使用内联汇编函数,循环,Switch指令来代替。下面是一个求Fibonacci的例子:

需要注意的是自动访问栈元素需要内联者知道当前的栈高。这在跳转的源和目标之间有不同栈高时将失败。当然你也仍然可以在这种情况下使用jump,但你最好不要在这种情况下访问栈上的变量(即使是内联变量)。

此外,栈高分析器会一个操作码接着一个操作码的分析代码(而不是根据控制流),所以在下面的情况下,汇编程序将对标签 two的堆栈高度产生错误的判断:

这个问题可以通过手动调整栈高来解决。你可以在标签前添加栈高需要的增量。需要注意的是,你没有必要关心这此,如果你只是使用循环或汇编级的函数。

下面的例子展示了,在极端的情况下,你可以通过上面说的解决这个问题:

定义汇编-局部变量

你可以通过 let关键字来定义在内联汇编中有效的变量,实际上它只是在 {}中有效。内部实现上是,在 let指令出现时会在栈上创建一个新槽位,来保存定义的临时变量,在块结束时,会自动在栈上移除对应变量。你需要为变量提供一个初始值,比如 0,但也可以是复杂的函数表达式:

赋值

你可以向内联局部变量赋值,或者函数局部变量。需要注意的是当你向一个指向 memorystorage赋值时,你只是修改了对应指针而不是对应的数据。

有两种方式的赋值方式:函数风格和指令风格。函数风格,比如 variable := value,你必须在函数风格的表达式中提供一个变量,最终将得到一个栈变量。指令风格 =: variable,值则直接从栈底取。以于两种方式冒号指向的都是变量名称(译者注:注意语法中冒号的位置)。赋值的效果是将栈上的变量值替换为新值。

Switch

你可以使用 switch语句来作为一个基础版本的 if/else语句。它需要取一个值,用它来与多个常量进行对比。每个分支对应的是对应切尔西到的常量。与某些语言容易出错的行为相反,控制流不会自动从一个判断情景到下一个场景(译者注:默认是break的)。最后有个叫 default的兜底。

可以有的 case不需要包裹到大括号中,但每个 case需要用大括号的包裹。

循环

内联编译支持一个简单的 for风格的循环。 for风格的循环的头部有三个部分,一个是初始部分,一个条件和一个后叠加部分。条件必须是一个函数风格的表达式,而其它两个部分用大括号包裹。如果在初始化的块中定义了任何变量,这些变量的作用域会被默认扩展到循环体内(条件,与后面的叠加部分定义的变量也类似。译者注:因为默认是块作用域,所以这里是一种特殊情况)。

函数

汇编语言允许定义底层的函数。这些需要在栈上取参数(以及一个返回的代码行),也会将结果存到栈上。调用一个函数与执行一个函数风格的操作码看起来是一样的。

函数可以在任何地方定义,可以在定义的块中可见。在函数内,你不能访问一个在函数外定义的一个局部变量。同时也没有明确的 return语句。

如果你调用一个函数,并返回了多个值,你可以将他们赋值给一个元组,使用 a, b := f(x)let a, b := f(x)

下面的例子中通过平方乘来实现一个指数函数。

内联汇编中要注意的事

内联汇编语言使用中需要一个比较高的视野,但它又是非常底层的语法。函数调用,循环, switch被转换为简单的重写规则,另外一个语言提供的是重安排函数风格的操作码,管理了 jump标签,计算了栈高以方便变量的访问,同时在块结束时,移除块内定义的块内的局部变量。特别需要注意的是最后两个情况。你必须清醒的知道,汇编语言只提供了从开始到结束的栈高计算,它没有根据你的逻辑去计算栈高(译者注:这常常导致错误)。此外,像交换这样的操作,仅仅交换栈里的内容,并不是变量的位置。

Solidity中的惯例

与EVM汇编不同,Solidity知道类型少于256字节,如, uint24。为了让他们更高效,大多数的数学操作仅仅是把也们当成是一个256字节的数字进行计算,高位的字节只在需要的时候才会清理,比如在写入内存前,或者在需要比较时。这意味着如果你在内联汇编中访问这样的变量,你必须要手动清除高位的无效字节。

Solidity以非常简单的方式来管理内存:内部存在一个空间内存的指针在内存位置 0x40。如果你想分配内存,可以直接使用从那个位置的内存,并相应的更新指针。

Solidity中的内存数组元素,总是占用多个32字节的内存(也就是说 byte[]也是这样,但是 bytesstring不是这样)。多维的 memory的数组是指向 memory的数组。一个动态数组的长度存储在数据的第一个槽位,紧接着就是数组的元素。

独立的汇编语言

上面介绍的在Solidity中嵌入的内联汇编语言也可以单独使用。实际上,它是被计划用来作为编译器的一种中间语言。在这个目的下,它尝试达到下述的目标:

  1. 使用它编写的代码要可读,即使代码是从Solidity编译得到的。
  2. 从汇编语言转为字节码应该尽可能的少坑。
  3. 控制流应该容易检测来帮助进行形式验证与优化。

为了达到第一条和最后一条的目标,Solidity汇编语言提供了高层级的组件比如, for循环, switch语句和函数调用。这样的话,可以不直接使用 SWAP, DUP, JUMP, JUMPI语句,因为前两个有混淆的数据流,后两个有混淆的控制流。此外,函数形式的语句如 mul(add(x, y), 7)比纯的指令码的形式 7 y x add num更加可读。

第二个目标是通过引入一个绝对阶段来实现,该阶段只能以非常规则的方式去除较高级别的构造,并且仍允许检查生成的低级汇编代码。Solidity汇编语言提供的非原生的操作是用户定义的标识符的命名查找(函数名,变量名等),这些都遵循简单和常规的作用域规则,会清理栈上的局部变量。

作用域:一个标识符(标签,变量,函数,汇编)在定义的地方,均只有块级作用域(作用域会延伸到,所在块所嵌套的块)。跨函数边界访问局部变量是不合法的,即使可能在作用域内(译者注:这里可能说的是,函数内定义多个函数的情况,JavaScript有这种语法)。不允许 shadowing。局部变量不能在定义前被访问,但标签,函数和汇编可以。汇编是非常特殊的块结构可以用来,如,返回运行时的代码,或创建合约。外部定义的汇编变量在子汇编内不可见。

如果控制流来到了块的结束,局部变量数匹配的 pop指令会插入到栈底(译者注:移除局部变量,因为局部变量失效了)。无论何时引用局部变量,代码生成器需要知道其当前在堆栈中的相对位置,因此需要跟踪当前所谓的堆栈高度。由于所有的局部变量在块结束时会被移除,因此在进入块之前和之后的栈高应该是不变的,如果不是这样的,将会抛出一个警告。

我们为什么要使用高层级的构造器,比如 switchfor和函数。

使用 switchfor和函数,可以在不用 jumpjumpi的情况下写出来复杂的代码。这会让分析控制流更加容易,也可以进行更多的形式验证及优化。

此外,如果手动使用 jumps,计算栈高是非常复杂的。栈内所有的局部变量的位置必须是已知的,否则指向本地变量的引用,或者在块结束时自动删除局部变量都不会正常工作。脱机处理机制正确的在块内不可达的地方插入合适的操作以修正栈高来避免出现 jump时非连续的控制流带来的栈高计算不准确的问题。

示例:

我们从一个例子来看一下Solidity到这种中间的脱机汇编结果。我们可以一起来考虑下下述Soldity程序的字节码:

它将生成下述的汇编内容:

在经过脱机汇编阶段,它会编译成下述的内容:

汇编有下面四个阶段:

  1. 解析
  2. 脱汇编(移除switch,for和函数)
  3. 生成指令流
  4. 生成字节码

我们将简单的以步骤1到3指定步骤。更加详细的步骤将在后面说明。

解析、语法

解析的任务如下:

  • 将字节流转为符号流,去掉其中的C++风格的注释(一种特殊的源代码引用的注释,这里不打算深入讨论)。
  • 将符号流转为下述定义的语法结构的AST。
  • 注册块中定义的标识符,标注从哪里开始(根据AST节点的注解),变量可以被访问。

组合词典遵循由Solidity本身定义的词组。

空格用于分隔标记,它由空格,制表符和换行符组成。 注释是常规的JavaScript / C ++注释,并以与Whitespace相同的方式进行解释。

语法:

脱汇编

一个AST转换,移除其中的 forswitch和函数构建。结果仍由同一个解析器,但它不确定使用什么构造。如果添加仅跳转到并且不继续的jumpdests,则添加有关堆栈内容的信息,除非没有局部变量访问到外部作用域或栈高度与上一条指令相同。伪代码如下:

生成操作码流

在操作码流生成期间,我们在一个计数器中跟踪当前的栈高,所以通过名称访问栈的变量是可能的。栈高在会修改栈的操作码后或每一个标签后进行栈调整。当每一个新局部变量被引入时,它都会用当前的栈高进行注册。如果要访问一个变量(或者拷贝其值,或者对其赋值),会根据当前栈高与变量引入时的当时栈高的不同来选择合适的 DUPSWAP指令。

伪代码:

状态变量的存储模型(Layout of State Variables in Storage)

大小固定的变量(除了 映射变长数组以外的所有类型)在存储(storage)中是依次连续从位置 0开始排列的。如果多个变量占用的大小少于32字节,会尽可能的打包到单个 storage槽位里,具体规则如下:

  • 在storage槽中第一项是按低位对齐存储(lower-order aligned)(译者注:意味着是大端序了,因为是按书写顺序。)。
  • 基本类型存储时仅占用其实际需要的字节。
  • 如果基本类型不能放入某个槽位余下的空间,它将被放入下一个槽位。
  • 结构体数组总是使用一个全新的槽位,并占用整个槽(但在结构体内或数组内的每个项仍遵从上述规则)

优化建议

当使用的元素占用少于32字节,你的合约的gas使用也许更高。这是因为EVM每次操作32字节。因此,如果元素比这小,EVM需要更多操作来从32字节减少到需要的大小。

因为编译器会将多个元素打包到一个 storage槽位,这样就可以将多次读或写组合进一次操作中,只有在这时,通过缩减变量大小来优化存储结构才有意义。当操作函数参数和 memory的变量时,因为编译器不会这样优化,所以没有上述的意义。

最后,为了方便EVM进行优化,尝试有意识排序 storage的变量和结构体的成员,从而让他们能打包得更紧密。比如,按这样的顺序定义, uint128, uint128, uint256,而不是 uint128, uint256, uint128。因为后一种会占用三个槽位。

非固定大小

结构体和数组里的元素按它们给定的顺序存储。

由于它们不可预知的大小。 映射变长数组类型,使用 Keccak-256哈希运算来找真正数据存储的起始位置。这些起始位置往往是完整的堆栈槽。

映射动态数组根据上述规则在位置 p占用一个未满的槽位(对 映射里嵌套 映射,数组中嵌套数组的情况则递归应用上述规则)。对一个动态数组,位置 p这个槽位存储数组的元素个数(字节数组和字符串例外,见下文)。而对于 映射,这个槽位没有填充任何数据(但这是必要的,因为两个挨着的 映射将会得到不同的哈希值)。数组的原始数据位置是 keccak256(p);而 映射类型的某个键 k,它的数据存储位置则是 keccak256(k . p),其中的 .表示连接符号。如果定位到的值以是一个非基本类型,则继续运用上述规则,是基于 keccak256(k . p)的新的偏移 offset

bytesstring占用的字节大小如果足够小,会把其自身长度和原始数据存在当前的槽位。具体来说,如果数据最多31位长,高位存数据(左对齐),低位存储长度 lenght * 2。如果再长一点,主槽位就只存 lenght * 2 + 1。原始数据按普通规则存储在 keccak256(slot)

所以对于接下来的代码片段:

按上面的代码来看,结构体从位置0开始,这里定义了一个结构体,但并没有对应的结构体变量,故不占用空间。 uint x实际是 uint256,单个占32字节,占用槽位0,所以映射 data将从槽位1开始。

data[4][9].b的位置在 keccak256(uint256(9) . keccak256(uint256(4) . uint256(1))) + 1

有人在这里尝试直接读取区块链的存储值, https://github.com/ethereum/solidity/issues/1550

内存变量的布局(Layout in Memory)

Solidity预留了3个32字节大小的槽位:

  • 0-64:哈希方法的 暂存空间(scratch space)
  • 64-96:当前已分配内存大小(也称 空闲内存指针(free memory pointer))

暂存空间可在语句之间使用(如在内联编译时使用)

Solidity总是在 空闲内存指针所在位置创建一个新对象,且对应的内存永远不会被释放(也许未来会改变这种做法)。

有一些在Solidity中的操作需要超过64字节的临时空间,这样就会超过预留的 暂存空间。他们就将会分配到 空闲内存指针所在的地方,但由于他们自身的特点,生命周期相对较短,且指针本身不能更新,内存也许会,也许不会被 清零(zerod out)。因此,大家不应该认为空闲的内存一定已经是 清零(zeroed out)的。

调用数据的布局(Layout of CallData)

当Solidity合约被部署后,从某个帐户调用这个合约,需要输入的数据是需要符合 the ABI specification, 中文翻译见这里: http://me.tryblockchain.org/