contract Coin{
//关键字"public"使变量能从合约外部访问。
address public minter;
mapping (address => uint) public balances;
//事件event让轻客户端能高效的对变化做出反应
event Sent(address from, address to, uint amount);
//这个构造函数的代码仅仅只能在合约创建的时候被运行
function Coin(){
minter = msg.sender;
}
function mint(address receiver, uint amount){
if(msg.sender != minter) return;
balances[receiver] += amount;
}
function send(address receiver, uint amount){
if(balances[msg.sender] < amount) return;
balances[msg.sender] -= amount;
Sent(msg.sender, receiver, amount);
}
}
mapping (address => uint) public balances;
表示声明一个map类型状态变量balances,其中k是address,v是balances。即可以存入键值对.可以根据address,查找到与之对应的balances.
我们可以提供一个接口如下提供查询访问:
function balances(address _account) returns (uint balance){
return balances[_account];
}
下面是一些使用时间单位的实用案例:
uint lastUpdated;
// 将‘上次更新时间’ 设置为 ‘现在’
function updateTimestamp() public {
lastUpdated = now;
}
// 如果到上次`updateTimestamp` 超过5分钟,返回 'true'
// 不到5分钟返回 'false'
function fiveMinutesHavePassed() public view returns (bool) {
return (now >= (lastUpdated + 5 minutes));
}
uint8 a = 5;
uint b = 6;
// 将会抛出错误,因为 a * b 返回 uint, 而不是 uint8:
uint8 c = a * b;
// 我们需要将 b 转换为 uint8:
uint8 c = a * uint8(b);
以下是其定义方式和一种初始化方式
struct myCat {
string name;
uint age;
}
myCat("Tom", 2);
声明一个public的动态数组名字叫myCats
myCat[] public myCats;
映射 如下的定义方式我们可以定义一个balance = accountBalance[address];通过传入address获取账户对应的余额。或者通过获取用户名:username = userIdToName[userid];
//对于金融应用程序,将用户的余额保存在一个 uint类型的变量中
mapping (address => uint) public accountBalance;
//或者可以用来通过userId 存储/查找的用户名
mapping (uint => string) userIdToName;
private/public
internal/external
internal 和 private 类似,不过, 如果某个合约继承自其父合约,这个合约即可以访问父合约中定义的“内部”函数。
external 与 public 类似,只不过这些函数只能在合约之外调用 - 它们不能被合约内的其他函数调用。稍后我们将讨论什么时候使用 external 和 public。
string greeting = "What's up dog";
function sayHello() public returns (string) {
return greeting;
}
这个函数可以读取greeting的值但是不改变合约的任何数据这时候我们就可以用view来修饰这个函数意味着:该函数可以读取数据但不能修改数据
function _multiply(uint a, uint b) private pure returns (uint) {
return a * b;
}
还有一种就是函数既不读取也不修改合约里的数据,就如上边这个函数就是求两个参数的乘积。不需要读取更不需要修改任何数据。
/**
* @dev 调用者不是‘主人’,就会抛出异常
*/
modifier onlyOwner() {
require(msg.sender == owner);
_;
}
那么这个修饰符如何使用呢
我们举如下这个例子:
contract MyContract is Ownable {
event LaughManiacally(string laughter);
//注意! `onlyOwner`上场 :
function likeABoss() external onlyOwner {
LaughManiacally("Muahahahaha");
}
}
用法其实和系统自带的修饰符如public,external是一样的。还有一种就是
带参数的函数修饰符
具体用法如下:
// 存储用户年龄的映射
mapping (uint => uint) public age;
// 限定用户年龄的修饰符
modifier olderThan(uint _age, uint _userId) {
require(age[_userId] >= _age);
_;
}
// 必须年满16周岁才允许开车 (至少在美国是这样的).
// 我们可以用如下参数调用`olderThan` 修饰符:
function driveCar(uint _userId) public olderThan(16, _userId) {
// 其余的程序逻辑
}
关于为什么需要继承就不赘述了。在solidity中如何做到继承呢如下:
contract Human {
function humanSayHello() returns(string) {
return "Hello!"
}
}
//一个叫woman的合约继承自Human
contract woman is Human {
}
那么除了Human中定义的private修饰的函数或者变量woman都将继承过来并可以使用。
如果我们的合约需要和区块链上的其他的合约会话,则需先定义一个 interface (接口)。
先举一个简单的栗子。 假设在区块链上有这么一个合约:
contract LuckyNumber {
mapping(address => uint) numbers;
function setNum(uint _num) public {
numbers[msg.sender] = _num;
}
function getNum(address _myAddress) public view returns (uint) {
return numbers[_myAddress];
}
}
这是个很简单的合约,您可以用它存储自己的幸运号码,并将其与您的以太坊地址关联。 这样其他人就可以通过您的地址查找您的幸运号码了。
现在假设我们有一个外部合约,使用 getNum 函数可读取其中的数据。
首先,我们定义 LuckyNumber 合约的 interface :
contract NumberInterface {
function getNum(address _myAddress) public view returns (uint);
}
那么接口定义好了,我们该如何使用它呢
contract MyContract {
address NumberInterfaceAddress = 0xab38...;
// ^ 这是FavoriteNumber合约在以太坊上的地址
NumberInterface numberContract = NumberInterface(NumberInterfaceAddress);
// 现在变量 `numberContract` 指向另一个合约对象
function someFunction() public {
// 现在我们可以调用在那个合约中声明的 `getNum`函数:
uint num = numberContract.getNum(msg.sender);
// ...在这儿使用 `num`变量做些什么
}
}
我们可以看到当以contract的形式声明一个interface后我们可以用通过接收接口名(合约地址)返回值的形式得到一个要调用的合约的一个对象。
通过这个对象我们就可以调用合约的东西啦~~~
前面说到合约调用,让我们感觉这个很像库的使用。OpenZeppelin为我们合约的健壮性提供了许多开源的库。接下来我们以safeMath为例
实现一下库的调用。
假设我们有一个 uint8, 只能存储8 bit数据。这意味着我们能存储的最大数字就是二进制 11111111 (或者说十进制的 2^8 - 1 = 255).
来看看下面的代码。最后 number 将会是什么值?
uint8 number = 255;
number++;
在这个例子中,我们导致了溢出 — 虽然我们加了1, 但是 number 出乎意料地等于 0了。 (如果你给二进制 11111111 加1, 它将被重置为 00000000,就像钟表从 23:59 走向 00:00)。
为了解决这些问题,我们引入safeMath合约(找到开源代码复制到我们自己新建的sol文件中再import就好了)
那么怎么使用呢
一个库 是 Solidity 中一种特殊的合约。其中一个有用的功能是给原始数据类型增加一些方法。
比如,使用 SafeMath 库的时候,我们将使用 using SafeMath for uint256 这样的语法。 SafeMath 库有四个方法 — add, sub, mul, 以及 div。现在我们可以这样来让 uint256 调用这些方法:
using SafeMath for uint256;
uint256 a = 5;
uint256 b = a.add(3); // 5 + 3 = 8
uint256 c = a.mul(2); // 5 * 2 = 10
示例如下,multipleReturns()是一个返回多个值的内部函数,我们在processMultipleReturns中调用并接收其返回值。
当我们不需要接收其所有返回值时如getLastReturnValue()函数中留空其他字段即可。
function multipleReturns() internal returns(uint a, uint b, uint c) {
return (1, 2, 3);
}
function processMultipleReturns() external {
uint a;
uint b;
uint c;
// 这样来做批量赋值:
(a, b, c) = multipleReturns();
}
// 或者如果我们只想返回其中一个变量:
function getLastReturnValue() external {
uint c;
// 可以对其他字段留空:
(,,c) = multipleReturns();
}
通常我们只声明一个变量时用uint8或者uint256,Solidity 都会为它保留256位的存储空间不会为你节省任何 gas
除非把 uint 绑定到 struct 里面。
struct NormalStruct {
uint a;
uint b;
uint c;
}
struct MiniMe {
uint32 a;
uint32 b;
uint c;
}
// 因为使用了结构打包,`mini` 比 `normal` 占用的空间更少
NormalStruct normal = NormalStruct(10, 20, 30);
MiniMe mini = MiniMe(10, 20, 30);
当用户从外部调用一个view函数,是不需要支付一分 gas 的。
这是因为 view 函数不会真正改变区块链上的任何数据 - 它们只是读取。因此用 view 标记一个函数,意味着告诉 web3.js,运行这个函数只需要查询你的本地以太坊节点,而不需要在区块链上创建一个事务(事务需要运行在每个节点上,因此花费 gas)。
所以关键是要记住,在所能只读的函数上标记上表示“只读”的“external view 声明,就能为你的用户减少在 DApp 中 gas 用量。
注意:如果一个 view 函数在另一个函数的内部被调用,而调用函数与 view 函数的不属于同一个合约,也会产生调用成本。这是因为如果主调函数在以太坊创建了一个事务,它仍然需要逐个节点去验证。所以标记为 view 的函数只有在外部调用时才是免费的。
function getArray() external pure returns(uint[]) {
// 初始化一个长度为3的内存数组
uint[] memory values = new uint[](3);
// 赋值
values.push(1);
values.push(2);
values.push(3);
// 返回数组
return values;
}
为什么要在内存中声明数组呢,
因为Solidity 使用storage(存储)是相当昂贵的,”写入“操作尤其贵。
这是因为,无论是写入还是更改一段数据, 这都将永久性地写入区块链。”永久性“啊!需要在全球数千个节点的硬盘上存入这些数据,随着区块链的增长,拷贝份数更多,存储量也就越大。这是需要成本的!
为了降低成本,不到万不得已,避免将数据写入存储。这也会导致效率低下的编程逻辑 - 比如每次调用一个函数,都需要在 memory(内存) 中重建一个数组,而不是简单地将上次计算的数组给存储下来以便快速查找
在数组后面加上 memory关键字, 表明这个数组是仅仅在内存中创建,不需要写入外部存储,并且在函数调用结束时它就解散了。与在程序结束时把数据保存进 storage 的做法相比,内存运算可以大大节省gas开销 – 把这数组放在view里用,完全不用花钱。
在实际使用中我们通常是在函数中使用for循环进行遍历然后条件选择得到我们需要的数组,这个数组并不需要写入,存在于内存中就可以支持我们接下来的工作了。
这样我们就无须花费任何gas。
contract GetPaid is Ownable {
function withdraw() external onlyOwner {
owner.transfer(this.balance);
}
}
你可以通过 transfer 函数向一个地址发送以太, 然后 this.balance 将返回当前合约存储了多少以太。 所以如果100个用户每人向我们支付1以太, this.balance 将是100以太。
注意我们使用 Ownable 合约中的 owner 和 onlyOwner,假定它已经被引入了
Solidity 中最好的随机数生成器是 keccak256 哈希函数.
我们可以这样来生成一些随机数:
// 生成一个0到100的随机数:
uint randNonce = 0;
uint random = uint(keccak256(now, msg.sender, randNonce)) % 100;
randNonce++;
uint random2 = uint(keccak256(now, msg.sender, randNonce)) % 100;
如货币的ERC20,一个 代币 在以太坊基本上就是一个遵循一些共同规则的智能合约——即它实现了所有其他代币合约共享的一组标准函数,例如 transfer(address _to, uint256 _value) 和 balanceOf(address _owner).
在智能合约内部,通常有一个映射, mapping(address => uint256) balances,用于追踪每个地址还有多少余额。
所以基本上一个代币只是一个追踪谁拥有多少该代币的合约,和一些可以让那些用户将他们的代币转移到其他地址的函数。
但是如果我们不是做货币而是做其他应用呢,像百度莱茨狗,网易招财猫这样的产品,是否还能使用ERC20标准呢,答案是不能。货币可分但是猫狗能分吗。我们可以有0.5个货币但是不能说有0.5只猫吧。还有并不是所有猫狗都像代币一样是平等的每一只的属性都不一样。
所以像这样的加密收藏品,我们有另外一套标准,它们被称为ERC721 代币.
请注意,使用像 ERC721 这样的标准的优势就是,我们不必在我们的合约中实现拍卖或托管逻辑,这决定了玩家能够如何交易/出售我们的加密收藏品。 如果我们符合规范,其他人可以为加密可交易的 ERC721 资产搭建一个交易所平台,我们的 ERC721 加密收藏品将可以在该平台上使用。 所以使用代币标准相较于使用你自己的交易逻辑有明显的好处。
以上参考自以太坊solidity官方文档以及cryptozombies的僵尸工厂教程。连接如下:
[1] https://cryptozombies.io/zh/course
[2] https://solidity.readthedocs.io/zh/latest/