contract Example {
// 这个无符号整数将会永久的被保存在区块链中
string name ="Jay";
uint age = 25;
}
字符串类型:string
string字符串用于保存任意长度的 UTF-8 编码数据。 如: string greeting = “Hello world!”
。
无符号整数: uint
uint 无符号数据类型, 指其值不能是负数,对于有符号的整数存在名为 int 的数据类型。
注: Solidity中, uint 实际上是 uint256代名词, 一个256位的无符号整数。你也可以定义位数少的uints — uint8, uint16, uint32 等,但一般来讲我们都使用简单的 uint。
注: 通常情况下我们不会考虑使用 uint 变种,因为无论如何定义 uint的大小,Solidity 为它保留256位的存储空间。例如,使用 uint8 而不是uint(uint256)不会为你节省任何 gas。
除非,把 uint 绑定到 struct 里面。
如果一个 struct 中有多个 uint,则尽可能使用较小的 uint, Solidity 会将这些 uint 打包在一起,从而占用较少的存储空间。
struct NormalStruct {
uint a;
uint b;
uint c;
}
//下面的比上面的省gas
struct MiniMe {
uint32 a;
uint32 b;
uint c;
}
// 因为使用了结构打包,`mini` 比 `normal` 占用的空间更少
NormalStruct normal = NormalStruct(10, 20, 30);
MiniMe mini = MiniMe(10, 20, 30);
并且把同样类型的变量放一起(即在 struct 中将把变量按照类型依次放置),这样 Solidity 可以将存储空间最小化。例如,有两个 struct:
前者比后者需要的gas更少,因为前者把uint32放一起了。
uint c; uint32 a; uint32 b; 和 uint32 a; uint c; uint32 b;
struct Person {
uint age;
string name;
}
Person satoshi = Person(172, "Satoshi");
静态数组 和 动态数组
// 固定长度为2的静态数组:
uint[2] fixedArray;
// 固定长度为5的string类型的静态数组:
string[5] stringArray;
// 动态数组,长度不固定,可以动态添加元素:
uint[] dynamicArray;
Person[] people;
array.push() 在数组的 尾部 加入新元素,所以元素在数组中的顺序就是我们添加的顺序,如:
uint[] numbers;
numbers.push(5);
numbers.push(10);
numbers.push(15);
// numbers is now equal to [5, 10, 15]
定义 public 数组,Solidity 会自动创建 getter 方法。语法如下:
Person [] public people;
// 创建一个新的Person:
Person satoshi = Person(172, "Satoshi");
// 将新创建的satoshi添加进people数组:
people.push(satoshi);
//也可以两步并一步
people.push(Person(16, "Vitalik"));
习惯上函数里的变量都是以“_”开头 (但不是硬性规定) 以区别全局变量。
function eatHamburgers(string _name, uint _amount) {
}
调Yong:eatHamburgers(“vitalik”, 100);
函数的属性默认为公共(public),意味着任何一方 (或其它合约) 都可以调用你合约里的函数。所以将自己的函数定义为 私有(private) 是一个好的编程习惯,只有当你需要外部世界调用它时才将它设置为公共。
在函数名字后面使用关键字 private 即可。和函数的参数类似,私有函数的名字用(_)起始。
uint[] numbers;
function _addToArray(uint _number) private {
numbers.push(_number);
}
1、返回值 returns
string greeting = "What's up dog";
function sayHello() public returns (string) {
return greeting;
}
2、函数的修饰符 view、pure、payable
function sayHello() public view returns (string)
function _multiply(uint a, uint b) private pure returns (uint)
contract OnlineStore {
function buySomething() external payable {
// 检查以确定0.001以太发送出去来运行函数:
require(msg.value == 0.001 ether);
// 如果为真,一些用来向函数调用者发送数字内容的逻辑
transferThing(msg.sender);
}
}
一些人会从 web3.js 调用这个函数 (从DApp的前端), 像这样 :
// 假设 `OnlineStore` 在以太坊上指向你的合约:
OnlineStore.buySomething().send(from: web3.eth.defaultAccount, value: web3.utils.toWei(0.001))
注:
3、处理多返回值
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(); //使用()括起来并用“,”做占位符
}
4、提现
学习了如何向合约发送以太,在你发送以太之后,它将被存储进以合约的以太坊账户中, 并冻结在哪里 —— 除非你添加一个函数来从合约中把以太提现。
contract GetPaid is Ownable {
function withdraw() external onlyOwner {
owner.transfer(this.balance);
}
}
注意我们使用Ownable 合约中的owner 和 onlyOwner,假定它已经被引入了。
你可以通过transfer 函数向一个地址发送以太, 然后 this.balance 将返回当前合约存储了多少以太。 所以如果100个用户每人向我们支付1以太, this.balance将是100以太。
你可以通过transfer 向任何以太坊地址付钱。 比如,你可以有一个函数在msg.sender超额付款的时候给他们退钱:
uint itemFee = 0.001 ether;
msg.sender.transfer(msg.value - itemFee);
或者在一个有卖家和卖家的合约中, 你可以把卖家的地址存储起来, 当有人买了它的东西的时候,把买家支付的钱发送给它 seller.transfer(msg.value)
。
Ethereum 内部有一个散列函数keccak256,它用了SHA3版本。一个散列函数基本上就是把一个字符串转换为一个256位的16进制数字。字符串的一个微小变化会引起散列数据极大变化。
这在 Ethereum 中有很多应用,但是现在我们只是用它造一个伪随机数。
例子:
//6e91ec6b618bb462a4a6ee5aa2cb0e9cf30f7a052bb467b0ba58b8748c00d2e5
keccak256(“aaaab”);
//b1f078126895a1424524de5321b339ab00408010b7cf0e6ed451514981e58aa9
keccak256(“aaaac”);
显而易见,输入字符串只改变了一个字母,输出就已经天壤之别了。
注: 在区块链中安全地产生一个随机数是一个很难的问题, 本例的方法不安全,但是在我们的Zombie DNA算法里不是那么重要,已经很好地满足我们的需要了。
类型转换
有时你需要变换数据类型。例如:
uint8 a = 5;
uint b = 6;
// 将会抛出错误,因为 a * b 返回 uint, 而不是 uint8:
uint8 c = a * b;
// 我们需要将 b 转换为 uint8:
uint8 c = a * uint8(b);
上面, a * b 返回类型是 uint, 但是当我们尝试用 uint8 类型接收时, 就会造成潜在的错误。如果把它的数据类型转换为 uint8, 就可以了,编译器也不会出错。
事件: 是合约和区块链通讯的一种机制。你的前端应用“监听”某些事件,并做出反应。
event IntegersAdded(uint x, uint y, uint result);
function add(uint _x, uint _y) public {
uint result = _x + _y;
//触发事件,通知app
IntegersAdded(_x, _y, result);
return result;
}
你的 app 前端可以监听这个事件。JavaScript 实现如下:
YourContract.IntegersAdded(function(error, result) {
// 干些事
}
// 下面是调用合约的方式:
var abi = /* abi是由编译器生成的 */
var ZombieFactoryContract = web3.eth.contract(abi)
var contractAddress = /* 发布之后在以太坊上生成的合约地址 */
var ZombieFactory = ZombieFactoryContract.at(contractAddress)
// `ZombieFactory` 能访问公共的函数以及事件
// 某个监听文本输入的监听器:
$("#ourButton").click(function(e) {
var name = $("#nameInput").val()
//调用合约的 `createRandomZombie` 函数:
ZombieFactory.createRandomZombie(name)
})
// 监听 `NewZombie` 事件, 并且更新UI
var event = ZombieFactory.NewZombie(function(error, result) {
if (error) return
generateZombie(result.zombieId, result.name, result.dna)
})
// 获取 Zombie 的 dna, 更新图像
function generateZombie(id, name, dna) {
let dnaStr = String(dna)
// 如果dna少于16位,在它前面用0补上
while (dnaStr.length < 16)
dnaStr = "0" + dnaStr
let zombieDetails = {
// 前两位数构成头部.我们可能有7种头部, 所以 % 7
// 得到的数在0-6,再加上1,数的范围变成1-7
// 通过这样计算:
headChoice: dnaStr.substring(0, 2) % 7 + 1,
// 我们得到的图片名称从head1.png 到 head7.png
// 接下来的两位数构成眼睛, 眼睛变化就对11取模:
eyeChoice: dnaStr.substring(2, 4) % 11 + 1,
// 再接下来的两位数构成衣服,衣服变化就对6取模:
shirtChoice: dnaStr.substring(4, 6) % 6 + 1,
//最后6位控制颜色. 用css选择器: hue-rotate来更新
// 360度:
skinColorChoice: parseInt(dnaStr.substring(6, 8) / 100 * 360),
eyeColorChoice: parseInt(dnaStr.substring(8, 10) / 100 * 360),
clothesColorChoice: parseInt(dnaStr.substring(10, 12) / 100 * 360),
zombieName: name,
zombieDescription: "A Level 1 CryptoZombie",
}
return zombieDetails
}
Addresses (地址): 以太坊区块链由 account (账户)组成,你可以把它想象成银行账户。一个帐户的余额是以太 (在以太坊区块链上使用的币种),你可以和其他帐户之间支付和接受以太币,就像你的银行帐户可以电汇资金到其他银行帐户一样。地址是账户唯一的标识符:
0x0cE446255506E92DF41614C46F1d6df9Cc969183
(这是CryptoZombies作者的ETH地址)
所以我们可以指定“地址”作为僵尸主人的 ID。当用户通过与我们的应用程序交互来创建新的僵尸时,新僵尸的所有权被设置到调用者的以太坊地址下。
Mapping(映射): 典型的K-V结构,可以使用key来查找value
mapping (address => uint) public accountBalance;
在 Solidity 中,有一些全局变量可以被所有函数调用。 其中一个就是 msg.sender,它指的是当前调用者(或智能合约)的 address。
function getmsg () view returns (address){
return msg.sender ;
}
假设我们当前地址为:0xCA35b7d915458EF540aDe6068dFe2F44E8fa733c
那么调用getmsg()将给我返回 0: address: 0xCA35b7d915458EF540aDe6068dFe2F44E8fa733c
由此可见 msg.sender就是调用者的地址。
注意:在 Solidity 中,功能执行始终需要从外部调用者开始。 一个合约只会在区块链上什么也不做,除非有人调用其中的函数,所以 msg.sender总是存在的。
// 得到僵尸的id
uint id = zombies.push(Zombie(_name, _dna)) - 1;
// 僵尸id的YongYou者是msg.sender
zombieToOwner[id] = msg.sender;
// msg.sender地址的僵尸数量+1
ownerZombieCount[msg.sender]++;
require:使得函数在执行过程中,当不满足某些条件时抛出错误,并停止执行
function sayHiToVitalik(string _name) public returns (string) {
// 比较 _name 是否等于 "Vitalik". 如果不成立,抛出异常并终止程序
/*
(敲黑板: Solidity 并不支持原生的字符串比较, 我们只能通过比较
两字符串的 keccak256 哈希值来进行判断)
*/
require(keccak256(_name) == keccak256("Vitalik"));
// 如果返回 true, 运行如下语句
return "Hi!";
}
当代码过于冗长的时候,最好将代码和逻辑分拆到多个不同的合约中,以便于管理。继承能够让我们访问被继承合约中的公共方法和属性:
继承格式为: 当前合约 is 被继承的合约
contract Doge {
function catchphrase() public returns (string) {
return "So Wow CryptoDoge";
}
}
contract BabyDoge is Doge {
function anotherCatchphrase() public returns (string) {
return "Such Moon BabyDoge";
}
}
在 Solidity 中,当你有多个文件并且想把一个文件导入另一个文件时,可以使用 import 语句:
import "./someothercontract.sol";
contract newContract is SomeOtherContract {
}
这样当我们在合约(contract)目录下有一个名为 someothercontract.sol 的文件( ./ 就是同一目录的意思),它就会被编译器导入。
在 Solidity 中,有两个地方可以存储变量 —— storage 或 memory。
大多数时候你都用不到这些关键字,默认情况下 Solidity 会自动处理它们。 状态变量(在函数之外声明的变量)默认为“存储”形式,并永久写入区块链;而在函数内部声明的变量是“内存”型的,它们函数调用结束后消失。
然而也有一些情况下,你需要手动声明存储类型,主要用于处理函数内的 结构体 和 数组 时:
// 所以你应该明确定义 `storage`:
Sandwich storage mySandwich = sandwiches[_index];
// 如果你只想要一个副本,可以使用`memory`:
Sandwich memory anotherSandwich = sandwiches[_index + 1];
除 public 和 private 属性之外,Solidity 还使用了另外两个描述函数可见性的修饰词:internal(内部) 和 external(外部)。
internal 和 private 类似,不过,如果某个合约继承自其父合约,这个合约即可以访问父合约中定义的“内部”函数。
external 与public 类似,只不过这些函数只能在合约之外调用 - 它们不能被合约内的其他函数调用。
请注意,这个过程虽然看起来像在定义一个合约,但其实内里不同:
首先,我们只声明了要与之交互的函数 —— 在本例中为 getNum —— 在其中我们没有使用到任何其他的函数或状态变量。
其次,我们并没有使用大括号({ 和 })定义函数体,我们 单单用分号(;) 结束了函数声明。
编译器就是靠这些特征认出它是一个接口的。
contract NumberInterface {
function getNum(address _myAddress) public view returns (uint);
}
在合约中使用接口:
contract MyContract {
address NumberInterfaceAddress = 0xab38...;
NumberInterface numberContract = NumberInterface(NumberInterfaceAddress);
// 现在变量 `numberContract` 指向另一个合约对象
function someFunction() public {
// 现在我们可以调用在那个合约中声明的 `getNum`函数:
uint num = numberContract.getNum(msg.sender);
// ...在这儿使用 `num`变量做些什么
}
}
1、onlyOwner: 可以限制第三方对 setKittyContractAddress的访问,除了我们自己,谁都无法去修改它
modifier onlyOwner() {
require(msg.sender == owner);
_;
}
// 修改这个函数:
function setKittyContractAddress(address _address) external onlyOwner {
kittyContract = KittyInterface(_address);
}
2、函数修饰符也可以带参数
// 存储用户年龄的映射
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 使用自己的本地时间单位。变量 now 将返回当前的unix时间戳(自1970年1月1日以来经过的秒数)
在 Solidity 中,数学运算很直观明了,与其它程序设计语言相同:
x ** y (x 的 y次方 // 例如: 5 ** 2 = 25)
例如: uint x = 5 ** 2; // equal to 5^2 = 25