Solidity 语法进阶
本小节我们进一步讲解常用的合约与合约的关系,并引入更高级的数据结构。
数据结构: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(); }
到现在这步,读者对于智能合约的基本编程已经有了一个充分的了解。相信萦绕在读者心头一定有疑问:以太坊智能合约编程,究竟和普通的无输入输出程序有何区别?在合约里,我如何加入更多的控制结构来保护方法不被恶意调用?本节将 ...