以太坊Solidity编程:智能合约实现之基本语法
基本数据类型
1. Solidity类型:
- solidity为静态类型语言
- 所有变量均需要预定义
- 和大多数语言一样,提供几个基本类型和基本类型组成的复杂类型
- 和现代语言相比,solidity仅覆盖最常见的类型。
2. Solidityt基本数据类型
- 布尔类型
- 布尔:真或假
- 操作符:!(逻辑非)
- &&(逻辑与)
- || (逻辑或)
- == (相等)
- != (不等)
- 整型
- int / uint : 是有符号和无符号的整数
- uint8到uint256步长8(从8到256位的无符号整数)
- uint和int分别是uint256和int256的别名
- 地址
- 地址:20字节(一个以太坊地址)
- 可表示用户账号,合约地址
- 操作符:≤、<、==、!=,≥,>
- 地址成员: 账户余额(balance)
- 地址成员:发送(transfer),从合约发起方向某个地址转入以太币(单位是wei)
- 注释
- 字符串常量
- 字符串常量用双引号或者单引号括起来,如“abc”,'abc’均可
- 字符串常量可以隐式转换位bytes
- 字符串常量支持转义符,如\n,\xNN(16进制)and \nNNNN(Unicode)
补充:
整型操作符:
- 比较: ≤,<,==,!=,≥,>(计量布尔量)
- 位操作符:&,!,^(位异或),~(位取反)
- 算术操作符:+,-,*,/,%(取余数),**(幂次方)
- 除以0操作,会报异常
枚举类型: - 用户自定义类型,基本同C++等语言的定义
- 枚举类型中的枚举值到整型可显示转换,但不能隐式转换
- 枚举类型不能为空,至少应当包含一个值
基本类型转换
- 分为隐式转换和显示转换,如字符串转为整型等
- 隐式转换:如果运算符支持两边不同的类型,编译器会尝试隐式转换类型,包括赋值
- 隐式转换需要能保证不会丢失数据,且语义可通
- 显示转换:如果编译器不允许隐式的自动转换,但你知道转换没问题时,可以进行转换
- 不正确的转换会带来错误
array类型
数组
- 数组是可以在编译时固定大小的,也可以是动态的
- 对于storage存储数组来说,成员类型可以是任意的(也可以是其他数组,映射或结构)
- 对于memory数组来说,成员类型不能是一个映射,如果是公开可见的函数参数,成员类型是必须是ABI类型的。
数组声明
- T[k]:元素类型为T,固定长度为k的数组
- T[]:动态大小的(变长)的数组
- uint[3][5]:声明5个uint[3](与大部分语言相同).访问时,uint[4][0]:第5个数组的第1个元素
- bytes类似于byte[]
数组成员函数
- 成员函数:length,存放元素的数量
- 动态length可变
- 成员函数:push,可在数组的尾部添加一个元素。函数返回新的长度
- Push目前仅storage变长数组可用
数组使用事项
数组作为返回参数使用
- 暂时还无法在外部函数中使用数组的数组(多维数组)
- 暂时无法从外部函数调用返回的动态内容
- 目前可行的解决办法是使用较大的静态尺寸大小的数组
数组创建和初始化
- 可使用new关键字创建或初始化memory的变长数组
- 但不能通过.length的长度来修改memory数组大小属性
- Storage数组可以修改长度
- 目前暂时是这样规定的
要点:变长数组须先new初始化,分配空间
要点:变长storage数组可以通过push初始化
要点:暂不能通过外部函数返回变长数组
要点:定长的数组不可直接赋值给变长数组
mapping类型
映射定义
- 映射或字典类型,一种键值对的映射关系存储结构
- 定义方式为mapping(KeyType=>KeyValue)
- 键的类型一般为基本数据类型,暂不支持映射、变长数组、结构体等复杂类型。值的类型无限制
- 映射可以视为哈希表。所有可能的键会被虚拟化的创建,映射到一个类型的默认值
- 映射表中,我们并不存储键的数据,仅存储它的keccak256哈希值,用来查找值时使用
- 映射并没有长度,键集合(或列表),值集合(或列表)这样的概念。
映射定义的例子:
映射的赋值:
映射使用要点
- 映射类型,仅能用来定义状态变量,或者是在内部函数中作为storage类型的引用
- 可以通过将映射标记为public,来让Solidity创建一个访问器
- 访问映射,需要提供一个键值作为参数
- 映射暂未提供迭代输出的方法
struct类型
结构体
- Solidity提供struct来定义自定义类型
- struct可以用于映射和数组中作为元素
- 其本身也可以包含映射和数组等类型
- 但struct内不能有struct。
结构体示例
结构体初始化
直接赋值(按顺序)
命名初始化
忽略mapping
数据的位置和引用类型
值类型:布尔、整型、地址、定长字节数组
- 在传值时,总是值传递,完全拷贝
引用类型:不定长数组、字符串、数组、结构体
- 复杂类型,占用空间较大,在拷贝时占用空间较大,一般都通过引用传递。
引用传递
- 值传递,传递的是内存的内容
- 引用传递,传递的是内存的地址
- 引用的改变,修改后会改变内存地址对应储存的值,也就是变量和其引用会同时改变。
数据的位置
- 复杂/引用类型,如数组(arrays)和结构体(struct)有一个额外的属性,数据的存储位置,可选为memory或storage
- Memory存储位置为内存
- Storage保存永久记录,存储在链上
数据的默认位置
- 基于程序的上下文,大多数时候这样的选择是默认的,也通过指定关键字storage和memory修改它
- 函数参数,包括返回的参数,默认是memory
- 局部复杂变量默认是storage
- 状态变量强制为storage
不同数据位置的赋值
- 在memory和storage之间,完全拷贝
- 任何位置的变量,赋值给状态变量,完全拷贝
- 状态变量(storage),赋值storage局部变量,引用传递
- memory的引用类型赋值给另一个memory的引用,不会创建另一个拷贝
注意事项
- 不能将memory赋值给局部变量
- memory只能用于函数内部
- 对于值类型,总是会进行拷贝
- storage在区块链中是用key/value的形式存储,而memory则表现为字节数组
- 值类型的局部变量是存储在栈上
- Gas消耗storage>>memory>stack
不同位置复杂类型赋值图
常规用法
- storage为合约级变量,在合约创建时就确定了,但内容可以被(交易)改变
- solidity认为交易就是改变了合约状态,故合约级变量称为“状态”变量。函数内部只能定义storage的引用
- memory只能用于函数内部,其声明EVM在运行时创建一块内存区域给变量使用
特殊变量和函数
地址相关(Address Related)
- <address>.balance(uint256): address的余额,以wei为单位
- <address>.transfer(uint256 amount): 从合约(地址)向address发送一定数量的ether,以wei为单位。
- <address>.send(uint256 amount) returns (bool): 同transfer。不建议。
示例:
合约相关
- this:当前合约的类型,可以显示的转换为Address
- selfdestruct(address recipt):销毁当前合约,并把它所有资金发送到给定的地址。
- 如果一个函数需要进行货币操作,必须要带上payable关键字。
数学和加密函数
- assert(bool condition):如果条件不满足,抛出异常
- keccak256(…) returns (bytes32):使用以太坊的(Keccak-256)计算HASH值。常用来做字符串相等判别。
特殊变量及函数
- msg.sender(address) 当前调用发起人的地址
- msg.value(uint) 这个消息所附带的货币量,单位为wei
- tx.origin(address) 交易的发送者。不建议
- msg.data(bytes) 完整的调用数据(calldata)。
- now(uint) 当前块的时间戳。
时间单位
- seconds、minutes、hours、days、weeks、years均可做为后缀,并进行相互转换,默认是seconds为单位。
- 后缀不能用于变量
货币单位
- 一个字面量的数字,可以使用后缀wei、finney、szabo或ether来在不同面额中转换
- 不含任何后缀的默认单位是wei
单位换算
货币使用示例
实战:课程积分
项目需求
- 为了活跃课程的气氛,促进同学之间的交流,决定建立课程积分体系:
- 每个同学都能领到100个课程积分
- 为别的同学答疑解惑,可以从对方那里获得一定的课程积分
- 课程结束后,积分最高的同学获得小红花
典型设计
基于sql数据库设计实现:
用户表(User)
- userId: 用户id
- email: 用户email
- …
用户积分表(UserCoin) - userId: 用户id
- coin:积分数量
- …
用户积分交易表i(UserTrade) - userId: 发送方用户id
- toUserId: 接收方用户id
- coin:发送数量
- …
不足
- 发放积分方式不透明,需要老师的信誉背书
- 积分交易数据不透明,可能出现刷分现象
- 服务器可能挂了…
Dapp设计的优势
- 去信任。代码透明,积分体系和发放形式完全公开,无需老师信用背书
- 去中心。数据透明,交易数据公共可读,无法随意篡改,公共监督。不用担心机器崩溃。
- 隐私保护。暂不需要。
Dapp数据结构设计
用状态变量实现数据结构
用户表(User)
struct User {
address userId; // 用户id
}
User[] userList;
用户积分表(UserCoin)
struct UserCoin {
address userId; //用户id
uint coin; //积分数量
}
UserCoin[] userCoinList;
用户积分交易表(UserTrade)
struct UserTrade {
address userId; // 发送方userid
address toUserId; // 接收方userid
uint coin; //发送数量
}
UserTrade[] userTradeList;
Dapp数据结构设计改进原则
- 基本思路同“SQL数据库映射到KV数据库”
- 根据查询/索引需求,在基本数据结构上,建立mapping
- 为节省存储,去掉冗余数据
功能需求1
- 每个同学都能领100个课程积分
- 函数 getCoin()
- 功能:由合约为调用者发送100个积分
- 查询需求:只能领一次,需要根据合约和调用者地址,查询对应的交易记录。
功能需求2
- 同学之间可以互送积分
- 函数 sendCoin()
- 功能:调用者向一个地址发送积分
- 查询需求:一个用户地址拥有的积分
Dapp数据结构的改进
用户积分交易
struct UserTrade {
address userId; // 发送方userid
address toUserId; // 接收方userid
uint coin; //发送数量
}
UserTrade[] userTradeList;
查询需求:根据“合约和调用者地址”,查询交易
mapping (address => mapping(address=>UserTrade[])) trans;
去除冗余数据
mapping (address => mapping(address=>uint[])) trans;
Dapp数据结构设计结果
用户
address[] userList;
mapping (address => uint8) userDict;
用户积分(address为key)
mapping (address => uint) balances;
用户积分交易表(address+address为key)
mapping (address => mapping (address => uint[])) trans;