Solidity智能合约零基础入门(二)Solidity基础语法
Solidity基础语法
引言
Solidity是一种合约导向编程语言,用于编写智能合约,运行在Ethereum虚拟机(EVM)上。本文档将为您介绍Solidity的基本语法和概念,帮助您快速上手智能合约开发。
合约结构
在 Solidity 中,合约类似于面向对象编程语言中的类。 每个合约中可以包含 状态变量、 函数、 函数修饰器、事件、 结构类型、 和 枚举类型 的声明,且合约可以从其他合约继承。
状态变量
状态变量是永久地存储在合约存储中的值。
1 | |
有效的状态变量类型参阅 类型 章节, 对状态变量可见性有可能的选择参阅 可见性和 getter 函数 。
函数
函数是合约中代码的可执行单元。
1 | |
函数调用 可发生在合约内部或外部,且函数对其他合约有不同程度的可见性( 可见性和 getter 函数)。
函数修饰器
函数修饰器可以用来以声明的方式改良函数语义(参阅合约章节中 函数 )。
1 | |
事件
事件是能方便地调用以太坊虚拟机日志功能的接口。
1 | |
有关如何声明事件和如何在 dapp 中使用事件的信息,参阅合约章节中的 事件。
结构类型
结构是可以将几个变量分组的自定义类型(参阅类型章节中的 结构体)。
1 | |
枚举类型
枚举可用来创建由一定数量的“常量值”构成的自定义类型(参阅类型章节中的 枚举类型)。
1 | |
类型
Solidity 是一种静态类型语言,这意味着每个变量(状态变量和局部变量)都需要被指定类型。 Solidity 提供了几种基本类型,可以用来组合出复杂类型。
值类型
以下类型之所以被称为值类型,是因为它们的变量总是通过值传递, 即在用作函数参数或赋值时总是被复制。
布尔类型
bool :可能的取值为常数值 true 和 false。
运算符:
!(逻辑非)&&(逻辑与, “and”)||(逻辑或, “or”)==(等于)!=(不等于)
运算符 || 和 && 都遵循同样的短路( short-circuiting )规则。 就是说在表达式 f(x) || g(y) 中, 如果 f(x) 的值为 true / false , 那么 g(y) 就不会被执行,即使会出现一些副作用。
整型
int / uint: 分别表示有符号和无符号的不同位数的整型变量。 关键字 uint8 到 uint256 (无符号整型,从 8 位到 256 位)以及 int8 到 int256, 以 8 位为步长递增。 uint 和 int 分别是 uint256 和 int256 的别名。
运算符:
- 比较运算符:
<=,<,==,!=,>=,>(返回布尔值) - 位运算符:
&,|,^(异或),~(位取反) - 移位运算符:
<<(左移),>>(右移) - 算数运算符:
+,-, 一元运算-(只适用于有符号的整数),*,/,%(取余),**(幂)
对于一个整数类型 X,您可以使用 type(X).min 和 type(X).max 来访问该类型代表的最小值和最大值。
定长浮点型
警告. Solidity 还没有完全支持定长浮点型。可以声明定长浮点型的变量, 但不能给它们赋值或把它们赋值给其他变量。 ↩
fixed / ufixed:表示各种大小的有符号和无符号的定长浮点型。 在关键字 ufixedMxN 和 fixedMxN 中, M 表示该类型占用的位数, N 表示可用的小数位数。 M 必须能整除 8,即 8 到 256 位。 N 则可以是从 0 到 80 之间的任意数。 ufixed 和 fixed 分别是 ufixed128x18 和 fixed128x18 的别名。
1 | |
运算符:
- 比较运算符:
<=,<,==,!=,>=,>(返回值是布尔型) - 算术运算符:
+,-, 一元运算-,*,/,%(取余数)
地址类型
地址类型有两种大致相同的类型:
address: 保存一个20字节的值(一个以太坊地址的大小)。address payable: 与address类型相同,但有额外的方法transfer和send。
这种区别背后的想法是, address payable 是一个您可以发送以太币的地址, 而您不应该发送以太币给一个普通的 address,例如,因为它可能是一个智能合约, 而这个合约不是为接受以太币而建立的。
1 | |
运算符:
<=,<,==,!=,>=和>
定长字节数组
值类型 bytes1, bytes2, bytes3, …, bytes32 代表从1到32的字节序列。
1 | |
运算符:
比较运算符:<=, <, ==, !=, >=, > (返回布尔型)
- 比较运算符:
<=,<,==,!=,>=,>(返回bool) - 位运算符:
&,|,^(按位异或),~(按位取反) - 移位运算符:
<<(左移位),>>(右移位) - 索引访问: 如果
x是bytesI类型,那么当0 <= k < I时,x[k]返回第k个字节(只读)。
移位运算符以无符号的整数类型作为右操作数(但返回左操作数的类型), 它表示要移位的位数。有符号类型的移位将产生一个编译错误。
成员变量:
.length表示这个字节数组的长度(只读).
枚举类型
枚举是在 Solidity 中创建用户定义类型的一种方式。 它们可以显式地转换为所有整数类型,和从整数类型来转换,但不允许隐式转换。 从整数的显式转换在运行时检查该值是否在枚举的范围内,否则会导致 异常。 枚举要求至少有一个成员,其声明时的默认值是第一个成员。 枚举不能有超过256个成员。
数据表示与 C 语言中的枚举相同。选项由后续的从 0 开始无符号整数值表示。
使用 type(NameOfEnum).min 和 type(NameOfEnum).max 您可以得到给定枚举的最小值和最大值。
1 | |
函数类型
函数类型是一种表示函数的类型。可以将一个函数赋值给另一个函数类型的变量, 也可以将一个函数作为参数进行传递,还能在函数调用中返回函数类型变量。
类似的格式如下
1 | |
引用类型
在 Solidity 中,相比于值类型,引用类型更复杂,通常包括占用更多内存的数组和结构体。当处理这些复杂的引用类型时,需要谨慎考虑它们的数据位置。
所有引用类型(如数组和结构体)都具有一个 “数据位置” 属性,指明数据是存储在内存中还是存储中。大多数情况下,数据位置有默认值,但可以通过在类型名后加 storage 或 memory 来显式指定。
- memory:用于临时存储,数据在执行完后会被销毁。
- storage:用于持久保存,数据会永久保存,直到被修改或删除。
- calldata:只读,常用于存储外部函数的参数。
默认数据位置:
- 函数参数(包括返回参数)的默认位置是 内存 (memory)。
- 局部变量的默认位置是 存储 (storage)。
- 状态变量的数据位置强制为 存储 (storage)。
示例
1 | |
数据位置的重要性
数据位置影响赋值行为:
- 在 存储 (storage) 和 内存 (memory) 之间赋值会创建新的拷贝。
- 存储 (storage)进行 赋值时,只会传递引用,不会创建新副本。
- 内存 (memory) 之间的赋值通常不会创建新副本,因为它们共享相同的数据。
总结
- 强制指定的数据位置:
- 外部函数,也就是
external,形参的数据位置可以为calldata,也可以为memory,但是绝对不是storage,public同理。 - 状态变量为
storage,也可以指定函数形参的数据位置为storage,不过基本上只能在内部函数internal执行。
- 外部函数,也就是
- 默认数据位置:
- 函数参数(包括返回参数)为
memory,也可以为calldata。 - 其他局部变量为
storage。
- 函数参数(包括返回参数)为
数组
Solidity 支持两种类型的数组:固定长度数组和动态数组。
一个元素类型为 T,固定长度为 k 的数组可以声明为 T[k], 而动态数组声明为 T[]。
固定长度数组:uint[5],uint[2][5] …
动态数组:uint[],uint[][5] …
示例
1 | |
索引: 对元素中的一个数据进行定位,数组索引从0开始,访问一个数组中的其中一个元素,访问时的下标顺序与声明时相反。访问一个超过它的末端的数组会导致一个失败的断言。
示例
1 | |
数组成员:
length:
数组有
length成员变量表示当前数组的长度。一经创建, 内存memory数组的大小就是固定的(但却是动态的,也就是说,它依赖于运行时数组的参数)。1
2
3function returnLength() public view returns (uint){
return d.length;
}push():
动态存储数组和
bytes(不是string)有一个叫push()的成员函数, 您可以用它在数组的末尾追加一个零初始化的元素。它返回一个元素的引用, 因此可以像x.push().t = 2或x.push() = b那样使用。1
2
3function add(uint x) public{
c.push();
}push(x):
动态存储数组和
bytes(不是string)有一个叫push(x)的成员函数, 您可以用它在数组的末端追加一个指定的元素。该函数不返回任何东西。1
2
3function add(uint x) public{
d.push(x);
}pop():
动态存储数组和
bytes(不是string)有一个叫pop()的成员函数, 您可以用它来从数组的末端移除一个元素。 这也隐含地在被删除的元素上调用 delete。该函数不返回任何东西。
1 | |
创建内存数组:具有动态长度的内存数组可以使用 new 操作符创建。 与存储数组不同的是,不可能 调整内存数组的大小(例如, .push 成员函数不可用)。 您必须事先计算出所需的大小,或者创建一个新的内存数组并复制每个元素。
正如Solidity中的所有变量一样,新分配的数组元素总是以 默认值 进行初始化。
示例
1 | |
数组切片: x[start:end]
数组切片是对一个数组的连续部分的预览。 它们被写成 x[start:end],其中 start 和 end 是表达式, 结果是uint256类型(或隐含的可转换类型)。分片的第一个元素是 x[start], 最后一个元素是 x[end - 1]。
如果 start 大于 end,或者 end 大于数组的长度, 就会出现异常。
start 和 end 都是可选的: start 默认为 0, end 默认为数组的长度。
1 | |
数组元素:数组元素可以是任何类型,包括映射或结构体。 并适用于类型的一般限制,映射只能存储在 storage 数据位置,下面是一些特殊的数组。
bytes
bytes 类似于 bytes1[], 但它在 calldata 中会被“紧打包”(译者注:将元素连续地存在一起,不会按每 32 字节一单元的方式来存放)。
您应该使用 bytes 而不是 bytes1[],因为它更便宜, 因为在 memory 中使用 bytes1[] 会在元素之间增加31个填充字节。 一般来说,对于任意长度的原始字节数据使用 bytes,对于任意长度的字符串(UTF-8)数据使用 string。 如果您能将长度限制在一定的字节数,使用 bytes1 到 bytes32 中的一种值类型,因为它们更便宜。
函数: bytes.concat
bytes.concat 函数可以连接任意数量的 bytes 或 bytes1 ... bytes32 值。 该函数返回一个单一的 bytes memory 数组,其中包含没有填充的参数内容。 如果您想使用字符串参数或其他不能隐式转换为 bytes 的类型, 您需要先将它们转换为 bytes 或 bytes1 /…/ bytes32。
示例
1 | |
string
string 与 bytes 相同,但不允许用长度或索引来访问。
Solidity没有字符串操作函数,但有第三方的字符串库。 可以用 keccak256(abi.encodePacked(s1)) == keccak256(abi.encodePacked(s2)) 来比较两个字符串的keccak256-hash,用 string.concat(s1, s2) 来连接两个字符串。
函数: string.concat
可以使用 string.concat 连接任意数量的 string 值。 该函数返回一个单一的 string memory 数组,其中包含没有填充的参数内容。 如果您想使用不能隐式转换为 string 的其他类型的参数,您需要先将它们转换为 string。
示例
1 | |
结构体
Solidity 支持通过结构体定义新类型。
示例
1 | |
结构体的赋值和访问
- 存储结构体:当结构体赋值给局部变量时,并不会复制数据,而是传递引用。
- 成员访问:可以直接访问结构体的成员,例如
campaigns[campaignID].amount = 0;。
映射类型
在映射中,人们可以通过键(Key)来查询对应的值(Value),比如:通过一个人的id来查询他的钱包地址。
声明映射的格式为mapping(_KeyType => _ValueType),其中_KeyType和_ValueType分别是Key和Value的变量类型。例子:
映射的规则
规则1:映射的_KeyType只能选择Solidity内置的值类型,比如uint,address等,不能用自定义的结构体。而_ValueType可以使用自定义的类型。下面这个例子会报错,因为_KeyType使用了我们自定义的结构体:
规则2:映射的存储位置必须是storage,因此可以用于合约的状态变量,函数中的storage变量和library函数的参数(见例子)。不能用于public函数的参数或返回结果中,因为mapping记录的是一种关系 (key - value pair)。
规则3:如果映射声明为public,那么Solidity会自动给你创建一个getter函数,可以通过Key来查询对应的Value。
规则4:给映射新增的键值对的语法为_Var[_Key] = _Value,其中_Var是映射变量名,_Key和_Value对应新增的键值对。例子:
映射的原理
原理1: 映射不储存任何键(Key)的资讯,也没有length的资讯。
原理2: 对于映射使用keccak256(h(key) . slot)计算存取value的位置。https://github.com/WTFAcademy/WTF-Solidity-Internals/tree/master/tutorials/02_MappingStorage)
原理3: 因为Ethereum会定义所有未使用的空间为0,所以未赋值(Value)的键(Key)初始值都是各个type的默认值,如uint的默认值是0。
在Remix上验证 (以 Mapping.sol为例)
映射示例 1 部署


