以太坊Solidity编程:智能合约实现之函数与合约
函数及基本控制结构
函数类型
- 函数也是一个类型,且属于值类型
- 可以将一个函数赋值给一个变量赋值给一个变量,一个函数类型的变量
- 还可以将一个函数作为参数进行传递
- 也可以在函数调用中返回一个函数
- 函数类型有两类,可分为internal和external
- 内部函数(internal):不能在当前合约的上下文环境以外的地方执行,内部函数只能在当前合约内被使用。如在当前的代码块内,包括库函数,和继承的函数中。
- 外部函数(External):外部函数由地址和函数方法签名两部分组成。可作为外部函数调用的参数,或者由外部函数调用返回。
函数类型的定义
function (param types) {internal|external} [pure|constant|view|payable][returns (return types)] varName;
- 如果不写类型,默认的函数类型是internal的
- 如果函数没有返回结果,则必须省略returns关键字
函数的定义
function f(<parameter types>) {internal|external} [pure|constant|view|payable][returns (<return types>)]{ // function body}
示例:
contract SimpleFunc {
function hello(uint i) {
// todo
}
}
入值和返回值
- 同JavaScript一样,函数有输入参数,但与之不同的是,函数可能有任意数量的返回参数
- 入参(Input Parameter)与变量的定义方式一致,稍微不同的是,不会用到的参数可以省略变量名称
- 出参(Output Parameters)在returns关键字后定义,语法类似变量的定义方式
示例: - Solidity内置支持元素(tuple),这种内置结构可以同时返回多个结果
示例
函数控制结构
- 支持if、else、while、do、for、break、continue、return、?:
- 条件判断中的括号不可省略,但在单行语句中的大括号可以省略
- 无Boolean类型的自动转换,比如if(1){…}在Solidity中是无效的
- 没有switch
函数调用
- 内部调用:同一个合约中,函数互相调用。调用在EVM中被翻译成简单的跳转指令
- 外部调用:一个合约调用另外一个合约的函数,或者通过web3调用合约函数。调用通过消息调用完成(bytes24类型,20字节合约地址+4字节的函数方法签名)。
示例:
命名参数调用和匿名函数参数
- 函数调用的参数,可以通过指定名字的方式调用,但可以以任意的顺序,使用方式为{}包含。参数的类型和数量要与定义一致。
示例:
省略函数参数名称
- 没有使用的参数名可以省略
示例:
变量作用范围
- 一个变量在声明后都有初始值为字节表示的全0
- Solidity使用了JavaScript的变量作用范围的规则
- 函数内定义的变量,在整个函数中均可用,无论它在哪里定义
示例:
函数可见性
函数的可见性
- 函数类型:internal和external
- 访问方式:内部访问和外部访问
- 处于访问控制的需要,函数具有“可见性(Visibility)类型”。
- 状态变量也具有可见性类型
可见性类型
- external:“外部函数”,可以从其他合约或者通过交易来发起调用。合约内不能直接调用。
- public:“公开函数”,可以从合约内调用,或者消息来进行调用
- Internal:“内部函数”,只能在当前合约或继承的合约里调用
- private:“私有函数”,仅在当前合约中可以访问,在继承的合约内,不可访问。
可见性图
默认访问权限
- 函数默认是public
- 状态变量默认的可见性是internal
合约声明示例
状态变量访问
- 编译器为自动为所有的public的状态变量创建访问函数。
- 生成的函数名为参数名称,输入参数根据变量类型来顶。如uint无须参数,uint[]则需要输入参数数组下标作为参数
示例:
函数修饰符
- 修改器(Modifiers)可以用来改变一个函数的行为
- 一般用于在函数执行前检查某种前置条件。
- 修改器是一种合约属性,可被继承,同时还可派生的合约重写。
- 函数可以有多个修改器,它们之间以空格隔开,修饰器会依次检查执行。
示例:
函数属性
- 根据对状态变量的修改情况,函数可以拥有pure|constant|view|public四个属性。
- 属性定义放置在函数可见性之后
- 不强制使用
- 主要是为了节省gas
属性介绍:
- 只有当函数有返回值的情况下,才需要使用pure、view、constant
- prue:可以读取状态变量但是不能改。
- view:不能改也不能读状态变量,否则编译通不过
- constant:view的旧版本,v4.17之后不建议
- 带关键字pure或view,就不能修改状态变量的值。默认只是向区块链读取数据,读取数据不需要花费gas。
状态变量的属性: - 状态变量可以声明为constant,同一般语言的常量,目前只支持值类型和String。
- public的状态变量,其getter函数属性为view。
事件
- 事件是使用EVM日志内置功能的方便工具
- 事件在合约中可被继承
- 当被调用时,会触发参数存储到交易的日志中
- 可以最多有3个参数被设置为indexed,来设置是否被索引。
- 设置为索引后,可以允许通过这个参数来查找日志,甚至可以按特定的值过滤。
错误和异常处理
Solidity的异常处理:
- Solidity使用“状态恢复”来处理错误
- 有某些情况下,异常是自动抛出的
- 抛出异常的效果是当前的执行被终止且被撤销(值的改变和账户余额的变化都会被回退)
- Solidity暂时没有捕捉异常的方法(try…catch)
assert/require:
- 函数assert和require来进行条件检查,如果条件不满足则抛出异常
- assert函数通常用来测试内部错误
- require函数来检查输入变量或合约状态变量是否满足条件,以及验证调用外部合约返回值
revert/throw: - revert函数可用于标记错误
- throw同revert类似,已不建议使用
三个异常示例(等价的):
常见异常:
- 数组访问越界,或是负的序号值访问数组
- 调用require/assert,但参数值为false
- .transfer()执行失败
- 如果你的public的函数在没有payable关键字时,却尝试在接收ether
- 如果你通过消息调用一个函数,但在调用的过程中,并没有正确结果(如gas不足等)
合约与继承
合约
- Solidity中合约类似面向对象语言中的类。
- 合约可以继承。
- 一个合约可以调用另外一个合约。
- 一个合约中可以创建另外一个合约。
- 合约操作另外一个合约,一般都需要直到其代码。
合约间调用
contract OwnedToken{
// TokenCreator是一个合约类型
// 未初始化前,为一个引用
TokenCreator creator;
address owner; // 状态变量
bytes32 name; // 状态变量
// 构造函数
function OwnedToken(bytes32 _name) public{
owner = msg.sender;
creator = TokenCreator(msg.sender);// 另外一个合约
name = _name;
}
}
合约中创建合约
- 一个合约可以通过new关键字来创建一个合约。
- 要创建合约的完整代码,必须提前知道
contact TokenCreator{
function createToken(bytes32 name) public returns (OwnedToken){
// 创建一个新的合约,name为构造函数所需变量
OwnedToken tokenAddress = new OwnedToken(name);
return tokenAddress;
}
}
继承
- Solidity通过复制包括多态的代码来支持多重继承。基本的继承体系与python类似
- 当一个合约从多个其他合约那里继承,在区块链上仅会创建一个合约,在父合约里的代码会复制来形成继承合约。
- 派生的合约需要提供所有父合约需要的所有参数。
contract Owned {
function owned() { owner = msg.sender; }
address owner;
}
// 使用`is`来继承另外一个合约
// 子合约可以使用所有的非私有变量,包括内部函数和状态变量
contract Mortal is Owned {
function kill() {
if (msg.sender == owner) selfdestruct(owner);
}
}
几种特殊的合约
- 抽象合约(Abstract Contracts)
- 合约包含抽象函数,也就是没有函数体的函数
- 这样的合约不能通过编译,即使合约内也包含一些正常的函数
- 抽象合约一般可以做为基合约被继承
contact Feline{
// 函数没有函数实现,为抽象函数,对应的合约为抽象合约
function utterance() returns (bytes32);
function getContractName() returns (string) {
return "Feline";
}
}
contract Cat is Feline{
// 继承抽象合约,实现函数功能
function utterance() returns (bytes32) {return "miaow";}
}
- 接口(Interfaces)
- 接口与抽象合约类似,与之不同的是,接口内没有任何函数是已实现的,同时还有如下限制:
- 不能继承其他合约,或接口
- 不能定义构造器
- 不能定义变量
- 不能定义结构体
- 不能定义枚举类
- 接口基本上限制为合约ABI定义可以表示的内容
// 接口
interface Token {
function transfer(address recipient, uint amount);
}
// 接口可以被
contract MyToken is Token {
function transfer(address recipient, uint amount){
// 函数发现
}
}
- 库(Libraries)
- 库与合约类似,但它的目的是在一个指定的地址,且仅部署一次,然后通过EVM的特性来复用代码。
- 使用库合约的合约,可以将库合约视为隐式的父合约(base contracts),不会显示的出现在继承关系中。
- 调用库函数的方式非常类似,如库L有函数f(),使用L.f()即可访问
library Set{
struct Data { mapping(uint => bool) flags; }
// 第一个参数的类型为“storage reference”, 仅存储地址,而不是这个库的特别特征
// 按照一般语言的惯例,`self`代表第一个参数
function insert(Data storage self, uint value)
public
return (bool)
{
if (self.flags[value])
return false; //already there
self.flags[value] = true;
return true;
}
}
contract C{
Set.Data knownValues;
function register(uint value) public{
// 库可以直接调用,而无需使用this
requires(Set.insert(knownValues, value));
}
}
实战:奖品竞拍
项目需求
- 大家之前都拿到了不少课程积分,为活跃课程氛围,特开展奖品竞拍活动:
-
- 大家用课程积分竞拍,采用连续竞价、明拍、限时模式。
- 奖品为电商兑换码,可以兑换不同学习物品
- 一次拍卖完成后,积分概不退还,可开始下一次拍卖。
基本数据结构
- 积分体系:完全使用之前的Mycoin即可,使用继承
- 拍卖列表:用户和出价,使用一个mapping即可
- 出价最高用户/出价:全局状态变量
- 限时/兑换码/竞拍状态:全局状态变量
功能需求:竞价
- 连续竞价,和之前的出价叠加
- 必须高于当前最高出价才算出价成功。
- 出价的积分打入合约地址
功能需求:竞价完成
- 结束时间已到
- 之前没有领取过
- 最高出价用户领取
- 全局状态变量管理员可以强制终止拍卖
功能需求:重开竞价
- 只有管理员可以重开
- 重新设置兑换码
- 重新设置兑换时间
- 重置竞拍状态
代码
MyCoin.sol
pragma solidity >=0.6.4;
contract MyCoin{
//数据结构
//1.用户
address[] userList;
mapping(address=>uint8) userDict;
//2.用户的币
mapping(address=>uint) balances;
//3.用户交易,第一个address为交易发起方,第二个为接收方,uint[]为交易量记录
mapping(address => mapping(address=>uint[])) public trans;
//用户初次可领取的数量
uint iniCount = 100;
function Mycoin() public{
//合约本身拥有的币
balances[address(this)] = 100000;
}
//领取货币,只能一次
function getCoin() public returns (bool sufficientAndFirstGet) {
//判断合约是否钱足够
if(balances[address(this)]<iniCount) return false;
//判断是否从合约领取过币
if(0!=trans[address(this)][msg.sender].length) return false;
//领取币
balances[address(this)] -= iniCount;
balances[msg.sender] += iniCount;
//记录交易
trans[address(this)][msg.sender].push(iniCount);
//加入用户列表
if(0 == userDict[msg.sender]){
userList.push(msg.sender);
userDict[msg.sender] = 1;
}
return true;
}
//发送货币
function sendCoin(address receiver, uint amount) public returns(bool sufficient){
//判断是否还有足够的币
if(balances[address(this)]<amount) return false;
//发生交易
balances[msg.sender] -= amount;
balances[receiver] += amount;
//记录交易
trans[msg.sender][receiver].push(amount);
//加入用户列表
if (0 == userDict[receiver]) {
userList.push(receiver);
userDict[receiver] = 1;
}
return true;
}
//获得某个用户的货币量
function getBalances(address addr) public returns(uint){
return balances[addr];
}
//获得领取的进度
function getPercent() public returns(uint){
uint sum = 0;
for(uint i = 0;i < userList.length; i++){
address userAddress = userList[i]; //用户的地址
sum = sum + balances[userAddress];
}
return (100*sum)/100000;
}
//获得币最多的用户地址
function getBest() public returns (address){
address maxAdd;
uint maxCoin = 0;
//获得币最多的用户地址
for(uint i = 0; i < userList.length; i++){
address userAddress = userList[i]; //用户地址
uint userCoin = balances[userAddress]; //用户积分
if(userCoin > maxCoin){
maxAdd = userAddress;
maxCoin = userCoin;
}
}
return maxAdd;
}
}
MyBid.sol
pragma solidity >=0.6.4;
import "./MyCoin.sol";
contract MyBid is MyCoin{
//数据结构
string private ticket;//电商兑换码
address owner;//合约创建者
//时间是unix的绝对时间戳
uint public auctionEnd;
// 拍卖的当前状态
address public highestBidder; //最高出价用户的地址
uint public highestBid; //最高出价
//出价列表
mapping (address => uint) bids;
// 拍卖结束后设为 true,将禁止所有的变更
bool ended;
//事件
event HighestBidIncreased(address bidder, uint amount);
event AuctionEnded(address winner, uint amount);
//dev 创建一个简单的拍卖
//_biddingTime,拍卖时间;_ticket,兑换码
constructor (uint _biddingTime, string memory _ticket) public{
ticket = _ticket;
auctionEnd = block.timestamp + _biddingTime;
owner = msg.sender;
}
// 竞价,value为加价,和之前的出价叠加
function bid(uint value) public {
// 如果拍卖已结束,撤销函数的调用。
require(block.timestamp < auctionEnd);
// 如果出价不够高,不继续执行
uint actualValue = bids[msg.sender] + value;
require(actualValue > highestBid);
//把积分发送到合约地址,已经检查额度
sendCoin(address(this),value);
//更新出价和最高出价
bids[msg.sender] = actualValue;
highestBidder = msg.sender;
highestBid = actualValue;
emit HighestBidIncreased(msg.sender, actualValue);
}
// 结束拍卖,并把最高的出价发送给受益人
function auctionEnded() public payable returns (string memory){
// 1. 条件
require(now >= auctionEnd,"Not End!"); // 拍卖尚未结束
require(!ended); // 该函数已被调用
//必须是最高出价用户调用,或者管理员强制终止
require(msg.sender == highestBidder || msg.sender == owner);
// 2. 生效
ended = true;
emit AuctionEnded(highestBidder, highestBid);
// 3. 返回ticket,只能查看一次
return ticket;
}
// `_;` 表示修饰符,可代表被修饰函数位置
modifier onlyOwner {
require(msg.sender == owner);
_;
}
//开始新的拍卖
function newBid(uint _biddingTime, string memory _ticket) public onlyOwner {
// 1. 条件
require(now >= auctionEnd);
require(ended);
//重置数据
ticket = _ticket;
auctionEnd = now + _biddingTime;
ended = false;
delete highestBidder; //重置最高出价用户的地址
delete highestBid; //重置最高出价
//delete bids[msg.sender];
}
}
注意:可能由于版本问题存在一些错误,如有参考,请及时更正!