语法进阶¶
本小节我们进一步讲解常用的合约与合约的关系,并引入更高级的数据结构。
数据结构:map¶
我们已经学习过了结构体struct和数组两种高级数据结构,这两者都是为了有结构地存储数据而设计的。另一种在编程语言中不可或缺的数据结构是映射关系。在Solidity语言中也如同Python的dict或者JavaScript的对象一样,现成内置了一个映射数据结构,mapping。
例如我们可以存储账号地址以及它对应的合约内的token数量的关系(假设是一个代币合约)。
mapping (address => uint) public accountBalance;
这里我们申明关键字mapping表示这是一个映射关系;address代表的是一个账户的地址;uint代表了该账户对应的token数量;接着我们用修饰符public申明这个变量可以读取;这个映射关系我们命名为accountBalance。
小练习
请申明两个 mapping 映射。
保存汽车Car和对应的主人Owner address的对应关系。
保存主人地址address和它对应的 Car 的数量关系。
Car[] public cars;
mapping (uint => _____) public carToOwner;
mapping (_____ => uint) ownerCarCount;
环境变量:msg.sender¶
智能合约没有main函数,它是被动地响应外部调用的。谁来调用呢?根据之前我们第7章对以太坊虚拟机的学习,可以知道肯定是首先由外部使用者触发的,这种触发可以调用一个合约,该合约也可以链式调用其他合约为自己填充部分数据、执行某项操作。调用的当事人地址可以被智能合约知晓,即为msg.sender全局变量。这个全局变量存在于每个智能合约的执行环境上下文中。
mapping (address => uint) myNumber;
function setMyNumber(uint _myNumber) public {
// 设置一个调用者最喜欢的数字
myNumber [msg.sender] = _myNumber;
}
function whatIsMyNumber() public view returns (uint) {
// 取回设置好的数字,如果未被设置,则返回0
return myNumber [msg.sender];
}
上述合约代码片段,允许调用者设置一个数字,并且把数字再次从合约中读取出来。在这两个操作中都直接对于一个映射对象myNumber进行了读写操作。我们可以看到msg.sender始终存在于合约运行环境中,无需引入或者申明;另外mapping的读取和写入也如同数组一样,通过键值对的方式写入和读取。
小练习
请修改下列_createCar函数,让其除了发出事件以外,更能够记录下汽车Car和主人的两种对应关系:carToOwner和ownerCarCount:
Car[] public cars;
mapping (uint => address) public carToOwner;
mapping (address => uint) ownerCarCount;
function _createCar(string _name, uint _color) private {
uint id = cars.push(Car(_name, _color)) - 1;
_______[id] = msg.sender; // 此处填充
ownerCarCount[________]++; // 此处填充
emit NewCar(id, _name, _color);
}
require还是assert?¶
有时候我们会进行一定的函数条件检查,来判定是否可以接下去进行函数执行。读者会联想到if-else语法来进行判断,但是某些场合下,我们要求进行更严肃的权限检查,或者条件满足检查。require和assert两个关键字就应运而生,两者都在条件不满足时可以终止程序的运行,但是有如下区别。
- require条件检查语句如果不通过,则扣除运行到当前语句时,程序执行所花费的 gas,终止程序执行,并返回。
- assert 条件检查语句如果不通过,则视为严重错误,扣除所有的gas,终止程序执行,并返回.
例如以下程序将会检查发送方的字符串是否符合一定标准。
function sayHi (string _name) public returns (string) {
require(keccak256(_name) == keccak256("Hello"));
//条件满足,则执行:
return "Hi!";
}
这里位置上替换为assert关键字也是完全可行的。两者都会检查输入值是否是Hello。因为没有原生态的string比较函数,所以我们采用哈希的方法比较了两者的哈希值。Assert关键字相比于require更加具有惩罚性,经常用在检查变量范围上下溢出等场合,如果检查出错,表明程序出现了严重错误。而require则一般用在权限检查场合,检查是否有权操作合约等,权限不够则弹出提示,相对比较温和。
小练习
我们不希望每个客户都创建无数的车。他们在我们合约内有且只能保留一辆车。所以创建第二辆车是不可能的。请改造如下函数,并仅允许合约调用者在无车的时候创建一辆:
function createRandomCar(string _name) public {
require(ownerCarCount[______] == ____); // 填充此处
uint randColor = _generateRandomColor(_name);
_createCar(_name, randColor);
}
继承和引入¶
智能合约的代码可以来源于自身项目内,也可以来源于外部早已部署完毕的链上合约。使用合约继承语法,不但可以减少重复的代码数量,也可以将代码更清晰地划分成数个组成部分。
contract Dog {
function bark() public returns (string) {
return "Wong!";
}
}
contract BabyDog is Dog {
function feed() public returns (string) {
return "Drink some milk.";
}
}
这里小奶狗 BabyDog 继承了狗 Dog 的合约(通过 is 关键字),他们俩都具有bark()方法,同时 BabyDog还具有独特的feed()方法。
但是合约的代码不可能总是正好处在同一个文件内,我们经常要应用其他项目中的合约文件。怎么操作呢?我们可以将其分成两个文件,并放置在同一个目录下,并通过import 关键字来引入,还是用 Dog 合约来举例。
contract Dog {
function bark() public returns (string) {
return "Wong!";
}
}
import "./Dog.sol"
contract BabyDog is Dog {
function feed() public returns (string) {
return "Drink some milk.";
}
}
小练习
请填充如下文件CarMaking.sol ,让合约能够顺利继承CarFactory。
pragma solidity ^_________;
_______ "./CarFactory.sol";
contract CarMaking is CarFactory {
}
省钱妙招:内存变量¶
在以太坊虚拟机讲解的时候,我们提到了不同的存储类型,花费的gas数额不同。它们的最终存储地方也不同。有时候为了省钱,我们会把临时变量留在内存里,而不是保存在区块链上。随着程序执行,内存里的变量会消亡,而区块链上的会永存。由于没有改变区块链状态,内存变量(memory)的花费会比状态变量(storage)的花费少很多。
contract Restaurant {
struct Hamburger {
string name;
string status;
}
Hamburger[] hamburgers;
function eatHamburger(uint _index) public {
// Hamburger myHamburger = hamburgers[_index];
// 上面这句编译器给一个 warning,然如果我们用下列代码,则warning消失
Hamburger storage myHamburger = hamburgers[_index]; // storage 关键字
// 直接修改了区块链上的数据
myHamburger.status = "Eaten!";
// 也可以使用 memory 关键字
Hamburger memory anotherHamburger = hamburgers[_index + 1];
// 此时修改的是内存中的数据,区块链不收影响
anotherHamburger.status = "Eaten!";
// 强制回写,影响区块链上的数据
hamburgers[_index + 1] = anotherHamburger;
}
}
上述分别使用了 storage 和 memory 关键字来区别我们索引的对象,可以看见当我们用 storage 显式声明了之后,指针 myHamburger 指向了区块链上的某一个存储类型的数据,修改myHamburger后,立即在区块链上生效。而memory关键字申明的anotherHamburger 则不然,它仅为一份存储类型数据的内存拷贝,任何修改都不影响原数据,仅在内存中生效,如果想让修改在区块链上生效,必须回写到存储类型的数据上。
接口与合约调用¶
合约的接口就是合约的抽象。我们可以通过定义合约接口,并指定合约地址,来调用另外一个在以太坊上早已经部署好的合约。例如下的合约。
contract MyNumber {
mapping(address => uint) numbers;
function setNum(uint _num) public {
numbers[msg.sender] = _num;
}
function getNum(address _myAddress) public view returns (uint) {
return numbers[_myAddress];
}
}
这个合约可以提炼成为一个简单的合约接口:
contract NumberInterface {
function getNum(address _myAddress) public view returns (uint);
}
我们因为只关心getNum函数来获取数字,所以就定义了getNum这一个合约接口函数。那么合约如何使用呢?我们可以配合合约地址来使用,如下所示。
contract MyContract {
//取得已经部署好的合约的地址
address NumberInterfaceAddress = 0x1E24F805d89211eD515dD8A4A8C54f96a3E0C1FE
// 初始化合约,获得合约实例
NumberInterface numberContract = NumberInterface(NumberInterfaceAddress);
function someFunction() public {
//调用合约的方法
uint num = numberContract.getNum(msg.sender);
}
}
小练习
请为如下的合约生成接口,命名该接口,并调用该接口的方法。
contract Dog {
function bark() public returns (string) {
return "Wong!";
}
}
contract DogInterface {
function ____() ______ _______ (______);
}
contract MyContract {
//取得已经部署好的合约的地址
address DogInterfaceAddress = 0x735E388e9A8a073f14bdbb1C2bd4704dd386213c
// 初始化合约,获得合约实例
DogInterface dogContract = ____________(____________);
function someFunction() public {
//调用合约的方法
string message = dogContract._____();
}
}
多返回值¶
Solidity 的语法对于返回值并没有强制规定是一个单值,相反它鼓励多值返回来减少编程复杂度。多值返回的语法相对简单,如下所示。
// 申明要返回3个值
function someFunction() internal returns(uint a, uint b, uint c) {
return (1, 2, 3); //封装,返回3个值
}
function processMultipleReturns() external {
uint a;
uint b;
uint c;
//多值返回,直接解封装:
(a, b, c) = someFunction();
}
function getLastReturnValue() external {
uint c;
//我们也可以直接抛弃某些不关心的值:
(,,c) = someFunction();
}