映射示例 3 key-value pair

运算符
在Solidity智能合约中,以下是一些常见运算符的简要说明和示例:
三元运算符
1 | |
允许根据条件表达式选择执行两个表达式之一。
1 | |
复合赋值运算符
-=,+=, *=, /=, %=, |=, &=, ^=, <<= 和 >>=
1 | |
用于简洁地对变量进行更新。
增量/减量运算符:
a++和a—
1 | |
1 | |
—a和++a
1 | |
1 | |
用于快速增加或减少变量的值。
删除运算符
在Solidity中是存在 delete 运算符的。使用 delete 可以将变量设置为其类型的默认值。对于简单类型,如 uint 或 int,默认值是 0;对于数组,delete 会使数组的所有元素被置为默认值;对于结构体,它将重置结构体中的所有成员变量为它们的默认值。
例如:
1 | |
请注意,delete 对于动态数组只会将特定索引处的元素重置为默认值,并不会改变数组的长度。而对于静态数组,delete 将会重置整个数组为默认值。对于映射(mapping),delete 会使特定的键对应的值被移除。对于复杂类型的数组或映射,delete 不会递归删除内部元素,只会将它们设置为默认值。
运算符优先顺序:
| 优先级 | 描述 | 操作符 | |
|---|---|---|---|
| 1 | 后置自增和自减 | ++, -- |
|
| 创建类型实例 | new <类型名> |
||
| 数组元素 | <数组>[<索引>] |
||
| 访问成员 | <对象>.<成员名> |
||
| 函数调用 | <函数>(<参数...>) |
||
| 小括号 | (<表达式>) |
||
| 2 | 前置自增和自减 | ++, -- |
|
| 一元运算减 | - |
||
| 一元操作符 | delete |
||
| 逻辑非 | ! |
||
| 按位非 | ~ |
||
| 3 | 乘方 | ** |
|
| 4 | 乘、除和模运算 | *, /, % |
|
| 5 | 算术加和减 | +, - |
|
| 6 | 移位操作符 | <<, >> |
|
| 7 | 按位与 | & |
|
| 8 | 按位异或 | ^ |
|
| 9 | 按位或 | ` | ` |
| 10 | 非等操作符 | <, >, <=, >= |
|
| 11 | 等于操作符 | ==, != |
|
| 12 | 逻辑与 | && |
|
| 13 | 逻辑或 | == |
|
| 14 | 三元操作符 | <判断条件> ? <如果为真时执行的表达式> : <如果为假时执行的表达式> |
|
| 赋值操作符 | =, ` |
=,^=,&=,<<=,>>=,+=,-=,*=,/=,%=` |
|
| 15 | 逗号 | , |
运算符按照特定的优先级进行求值,例如:
*、/、%(乘、除、取余)具有高于+、-(加、减)的优先级。&&(逻辑与)的优先级高于||(逻辑或)。- 使用括号
()可以改变默认的优先级。
下面是一个运算符优先级的例子:
1 | |
基本类型之间的转换
隐式转换
在某些情况下,在赋值过程中,在向函数传递参数和应用运算符时, 编译器会自动应用隐式类型转换。一般来说,如果在语义上有意义, 并且不会丢失信息,那么值-类型之间的隐式转换是可能的。
uint8 => uint16, int128=>int256, int8 !=> uint256【!=-1】
显式转换
如果编译器不允许隐式转换,但您确信转换会成功, 有时可以进行显式类型转换。 这可能会导致意想不到的行为,并使您绕过编译器的一些安全特性, 所以一定要测试结果是否是您想要的和期望的!
int y=> uint(y)
字面常数和基本类型之间的转换
整数类型
十进制和十六进制的数字字面常数可以隐含地转换为任何足够大的整数类型去表示它而不被截断。
固定大小的字节数组
十进制数字字面常数不能被隐含地转换为固定大小的字节数组。 十六进制数字字面常数是可以的,但只有当十六进制数字的数量正好符合字节类型的大小时才可以。 但是有一个例外,数值为0的十进制和十六进制数字字面常数都可以被转换为任何固定大小的字节类型
地址类型
正如在 地址字面量(Address Literals) 中所描述的那样,正确大小并通过校验测试的十六进制字是 address 类型。 其他字面常数不能隐含地转换为 address 类型。
总结:
- 隐式转换:只有在“范围扩大 + 不丢失信息”时才允许。
- 显式转换:允许开发者强制转换,但可能导致意外结果 → 必须验证。
字面量转换规则:
- 整数字面量可隐式转换(不超范围)。
- 十六进制字面量可用于定长字节数组(需精确字节数)。
- 地址必须是符合格式的十六进制字面量。
参考资料 :