solidity教程—看了就会写!!!
文章目录
- solidity教程—看了就会写!!!
-
- 一、solidity 模板
- 二、数据类型
- 三、以太坊的基础操作(以太坊区块链底层平台可以使用)
- 四、区块链操作
-
- 1. 映射
- 2. 函数的重载
- 3. 函数命名的参数
- 4. 返回值特性(没什么用)
- 5. 变量的生命周期与作用域
- 6. 面对对象——合约的继承和合约的访问权限
- 7. 废弃的constant静态修饰
- 8. 构造函数
- 9. 函数 modifier 的强大功能
- 10. 全局变量自动 getter 函数
- 11. 复杂的映射 mapping 使用
- 12. 继承中函数的重载
- 13. 多重继承
- 14. 合约的销毁
- 15. storage 引用详解
- 16. 结构体定义与初始化
- 17. 结构体中的 mapping 特性
- 18. 结构体中的 storage 特性
- 19. 结构体 memory 转 storage
- 20. 结构体 storage 转 memory
- 21. 结构体中 memory 转 memory
- 22. 枚举体
- 23. 事件
- 24. 索引
- 25. 接口
- 26. 批量赋值
- 27. 智能协议的永固性
- 28. 省 gas 的招数:结构封装 (Struct packing)
- 30. 时间单位
- 31. “view” 函数不花 “gas”
- 32. 异常处理
一、solidity 模板
pragma solidity ^0.4.25; // 版本号
contract Helloworld{
string Myname = "zzz"; // 属性
// 在 solidity 中,所有的函数都需要有权限,权限种类和 JAVA 几乎相同
// 当每一个智能合约一旦部署到区块链之上时,它会一直存在,不同部署的智能合约互不影响
// view 在获取数据的时候不会消耗燃料,意味着它只能读取数据不能更改数据
function getName() public view returns(string){ // 获取Myname 的函数
return Myname;
}
// 在获取输入修改的数据的时候会消耗燃料,会修改到数据
function changName(string _newName) public{ // 修改 Myname 的函数
Myname = _newName;
}
// pure 在获取输入的数据时,不会消耗燃料,不会修改到数据,表明这个函数甚至都不访问应用里的数据
function pureTest(string _name) pure public returns(string){
return _name;
}
}
二、数据类型
1. bool
默认为 false
pragma solidity ^0.4.25; // 版本号
contract Helloworld{
bool a;
int num1 = 100;
int num2 = 200;
function getBool() public view returns(bool){
return a; // false
}
function panduan() public returns(bool){
return num1 == num2; // false
}
}
2. int /uint
区别:int 有正负号,uint 没有负号
幂运算:2 ** 2 =4
pragma solidity ^0.4.25; // 版本号
contract Helloworld{
int num1 = 100; // int256 简写为 int
uint num2 = 200; // uint256 简写为 uint
uint8 num3 = 2; // 在类型这里可以限制范围,如此限制,表示 0~255 的数字可以使用,因为是 uint 没有负号,所以从 0 开始,因为限制范围为 8 ,所以表示有 8 个二进制的 1 ,将其转换为10进制就为 255。 支持关键字 uint8 到 uint256 (无符号,从 8 位到 256 位)以及 int8 到 int256,以 8 位为步长递增。如:uint16、uint24、uint32
}
3. 位运算
运算规则:先将其转换为 2进制 的形式,然后对其进行位运算。
- & 与 双操作
- | 或 双操作
- ~ 异或(非) 单操作
- ^ 位取反 双操作 一样就为 0 ,不一样就为 1
- << 左移位 单操作 例如:<<1 表示二进制左移 1 位
- >> 右移位 单操作
4. 定长字节数组
bytes1~bytes32 : bytes1 就表示 2进制 的8位,每加一就 2进制 增加8位,一直到 256位
pragma solidity ^0.4.25; // 版本号
contract Bytes32{
// 当我们给变量添加了一个 public 时,表示会自动生成一个 get 函数,用于调用。
// 如:bytes1 public num1 ==>>生成 function num1() external view returns (bytes1) { return num1; }
bytes1 public num1 = 0x7a; // 0111 1010
// 7 a
bytes1 public num2 = 0x68; // 0110 1000
// 6 8
// 给其赋值的只能是 16进制 的数据:一个16进制的字符有4位、有4个二进制的字符、有半个字节;一个二进制的字符有1位;一个字节有 8位、有两个16进制的字符。
// 如:2进制:0111 1010 ===> 16进制:0x7a
// 其返回类型如果为 bytes... 则返回值为 16进制
function weihuo() view public returns(bytes1){
return num1 | num2; // 0x7a 2进制为:0111 1010
}
// 利用 .length 来获取某个字节的长度
// 返回的字节长度是被固定住的,如果为 bytes1 那么字节长度就一定为 1 ,不能被修改。就算没用使用完,返回的长度仍然固定不变的。
function getLength() view public returns(uint){
return num1.length; // 1 因为返回的类型为 uint ,所以返回该字节长度为 1
}
}
5. 动态字节数组
pragma solidity ^0.4.25; // 版本号
contract DynamicByte{
bytes public name = new bytes(2); // 创建动态字节数组对象(初始化为2字节),假设每次调用其他函数前都先调用该自动生成的函数
function InitName(){ // 动态字节数组初始化
name[0] = 0x7a;
name[1] = 0x68; // 调用自动生成的函数前,返回 0x0000 , 调用函数后,返回 0x7a68
}
function getLength() view public returns(uint){
return name.length; // 2 固定给的
}
function changeName() public{
name[0] = 0x88; // 调用该函数后,在调用获取长度的函数时,返回值为 0x8800
}
function changeLength() public{
name.length = 5; // 调用该函数后,再调用获取长度的函数时,返回值为 5 ,再调用获取字节数组的函数时,返回值为 0x7a68000000
}
function pushtest() public{ // 字节数组的函数 .push(需要添加的 16进制) 在字节数组末尾添加
name.push(0x99); // 如果该字节数组中已经满 字节 了,则继续添加时,字节数组会自动扩充 字节容量 ,如该字节数组: 没有调用其他任何函数的 前提下,调用该函数后的 字节数组为 0x000099 ,字节数组长度为 3
// 注:array.push() 在数组的 尾部 加入新元素 ,所以元素在数组中的顺序就是我们添加的顺序
}
}
6. 结构体类型的数组和散列函数 keccak256
pragma solidity ^0.4.19;
contract ZombieFactory{
struct Zombie {
string name;
uint dna;
}
Zombie[] public zombies;
function createZombie(string _name,uint _dna){
zombies.push(Zombie(_name,_dna));
}
// Ethereum 内部有一个散列函数keccak256,它用了SHA3版本。一个散列函数基本上就是把一个字符串转换为一个256位的16进制数字。字符串的一个微小变化会引起散列数据极大变化。
function _generateRandomDna(string _str) public view returns(uint) {
uint rand = uint(keccak256(_str));
return rand % zombies[0].dna;
}
}
7. 字符串类型
字符串类型和 JS 、SQL等语言差不多,可以用单引号或者双引号。
pragma solidity ^0.4.25; // 版本号
contract String{
string name = 'zhengjianxun';
function getLength() view returns(uint){
return bytes(name).length; // 12 将 name 由 string 强制转换为 byte 类型,说明一个英文符号就只占一个字节。
}
function getName16_0() returns(bytes1){
return bytes(name)[0]; // 0x7a 就是 z 的 16进制
}
function getName16S() view returns(bytes){
return bytes(name); // 0x7a68656e676a.......
}
function changeName2(){
bytes(name)[0] = 'L'; // 在调用 getName()函数 之前,若先调用了该函数,则会返回:0x4c68656e76a......
}
}
8. string处理中文和特殊字符
pragma solidity ^0.4.25; // 版本号
contract ChinesString{
string name1 = '!@#¥%&';
string name2 = '郑建勋';
function getLength1() view returns(uint){
return bytes(name2).length; // 9 说明一个中文字符只占 3 个字节
}
function getNameBytes1() view returns(bytes){
return bytes(name2); // 0x98391e5bbbae58b8b
}
function getLength2() view returns(uint){
return bytes(name1).length;
}
function getNameBytes2() view returns(bytes){
return bytes(name1);
}
}
9. 字节数组的转换
固定长度字节数组之间的互相转换
pragma solidity ^0.4.25; // 版本号
contract Bytes32To{
bytes12 name = 0x7a68656e676a69616e78756e;
function changeBytes1() view returns(bytes1){
return bytes1(name); // 0x7a 截取一个字节长度的字符
}
function changeBytes2() view returns(bytes2){
return bytes2(name); // 0x7a68 截取两个字节长度的字符
}
function changeBytes16() view returns(bytes16){
return bytes16(name); // 0x7a68656e676a69616e78756e0000;
}
}
固定长度字节数组转换为动态长度字节数组
pragma solidity ^0.4.25; // 版本号
contract Bytes32ToDynamicByte{
bytes12 name = 0x7a68656e676a69616e78756e;
function fixBytesTodynamicBytes() view returns(bytes){
//return bytes(name); // 错误!!! 不可以这样直接转换
// 在函数体内部声明的 数组 类型对象,需要加 memory 进行修饰,函数体外部则不需要。
bytes memory newName = new bytes(name.length);
// 在 Solidity 中,for 循环里面的初始变量 i 不可以是 int 类型,必须是 uint 类型。
for(uint i = 0;i<name.length;i++){
newName[i] = name[i];
}
return newName; // 0x7a68656e676a69616e78756e
}
}
**DynamicByte 转换为 string **
pragma solidity ^0.4.25; // 版本号
contract DynamicByteToString{
bytes name = new bytes(2);
function Init(){ // 初始化
name[0] = 0x7a;
name[1] = 0x68;
}
function bytesToString() view returns(string){
return string(name); // zh 成功将 16进制 的字节数组转换为字符串
}
}
Bytes12 与 String 互相转换 (以太坊区块链底层平台可以使用)
pragma solidity ^0.4.25; // 版本号
contract Bytes12ToString{
// bytes2 name = 0x7a68;
// function changeIt() view returns(string){
// return string(name); // 静态Bytes不可以转换String
// }
// 输入的是多少个字节,就必须是固定的多少字节
function bytes12ToString(bytes12 _newname) view public returns(string){
bytes memory newname = new bytes(_newname.length);
for(uint i=0; i<_newname.length; i++){
newname[i] = _newname[i];
}
return string(newname);
}
}
10. 固定数组
pragma solidity ^0.4.25; // 版本号
contract FixArray{
// 创建固定数组类型并初始化
uint[5] arr = [1,2,3,4,5];
uint[] result = new uint[](5);
// 初始化函数
function Init(){
arr[0] = 100;
arr[1] = 200;
}
// 获取固定数组
function getArrayContent() view returns(uint[5]){
return arr;
}
// 固定数组元素求和
function getSun() view returns(uint){
uint sun = 0;
for(uint i=0; i<arr.length; i++){ // 这里的数组是可以获取长度的
sun += arr[i]; // 但是不可以修改其长度!!!
} // 也不可以给数组添加数据,编 译不通过。
return sun;
}
}
11. 动态数组
pragma solidity ^0.4.25; // 版本号
contract DynamicArray{
// 创建动态数组类型并初始化
uint[] arr = [1,2,3,4,5];
// 初始化函数
function Init(){
arr[0] = 100;
arr[1] = 200;
}
// 获取动态数组
function getArrayContent() view returns(uint[]){
return arr;
}
// 获取动态数组长度
function getLength() view returns(uint){
return arr.length;
}
// 动态数组元素求和
function getSun() view returns(uint){
uint sun = 0;
for(uint i=0; i<arr.length; i++){
sun += arr[i];
}
return sun;
}
// 修改动态数组长度
function changeLength(){
arr.length == 10;
}
// 给动态数组末尾添加元素
function pushContent(){
arr.push(6);
}
}
12. 固定二维数组
pragma solidity ^0.4.25; // 版本号
contract FixTwoArray{
// 创建固定二维数组类型并初始化
// 在 solidity 中,二维数组的声明方式和其他语言不同,是反过来的,前面是列,后面是行
uint[2][3] arr = [[1,2],[3,4],[5,6]];
// 获取固定二维数组的某个元素的长度(也是该二维数组的列数)
function getColLength() view returns(uint){
return arr[0].length; // 2
}
// 获取固定二维数组的长度(也是该二维数组的行数)
function getRowLength() view returns(uint){
return arr.length; // 3
}
// 固定二维数组和固定数组一样,是不可以修改该长度的(行数)!!!
// function changeRowLength(){
// arr.length = 10;
// }
// 获取固定二维数组
function getTwoArrayContent() view returns(uint[2][3]){
return arr; // 以按顺序输出的方式输出
}
// 固定二维数组元素求和
function getSum() view returns(uint){
uint sun = 0;
for(uint i=0; i<arr.length; i++){
for(uint j=0; j<arr[i].length; j++){
sun += arr[i][j]; // 声明和获取方式相反,i 为行,j 为列!!!
}
}
return sun;
}
// 修改固定二维数组中某一个数据
// 在 solidity 中,二维数组的声明和获取二维数组中某个数据的方式是不同的,声明是反的,获取是正的!!!
function changeTwoArrayContent(){
arr[1][0] = 100; // 修改了第 2 列,第 1 行的数据
}
}
13. 动态二维数组
pragma solidity ^0.4.25; // 版本号
contract DynamicTwoArray{
// 创建动态二维数组类型并初始化
// 在 solidity 中,二维数组的声明方式和其他语言不同,是反过来的,前面是列,后面是行
uint[][] arr = [[1,2],[3,4],[5,6]];
// 获取动态二维数组的某个元素的长度(也是该二维数组的列数)
function getRowLength() view returns(uint){
return arr[1].length; // 2
}
// 获取动态二维数组的长度(也是该二维数组的行数)
function getColLength() view returns(uint){
return arr.length; // 3
}
// 动态二维数组可以修改长度(行数)
function changeColLength(){
arr.length = 10;
}
// 动态二维数组可以修改某个元素的长度(列数)
function changeRowLength(){
arr[0].length = 10;
}
// 目前 solidity 还不可以获取动态二维数组
// function getTwoArrayContent() view returns(uint[][]){
// return arr;
// }
// 动态二维数组元素求和
function getSum() view returns(uint){
uint sun = 0;
for(uint i=0; i<arr.length; i++){
for(uint j=0; j<arr[0].length; j++){
sun += arr[i][j]; // 声明和获取方式相反,i 为行,j 为列!!!
}
}
return sun;
}
// 修改动态二维数组中某一个数据
// 在 solidity 中,二维数组的声明和获取二维数组中某个数据的方式是不同的,声明是反的,获取是正的!!!
function changeTwoArrayContent(){
arr[1][0] = 100; // 修改了第 2 行,第 1 列的数据
}
// 获取动态二维数组中某个元素
function getTwoArrayContent(uint col,uint row) view public returns(uint){
return arr[col][row];
}
}
13. 数组字面量
pragma solidity ^0.4.25; // 版本号
contract ArrayLiterals{
// function getArraLiterals() returns(uint[3]){
// return [1,2,3]; // 报错! 因为返回值是默认 uint256 类型的,而我们 return 返回的是 uint8 类型的。
// }
// function getArraLiterals1() returns(uint[3]){
// return [256,2,3]; // 依然报错! 原因和上述相同,我们 return 返回的是什么类型,取决于我们返回的数据的最大值在什么类型的范围内(最小匹配原则,最小是指类型)。当前的最大值为 256,其返回值的范围在类型 uint16 里面,所以依然报错。
// }
function getArraLiterals2() view returns(uint8[3]){
return [1,2,3]; // 解决报错的办法1:将返回值的范围类型进行固定
}
function getArraLiterals3() view returns(uint[3]){
return [uint(1),2,3]; // 解决报错的办法2:将我们返回的第一个值的范围类型进行强制转换
}
// 字面量一般用于输入数组参数列表到一个函数中完成合约交互的作用
function getArraLiterals4(uint[3] grade) view returns(uint){
uint sum = 0;
for(uint i=0; i<grade.length; i++){
sum += grade[i];
}
return sum;
}
}
14. 地址类型成员变量
pragma solidity ^0.4.25; // 版本号
contract AddressTest{
// 地址分为合约地址和账户地址:合约地址是部署合约时,随机生成的;账户地址是不变的。
address public account = 0x05fd1b1a7e82668a73a175b999ed9e4df3ca9c61;
address public account1 = 0x3d2544e7c615ed47e22ec75b19aab93983a5343e;
// 地址类型 address 其实就是和 uint160 一样的存储方式,都是 20 个字节,160位。
// 所以可以将 address 类型和 uint160 类型进行相互转换
function changeIt() view returns(uint160){
return uint160(account);
}
function changeIt1() view returns(address){
return address(34189403436568948582594661148477267543495187553);
}
// 由于地址是数字存储的,所以可以比较其大小
function check() view returns(bool){
return account > account1;
}
}
三、以太坊的基础操作(以太坊区块链底层平台可以使用)
1. 使用钱包转移资金
pragma solidity ^0.4.25; // 版本号
contract PayAbleTest{
// ether:以太币;wei:位;一个以太币 等于 10^18
// payable 关键字代表我们可以通过这个函数让账户地址给我们的合约地址充值以太币(转账)
function pay() payable{
// 默认是账户地址转账给合约地址
}
// 返回当前账户的地址
function getAddress view returns(address){
return msg.sender; // msg.sender 是当前账户地址的地址
}
// 上述函数的转账方式相当于使用 transfer 的方法(函数) ,this 是指当前的合约地址,msg.value 是当前账户地址的以太币
function transfer1() payable{
this.transfer(msg.value);
}
// 使用 transfer方法(函数) 完成当前账户地址给指定地址转账
function transfer2(address account) payable{
account.transfer(msg.value);
}
// send()函数 会对转账是否成功进行一个 bool 类型的提示返回,如果转账成功返回 true,且成功转账;否则返回 false,且无法转账。
function sendMoney(address account) payable returns(bool){
return account.send(10 ether);
// 手动转账,如果当前账户地址 value 小于指定金额,转账失败,返回 false,否则成功,返回 true;
}
//使用 balance属性 获取合约地址上的账户余额,当前的 this 表示的是当前所部署的合约地址
function getBalance() view returns(uint){
return this.balance;
}
// 可查看当前的 this 是什么?
function getThis() view returns(address){
return this;
}
// 使用 balance属性 获取账户地址上的账户金额(也可以是某个地址的账户金额)
function getRandomBalance(address random) view returns(uint){
return random.balance;
}
}
2.以太坊中的全局属性
区块
/* blockhash (uint blockNumber) returns (bytes32):返回给定区块号的哈希值,只支持最近的 256 个区块且不包含当前区块。 在 Solidity 0.4.22 之前这个属性是 block.Blockhash(uint blockNumber)。
- block.coinbase (address): 当前块矿工的地址,括号中表示返回值的类型。
- block.difficulty (uint): 当前块的难度。
- block.gaslimit (uint): 当前块的 gaslimit。
- block.number (uint): 当前区块的块号。
- block.timestamp (uint): 当前块的 Unix 时间戳(从 1970/1/1 00:00:00 UTC 开始所经过的秒数)。
- gasleft() (uint256): 获取剩余 gas。
消息
- msg.data (bytes): 完整地调用数(calldata)。
- msg.gas (uint): 当前调用发起人还剩的 gas。
- msg.sender (address): 当前调用发起人的地址。
- msg.sig (bytes4): 调用数据(calldata)的前四个字节(例如,函数标识 符)。
- msg.value (uint): 这个消息所附带的以太币, 单位为 wei。
- now (uint): 当前块的时间戳(block.timestamp 的别名)。
- tx.gasprice (uint): 交易的 gas 价格。
- tx.origin (address): 交易的发送者(全调用链)。
msg 的所有成员值,如 msg.sender、 msg.value 的值可以因为每一次外部函数调用或库函数调用发生变化(因为 msg 就是和调用相关的全局变量)。对于同一个链上连续的区块来说, 当前区块的时间戳会大于上一个区块的时间戳。 为了可扩展性的原因,只能查最近的 256 个块, 其他的将返回 0 */
pragma solidity ^0.4.25; // 版本号
contract AddressTest{
// 获取当前区块的难度
function getDifficulty() view returns(uint){
return block.difficulty;
}
// 获取当前区块的块号
function getNumber() view returns(uint){
return block.number;
}
}
四、区块链操作
1. 映射
- 只有 4 种类型可以做映射的键 key(bool、int、address、string)
- 任何类型都可以做映射的值 value(任何类型)
- 无法 naiive 的遍历整个 mapping
- 赋值 employees[key] = value
- 取值 value = employee[key]
- value 是引用,在 storage 上存储,可以直接修改
- 当 key 不存在,value = type’s default(类型的默认值)
pragma solidity ^0.4.25; // 版本号
// 利用映射完成注册信息效果
contract mappingTest{
// 定义映射 mapping 通过前者映射后者
mapping(address => uint) idmapping;
mapping(uint => string) namemapping;
// 注册用户总数量
uint public sum = 0;
// 输入注册用户的姓名,并完成信息映射
function register(string name){
address account = msg.sender;
sum++; // 每当使用新当前用户地址注册,则注册用户总数会累加
idmapping[account] = sum; // 完成 用户地址 和 id 的映射
namemapping[sum] = name; // 完成 id 和 用户姓名 的映射
}
// 通过地址查询 id
function getIdByAddress(address area) view returns(uint){
return idmapping[area];
}
// 通过 id 查询 用户姓名
function getNameById(uint id) view returns(string){
return namemapping[id];
}
}
2. 函数的重载
pragma solidity ^0.4.25; // 版本号
contract chongZai{
uint public a;
function test1(uint num){
a = num;
}
function test1(uint8 num){
a = num;
}
function testIt(){
// test1(100); // 报错! 因为两个 test1 的类型虽然不相同,但是当用 100 去调用的时候,由于两个 test1 的参数类型范围都包括了 100 ,所以报错!
test1(256); // 使用参数 256 去调用 test1 ,不会报错,调用到了第一个 test1 ,因为第二个的参数类型范围不包括 256。
}
// function test2(uint160 num){
// a = 100;
// }
// function test2(address num){
// a = 200;
// }
// 虽然上述两个重载函数可以通过编译,但是无论传入的参数的是数字还是地址,就算没有超过其参数类型范围,也都不能被调用,因为这两个的参数类型被解析出来是一样的,无法分辨,所以会报错!在金联盟底层区块链中和以太坊底层区块链中的情况不相同。
// function test(){
// test2(100); // 在两种区块链中情况相同
// test2(0x3d2544e7c615ed47e22ec75b19aab93983a5343e);// 在两种区块链中情况不同
// }
}
3. 函数命名的参数
pragma solidity ^0.4.25; // 版本号
contract funcParam{
uint public num;
string public name;
function setParam(uint _num,string _name){
num = _num;
name = _name;
}
// 通过正常方式赋值
function Test(){
setParam(10,"zzz");
}
// 通过函数命名参数方式1赋值
function Test1(){
setParam({_num:100,_name:"zzz"});
}
// 通过函数命名参数方式2赋值
function Test2(){
setParam({_name:"lqq",_num:200});
}
}
4. 返回值特性(没什么用)
pragma solidity ^0.4.25; // 版本号
contract funReturn{
// 返回值可以有名字
function returnTest1() view returns(uint mul){
uint a = 10;
return a; // 0 uint256:mul 10
}
// 可以给返回值赋值
function returnTest2() view returns(uint mul){
mul = 100; // 0 uint256:mul 100
}
// 如果有 return ,则返回值以 return 的返回值为主
function returnTest3() view returns(uint mul){
uint a = 10;
mul = 100;
return a; // 0 uint256:mul 10 ,以 return 为准
}
function returnTest4() view returns(uint mul){
uint a = 10;
mul = 100;
return 1; // 0 uint256:mul 1 ,以 return 为准
}
// 函数可以有多个返回值,直接返回常量
function returnTest5() view returns(uint add,uint mul){
return (10,60); // 0 uint256:add 10
// 1 uint256:mul 60
}
// 函数可以有多个返回值,给多返回值赋值,传入数据 3,4
function returnTest6(uint a,uint b) view returns(uint add,uint mul){
add = a+b; // 0 uint256:add 5
mul = a*b; // 1 uint256:mul 6
}
// 函数可以有多个返回值,直接 return(param list) ,传入数据 3,4
function returnTest7(uint a,uint b) view returns(uint add,uint mul){
return (a+b,a*b); // 0 uint256:add 7
// 1 uint256:mul 12
}
// 进行数据交换,输入数据 4,5
function returnTest8(uint a,uint b) view returns(uint a1,uint a2){
return (b,a); // 0 uint256:a1 5
// 1 uint256:a2 4
}
}
5. 变量的生命周期与作用域
pragma solidity ^0.4.25; // 版本号
contract Vaulecopy{
uint a = 100; // 全局变量
uint b = 200;
//uint a = 300; // 报错!重命名
function test1() view returns(uint){
uint a = 299; // 局部变量
return a; // 299
}
// 传入数据 300
function test2(uint a) view returns(uint){
// uint a; // 报错!重定义
// for(uint a=0;a<5;a++){} // 报错!重定义
a = 700; // 传入的变量也是局部变量,传入的值会被赋值覆盖
return a; // 700
}
}
6. 面对对象——合约的继承和合约的访问权限
访问权限:
1). external:
外部函数是合约接口的一部分,所以我们可以从其它合约或通过交易来发起调用。一个外部函数father() (假设该函数被 external 修饰),不能通过内部的方式来发起调用,但可以通过间接的合约内部调用(如 father()不可以,但可以通过 this.father())。如果是子孙合约调用该外部函数,则和前面一样,需要通过间接的合约内部调用(如 father()不可以,但可以通过 this.father())。外部函数在接收大的数组数据时更加有效。 不可以用于修饰变量!修饰函数时较为特殊!
2). public:
公开函数是合约接口的一部分,可以通过内部,或者消息来进行调用。对于public类型的状态变量,会自动创建一个访问器。对于变量来说,public 只可以修饰全局变量,不可以修饰局部变量。合约中的方法默认为 public。
3). internal:
这样声明的函数和状态变量只能通过内部访问。如在当前合约中调用,或继承的合约里调用。需要注意的是不能加前缀this,前缀this是表示通过外部方式访问。合约中的状态变量默认为 internal。在继承中,子合约只能继承父合约中的所有 public 类型和 internal 类型的状态变量。
4). private:
私有函数和私有状态变量仅在当前合约中可以访问。都不可被外部访问(调用),且不可被继承。
私有函数的命名规则最好以 _ 开头,函数的参数也最好以 _ 开头。
pragma solidity ^0.4.25; // 版本号
// 创建一个 grandfather 合约(用于给其他合约继承)
contract grandfather{
uint gudong1 = 100; // 若不加权限修饰符,就默认是 public,可被继承,可以在外部调用
// public 是公共的权限修饰符,可被继承,可以在外部调用
uint public gudong2 = 200;
// internal 是内部的权限修饰符,可被继承,不可以在外部调用
uint internal gudong3 = 300;
// private 是私有的权限修饰符,不可被继承,不可以在外部调用
uint private gudong4 = 400;
function zhongdi() public view returns(string){
return "zhongdi";
}
}
// 创建一个 father 合约去继承父合约 grandfather
contract father is grandfather{
uint public money = 10000;
function dahan1() view returns(string){
return "dahan1"; // 访问权限为 默认的public,可被继承
}
function dahan2() public view returns(string){
return "dahan2"; // public 是公共的权限修饰符,可被继承
}
function dahan3() internal view returns(string){
return "dahan3"; // internal 是内部的权限修饰符,可被继承
}
function dahan4() private view returns(string){
return "dahan4"; // private 是私人的权限修饰符,不可被继承
}
function dahan5() external view returns(string){
return "dahan5"; // external 是外部的权限修饰符,特殊继承
}
function dahantest() public view{
//this.dahan4(); // 报错!因为 this. 表示使用外部方法调用
dahan4(); // 通过,因为 private 可以被内部调用(访问)
}
}
// 创建一个 son 合约去继承父合约 father
contract son is father{
// 获取从父合约继承过来的 money
function getMoney() view returns(uint){
return money;
}
// 获取从爷合约继承过来的 gudong1 和 gudong2 和 gudong3 变量
function getGudong1() view returns(uint){
return gudong1; // 访问权限为 默认的public,可被继承
}
function getGudong2() view returns(uint){
return gudong2; // 访问权限为 公共的
}
function getGudong3() view returns(uint){
return gudong3; // 访问权限为 内部的
}
// 获取从父合约继承过来的 dahan1 和 dahan2 和 dahan3 和 dahan5 函数
function test1() view returns(string){
return dahan1(); // 访问权限为 默认的public
}
function test2() view returns(string){
return dahan2(); // 访问权限为 公共的
}
function test3() view returns(string){
return dahan3(); // 访问权限为 内部的
}
function test5() view returns(string){
return this.dahan5(); // 访问权限为 外部的
}
// 获取从爷合约继承过来的 zhongdi 函数
function test4() view returns(string){
return zhongdi();
}
}
// 创建一个 externalTest 合约(当作一个外部合约)
contract externalTest{
// 实例化 father 合约对象,对象名为 f
son s = new son();
// 通过对象名 f 来调用 dahan5() 函数,相当于使用外部合约来调用外部函数
function externalTestIt() public view returns(string){
return s.test1();
}
}
7. 废弃的constant静态修饰
pragma solidity ^0.4.25; // 版本号
contract constractTest{
string Myname = "zzz"; // 属性
// 在输入修改的数据的时候会消耗燃料,会修改到数据
function changName(string _newName) public{ // 修改 Myname 的函数
Myname = _newName;
}
// view 只是在查看数据的时候使用,单纯获取(查看)数据的时候不会消耗燃料
function getName() public view returns(string){ // 获取Myname 的函数
return Myname;
}
// pure 完全禁止读写状态变量!但是可以读写临时变量
function pureTest(string _name) pure public returns(string){
return _name;
}
// 在函数中使用 constant 关键字时,4.0版本中因为他是和 view 关键字等价,在 5.0版本中将会废弃掉 constant 关键字。
function constTest(uint a) public constant returns(uint){
return a;
}
function constTest1() public constant returns(string){
return Myname;
}
// 全局变量有 constant 属性,局部变量不用于该属性。
// 全局变量加上了 constant 属性后,其值是不能被改变的。
// 当前版本支持拥有 constant 属性的数据类型有:int、uint、string、bytes1~bytes32。
}
8. 构造函数
pragma solidity ^0.4.25; // 版本号
contract gouzao{
uint public a;
address public b;
// 老版的构造函数,构造函数不可以重载
function gouzao(){
a = 100;
}
// function gouzao(uint _a, address _b){
// a = _a;
// b = _b;
// }
// 新版的构造函数,构造函数不可以重载
// constructor(){
// a = 100;
// }
// constructor(uint _a, address _b){
// a = _a;
// b = _b;
// }
// constructor(){
// b = msg.sender;
// }
}
9. 函数 modifier 的强大功能
pragma solidity ^0.4.25; // 版本号
// 如果同一个函数有多个 修饰器 modifier,它们之间以空格隔开,修饰器 modifier 会依次检查执行。
// 可继承,覆盖,但是不可以重载
contract modifierTest1{
address owner;
uint num = 0;
constructor(){
owner = msg.sender;
}
// 定义 modifier ,相当于有一个可以封装代码的功能
modifier OnluOwner{
// require 是判断语句,相当于 if
require(msg.sender == owner);
_; // 动态添加
}
// 附加上了 modifier ,先执行了 require(msg.sender == owner); ,如果判读成立,则继续执行,否则回滚报错!
function change(uint _num) OnlyOwner{
num = _num;
}
}
//require使得函数在执行过程中,当不满足某些条件时抛出错误,并停止执行:
function sayHiToVitalik(string _name) public returns (string) {
// 比较 _name 是否等于 "Vitalik". 如果不成立,抛出异常并终止程序
// (敲黑板: Solidity 并不支持原生的字符串比较, 我们只能通过比较
// 两字符串的 keccak256 哈希值来进行判断)
require(keccak256(_name) == keccak256("Vitalik"));
// 如果返回 true, 运行如下语句
return "Hi!";
}
pragma solidity ^0.4.25; // 版本号
// 利用映射和 modifier 完成注册信息效果
contract modifierMappingTest{
// 定义映射 mapping 通过前者映射后者
mapping(address => uint) idmapping;
mapping(uint => string) namemapping;
// 注册用户总数量
uint sum = 0;
// 用户只能被注册一次,表示当如果当前地址已经注册,则该 id 不为 0 ,则不执行后面的注册函数里的操作,否则执行后面的注册函数里的操作。
modifier control{
require(idmapping[msg.sender] == 0);
_;
}
// 输入注册用户的姓名,并完成信息映射
function register(string name) control{
address account = msg.sender;
sum++; // 每当使用新当前用户地址注册,则注册用户总数会累加
idmapping[account] = sum; // 完成 用户地址 和 id 的映射
namemapping[sum] = name; // 完成 id 和 用户姓名 的映射
}
// 通过地址查询 id
function getIdByAddress(address area) view returns(uint){
return idmapping[area];
}
// 通过 id 查询 用户姓名
function getNameById(uint id) view returns(string){
return namemapping[id];
}
}
pragma solidity ^0.4.25; // 版本号
// 利用 modifier 完成游戏人物设计
contract modifierTest2{
uint public level = 100;
string public name;
uint public DNA;
// 封装一个判断当前等级是否大于所需要的等级的判断代码
modifier contrlLevel(uint needLevel){
require(level >= needLevel);
_;
}
// 传一个指定等级到 modifier ,判断是否成立,如果成立,则改名,否则不改名。
function changeName() contrlLevel(2){
name = "zzz";
}
// 传一个指定等级到 modifier ,判断是否成立,如果成立,则修改 DNA ,否则不修改。
function changeDNA() contrlLevel(10){
DNA = 999;
}
}
pragma solidity ^0.4.25; // 版本号
// modifier 的执行顺序和互相穿插
contract modifierTest3{
uint public a = 0;
// 封装两个用于给 a 赋值的代码
modifier mod1(){
a = 1;
_;
a = 2;
}
modifier mod2(){
a = 3;
_;
a = 4;
}
// 调用这第一个封装代码,则执行顺序为:a=1——a=100——a=2
function test1() mod1{
a = 100;
}
// 调用这两个封装代码,则执行顺序为:a=1——a=3——a=100——a=4——a=2
function test2() mod1 mod2{
a = 100;
}
}
10. 全局变量自动 getter 函数
pragma solidity ^0.4.25; // 版本号
contract getter{
uint public num = 0;
// 1.当我们创建了一个和 public 类型的全局变量名字相同的函数时,该 public 类型的全局变量自动生成的默认的函数就会消失,因为相同。
// function num() external view returns(uint){
// return num;
// }
// 2.当我们想在当前合约中调用当前合约中的 public 类型的变量所生成的默认函数时,需要用 this. 来修饰。因为默认生成的函数是 external 修饰的。
function test() view returns(uint){
return this.num();
}
// 3.在映射中,如果该映射使用了 public 来修饰,则也会自动生成一个默认函数:
mapping(uint => string) public map;
function test2(){
map[1] = "zzz";
}
//function map(uint key) external returns(string){
//
//}
// 所以也可以通过 this. 来调用:
function test3() view returns(string){
return this.map(1);
}
}
11. 复杂的映射 mapping 使用
pragma solidity ^0.4.25; // 版本号
contract mappings{
mapping(uint => mapping(uint => mapping(uint => string))) public map;
function test(){
map[0][1][2] = "zzz";
}
function test1() view returns(string){
return this.map(0,1,2);
}
}
12. 继承中函数的重载
pragma solidity ^0.4.25; // 版本号
contract father{
uint public Money = 1000;
function dahan() view returns(string){
return "打小鼾";
}
}
// 利用继承中的重载将父合约的内容覆盖
contract sonCZ is father{
uint Money = 999999;
function getMoney() view returns(uint){
return Money; // 999999
}
function dahan() view returns(string){
return "打大鼾";
}
function test() view returns(string,string){
father f = new father();
return (f.dahan(),dahan()); // 打小鼾,打大鼾
}
}
13. 多重继承
pragma solidity ^0.4.25; // 版本号
contract father{
uint public height = 180;
uint public Money = 2000;
}
contract mother{
uint public height = 160;
// 构造函数
function mother(uint _h){
height = _h;
}
}
// 多重继承,后继承的会覆盖先继承的内容
contract sonDJC is father,mother{
// 如果子合约有相同的变量或函数,则会覆盖
uint public height = 199;
function getHeight() view returns(uint){
return height; // 160
}
}
// 继承时,可在声明构造函数时,顺便声明父合约的构造函数
contract sonDJC is mother{
uint y;
function sonDJC(uint _y) mother(_y*_y){
y = _y;
}
}
// 还可以在继承时,顺便声明父合约的构造函数
contract sonDJC is mother(199){
uint y;
function sonDJC(uint _y) {
y = _y;
}
}
继承—————抽象合约
pragma solidity ^0.4.25;
contract Parent{ // 抽象合约
function someFunc() returns (uint);
}
contract Child is Parent {
function someFunc() returns (uint) {
return 1;
}
}
14. 合约的销毁
pragma solidity ^0.4.25; // 版本号
contract destruct{
address owner;
uint public money = 0;
constructor(){
owner = msg.sender;
}
function increment(){
money += 10;
}
function kill(){
if(msg.sender == owner){
selfdestruct(owner);
}
}
}
15. storage 引用详解
pragma solidity ^0.4.25; // 版本号
contract memoryTest{
uint[] arrx; // 这个状态变量存储在区块链的网络之上
// 当我传入一个可变长度数组的时候,会在内存中为它分配空间,形参默认为 memory 类型
function test(uint[] arry) returns(uint){
arrx = arry; // 将内存的 arry 拷贝给区块链上的 arrx 变量
// 当我们在函数体内部定义了一个可变长度的数组时,必须手动将其声明为 storage 类型或者 memory 类型
// 我实际上再操作的是区块链上的 arrx
uint[] storage Z = arrx;
// 通过指针实际上修改了区块链上的 arrx 的值
Z[0] = 100;
return arrx[0];
}
// 返回 arrx 的第 a 个元素
function test1(uint a) view returns(uint){
return arrx[a];
}
// 返回 arrx 长度
function test3() view returns(uint){
return arrx.length;
}
}
16. 结构体定义与初始化
pragma solidity ^0.4.25; // 版本号
contract structTest{
struct student1{
uint grade;
string name;
}
struct student2{
// 结构体内部不能包含自己本身,但是可以是动态长度的数组,也可以是映射
//student2 stu;
student2[] stu;
mapping(uint => student2) hahah;
}
student1 stu = student1(99,"stu");
// 给在函数中的结构体初始化赋值时,都要手动将其声明为 storage 类型或者 memory 类型。
function init() view returns(uint, string){
student1 memory s = student1(100, "zzz");
return(s.grade,s.name);
}
// 初始化的第二种方式
function init1() view returns(uint,string){
student1 memory s = student1({grade:100,name:"zzz"});
return(s.grade,s.name);
}
}
在 Solidity 中,有两个地方可以存储变量 —— storage
或 memory
。
Storage
该存储位置存储永久数据,这意味着该数据可以被合约中的所有函数访问。可以把它视为计算机的硬盘数据,所有数据都永久存储。保存在存储区(Storage)中的变量,以智能合约的状态存储,并且在函数调用之间保持持久性。与其他数据位置相比,存储区数据位置的成本较高。
Memory
内存位置是临时数据,比 Storage
便宜。它只能在函数中访问。你可以把它想象成每个单独函数的内存(RAM)。通常,内存数据用于保存临时变量,以便在函数执行期间进行计算。一旦函数执行完毕,它的内容就会被丢弃。
大多数时候你都用不到这些关键字,默认情况下 Solidity 会自动处理它们。 状态变量(在函数之外声明的变量)默认为“存储(storage)”形式,并永久写入区块链;而在函数内部声明的变量是“内存(memory)”型的,它们函数调用结束后消失。
然而也有一些情况下,你需要手动声明存储类型,主要用于处理函数内的 结构体 和 数组 时:
contract SandwichFactory {
struct Sandwich {
string name;
string status;
}
Sandwich[] sandwiches;
function eatSandwich(uint _index) public {
// Sandwich mySandwich = sandwiches[_index];
// 看上去很直接,不过 Solidity 将会给出警告
// 告诉你应该明确在这里定义 `storage` 或者 `memory`。
// 所以你应该明确定义 `storage`:
Sandwich storage mySandwich = sandwiches[_index];
// 这样 `mySandwich` 是指向 `sandwiches[_index]`的指针
// 在存储里,另外
mySandwich.status = "Eaten!";
// 这将永久把 `sandwiches[_index]` 变为区块链上的存储
// 如果你只想要一个副本,可以使用`memory`:
Sandwich memory anotherSandwich = sandwiches[_index + 1];
// 这样 `anotherSandwich` 就仅仅是一个内存里的副本了
// 另外
anotherSandwich.status = "Eaten!";
// 将仅仅修改临时变量,对 `sandwiches[_index + 1]` 没有任何影响
// 如果你想把副本的改动保存回区块链存储
// 不过你可以这样做:
sandwiches[_index + 1] = anotherSandwich;
}
}
17. 结构体中的 mapping 特性
pragma solidity ^0.4.25; // 版本号
contract structTest1{
struct student{
uint grade;
string name;
mapping(uint => string) map;
}
// 在函数体外部定义的结构体类型的变量,都默认是 storage 类型,是在存储上。
student meimei;
// 在函数体内部定义的结构体类型的变量,必须将其手动声明为 storage 类型或者 memory 类型。
function init() view returns(uint,string,string){
student storage s;
s.grade = 100;
s.name = "zzz";
s.map[0] = "hello";
meimei = s;
meimei.map[0] = "world";
return(s.grade,s.name,meimei.map[0]);
}
}
18. 结构体中的 storage 特性
pragma solidity ^0.4.25; // 版本号
contract structTest2{
struct student{
uint grade;
string name;
}
student stu;
// storage 表示引用,会修改区块链上的值:内存中的 meimei 引用内存中的 s , s 引用 区块链上的 stu 。
function test(student storage s) internal{
student storage meimei = s;
// 修改内存中的 meimei 的值,会一起修改到内存中的 s 的值,也会修改到区块链中的 stu
meimei.name = "zzz";
}
function call() view returns(string){
test(stu);
return stu.name; // zzz
}
}
19. 结构体 memory 转 storage
pragma solidity ^0.4.25; // 版本号
contract structTest3{
struct student{
uint grade;
string name;
}
student stu; // 默认 storage 类型
// 函数的形参传递了指针引用。
function test(student s) internal{
// 把内存上的 s 的值赋值给了区块链上的 stu
stu = s;
// 修改函数形参上的 s ,只是修改了其内存中的空间,没有修改掉我的区块链上的空间,因为他们是互相独立的空间。
stu.name = "zzz";
}
function call() view returns(string){
// 内存中开辟空间
student memory tmp = student(100,"tmp");
test(tmp);
return tmp.name; // tmp
}
}
20. 结构体 storage 转 memory
pragma solidity ^0.4.25; // 版本号
contract structTest4{
struct student{
uint grade;
string name;
}
student stu = student(100,"stu"); // 默认 storage 类型
// s 形参是一个引用。
function test(student storage s) internal{
// meimei 是一个内存中的副本,把 s 引用的 stu 的内容复制给了 meimei 这个内存中的对象。
student memory meimei = s;
// 修改 meimei 的值,不会修改到 stu 的值,因为他们是完全独立的空间。
meimei.name = "zzz";
}
// 传递了 stu 过去给 s ,相当于 s 是 stu 的引用,但是 s 赋值给 memory 类型的 meimei ,则是传递了一个副本过去,meimei 另外开辟了一个空间。
function call() view returns(string){
test(stu);
return stu.name; // stu
}
}
21. 结构体中 memory 转 memory
pragma solidity ^0.4.25; // 版本号
contract structTest5{
struct student{
uint grade;
string name;
}
// memory 类型之间的传递,由于 solidity 的优化,是通过指针来传递的。
// 形参都默认为 memory 类型
function test(student s) internal{
student memory lina = s;
lina.name = "zzz";
}
function call() view returns(string){
student memory meimei = student(100,"meimei");
test(meimei);
return meimei.name; // zzz
}
}
22. 枚举体
pragma solidity ^0.4.25; // 版本号
contract enumTest{
// enum 必须要有成员对象,不能为空。不能有汉字,末尾不能加分号。
// enum 是以 uint 类型来存储的,从 uint8 开始,到 256 时,uint8 会变成 uint16 以此类推
enum girl{fengjie,binbin,yuanyuan} // 0,1,2...
girl dateGirl = girl.fengjie; // 枚举变量
function getEnum() view returns(girl){
return girl.fengjie; // 0 返回的是枚举体的下标
}
function oneNightDate() view returns(string){
require(dateGirl == girl.fengjie);
dateGirl = girl.binbin;
return "date with fengjie";
}
???
function seconedNightDate() view returns(string){
require(dateGirl == girl.binbin);
return "date with binbin";
}
}
23. 事件
Solidity中,要定义事件,可以使用event关键字(在用法上类似于function关键字)。然后可以在函数中使用emit关键字触发事件。
示例:
pragma solidity ^0.5.0;
contract Counter {
uint256 public count = 0;
event Increment(address who); // 声明事件
function increment() public {
emit Increment(msg.sender); // 触发事件
count += 1;
}
}
上面的代码中,
1). event Increment(address who) 声明一个合约级事件,该事件接受一个address类型的参数,该参数是执行increment操作的账户地址。
2). emit Increment(msg.sender) 触发事件,事件会记入区块链中。
3). 按照惯例,事件名称以大写字母开头,以区别于函数。
24. 索引
solidity语言中定义事件【可以在事件参数上增加indexed属性,最多可以对三个参数增加这样的属性。加上这个属性,
event Transfer(address indexed _from, address indexed _to, uint indexed amount);
indexed属性在solidity事件中非常重要【过滤当前事件名中,设置了为indexed索引参数值,作为条件判断筛选】
过滤某个地址为发送者的事件:
【非常重要】这里的事件名Transfer有三个参数,如果设置为null,代表所有【必须在solidity中定义为indexed的属性才支持】
let filter = this.contract_instance.filters.Transfer(this.active_wallet.address, null,null);
this.contract_instance.on(filter, (from, to, value, event) => {
console.log("监听发送以太坊事件:");
console.log(`from:${from}+to:${to}+value:${value}`);
});
过滤某个地址为接收者的事件:
【非常重要】这里的事件名Transfer有三个参数,如果设置为null,代表所有【必须在solidity中定义为indexed的属性才支持】
let filter = this.contract_instance.filters.Transfer(null, this.active_wallet.address,null);
this.contract_instance.on(filter, (from, to, value, event) => {
console.log("监听发送以太坊事件:");
console.log(`from:${from}+to:${to}+value:${value}`);
});
过滤指定数量的事件【由于事件默认的参数是hex十六进制,所以筛选的时候也必须是十六进制】:
【非常重要】这里的事件名Transfer有三个参数,如果设置为null,代表所有【必须在solidity中定义为indexed的属性才支持】
// 过滤value为100的事件
let filter = this.contract_instance.filters.Transfer(null,null,"0x100");
// 过滤value为数组中值的事件
let filter = this.contract_instance.filters.Transfer(null,null,["0x99","0x100","0x101"]);
this.contract_instance.on(filter, (from, to, value, event) => {
console.log("监听发送以太坊事件:");
console.log(`from:${from}+to:${to}+value:${value}`);
});
25. 接口
方法一、我们可以在合约中这样使用:
pragma solidity ^0.4.25;
contract NumberInterface { // 抽象合约
function getNum(address _myAddress) public view returns (uint);
}
请注意,这个过程虽然看起来像在定义一个合约,但其实内里不同:
首先,我们只声明了要与之交互的函数 —— 在本例中为 getNum —— 在其中我们没有使用到任何其他的函数或状态变量。
其次,我们并没有使用大括号({ 和 })定义函数体,我们单单用分号(;)结束了函数声明。这使它看起来像一个合约框架。
编译器就是靠这些特征认出它 (NumberInterface) 是一个接口的。
我们可以在合约中这样使用:
contract MyContract {
address NumberInterfaceAddress = 0xab38...;
// 这是FavoriteNumber合约在以太坊上的地址
NumberInterface numberContract = NumberInterface(NumberInterfaceAddress);
// 现在变量 `numberContract` 指向另一个合约对象
function someFunction() public {
// 现在我们可以调用在那个合约中声明的 `getNum`函数:
uint num = numberContract.getNum(msg.sender);
// ...在这儿使用 `num`变量做些什么
}
}
通过这种方式,只要将您合约的可见性设置为public(公共)或external(外部),它们就可以与以太坊区块链上的任何其他合约进行交互。
方法二、我们可以在合约中这样使用:
pragma solidity ^0.4.25;
interface Parent { // 接口
// 只有 function 定义,啥都没有。
function someFunc() returns (uint);
}
contract Child is Parent {
function someFunc() returns (uint) {
return 1;
}
}
26. 批量赋值
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();
}
27. 智能协议的永固性
在你把智能协议传上以太坊之后,它就变得不可更改, 这种永固性意味着你的代码永远不能被调整或更新。
你编译的程序会一直,永久的,不可更改的,存在以太坊上。这就是 Solidity 代码的安全性如此重要的一个原因。如果你的智能协议有任何漏洞,即使你发现了也无法补救。你只能让你的用户们放弃这个智能协议,然后转移到一个新的修复后的合约上。
所以我们在编写智能合约的时候尽量不要将一些地方写成硬代码(不可修改的代码),应该尽量写成函数(后续可修改)。
但这恰好也是智能合约的一大优势。代码说明一切。如果你去读智能合约的代码,并验证它,你会发现,一旦函数被定义下来,每一次的运行,程序都会严格遵照函数中原有的代码逻辑一丝不苟地执行,完全不用担心函数被人篡改而得到意外的结果。
```
28. 省 gas 的招数:结构封装 (Struct packing)
通常情况下我们不会考虑使用 uint
变种,因为无论如何定义 uint
的大小,Solidity 为它保留256位的存储空间。例如,使用 uint8
而不是uint
(uint256
)不会为你节省任何 gas。
除非,把 uint
绑定到 struct
里面。
如果一个 struct
中有多个 uint
,则尽可能使用较小的 uint
, Solidity 会将这些 uint
打包在一起,从而占用较少的存储空间。例如:
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);
所以,当 uint
定义在一个 struct
中的时候,尽量使用最小的整数子类型以节约空间。 并且把同样类型的变量放一起(即在 struct 中将把变量按照类型依次放置),这样 Solidity 可以将存储空间最小化。例如,有两个 struct
:
uint c; uint32 a; uint32 b;和
uint32 a; uint c; uint32 b;
前者比后者需要的gas更少,因为前者把uint32
放一起了。
30. 时间单位
Solidity 使用自己的本地时间单位。
变量 now 将返回当前的unix时间戳(自1970年1月1日以来经过的秒数)。我写这句话时 unix 时间是 1515527488。
注意:Unix时间传统用一个32位的整数进行存储。这会导致“2038年”问题,当这个32位的unix时间戳不够用,产生溢出,使用这个时间的遗留系统就麻烦了。所以,如果我们想让我们的 DApp 跑够20年,我们可以使用64位整数表示时间,但为此我们的用户又得支付更多的 gas。真是个两难的设计啊!
Solidity 还包含秒(seconds),分钟(minutes),小时(hours),天(days),周(weeks) 和 年(years) 等时间单位。它们都会转换成对应的秒数放入 uint 中。所以 1分钟 就是 60,1小时是 3600(60秒×60分钟),1天是86400(24小时×60分钟×60秒),以此类推。
下面是一些使用时间单位的实用案例:
uint lastUpdated;
// 将‘上次更新时间’ 设置为 ‘现在’
function updateTimestamp() public {
lastUpdated = now;
}
// 如果到上次`updateTimestamp` 超过5分钟,返回 'true'
// 不到5分钟返回 'false'
function fiveMinutesHavePassed() public view returns (bool) {
return (now >= (lastUpdated + 5 minutes));
}
31. “view” 函数不花 “gas”
当玩家从外部调用一个view函数,是不需要支付一分 gas 的。
这是因为 view 函数不会真正改变区块链上的任何数据 - 它们只是读取。因此用 view 标记一个函数,意味着告诉 web3.js,运行这个函数只需要查询你的本地以太坊节点,而不需要在区块链上创建一个事务(事务需要运行在每个节点上,因此花费 gas)。
稍后我们将介绍如何在自己的节点上设置 web3.js。但现在,你关键是要记住,在所能只读的函数上标记上表示“只读”的“external view 声明,就能为你的玩家减少在 DApp 中 gas 用量。
注意:如果一个 view 函数在另一个函数的内部被调用,而调用函数与 view 函数的不属于同一个合约,也会产生调用成本。这是因为如果主调函数在以太坊创建了一个事务,它仍然需要逐个节点去验证。所以标记为 view 的函数只有在外部调用时才是免费的。
32. 异常处理
以下三个为错误处理的主要方法:
- revert ( )
- require ( )
- assert ( )
1.require(bool condition)
如果不满足条件,此方法调用将恢复到原始状态。此方法用于检查输入或外部组件的错误。
如:
function sayHiToPet(string _name) public returns (string) {
// 比较 _name 是否等于 "Pet". 如果不成立,抛出异常并终止程序
// (敲黑板: Solidity 并不支持原生的字符串比较, 我们只能通过比较
// 两字符串的 keccak256 哈希值来进行判断)
require(keccak256(_name) == keccak256("Pet"));
// 如果返回 true, 运行如下语句
return "Hi!";
}
如果你这样调用函数 sayHiToPet(“Pet”) ,它会返回“Hi!”。而如果调用的时候使用了其他参数,它则会抛出错误并停止执行。
以下场景使用 require() :
1.验证用户输入,即: require(input<20);
2.验证外部合约响应,即: require(external.send(amount));
3.执行合约前,验证状态条件,即: require(block.number > SOME_BLOCK_NUMBER) 或者 require(balance[msg.sender]>=amount)
4.一般地,尽量使用 require 函数
5.一般地,require 应该在函数最开始的地方使用
以下场景使用 assert():
1.检查 溢出overflow/下溢underflow,即:c = a+b; assert(c > b)
2.检查非变量(invariants),即:assert(this.balance >= totalSupply);
3.验证改变后的状态
4.预防不应该发生的条件
5.一般地,尽量少使用 assert 调用
// 可以对其他字段留空:
(,c) = multipleReturns();
}
### 27. 智能协议的永固性
在你把智能协议传上以太坊之后,它就变得不可更改, 这种永固性意味着你的代码永远不能被调整或更新。
你编译的程序会一直,永久的,不可更改的,存在以太坊上。这就是 Solidity 代码的安全性如此重要的一个原因。如果你的智能协议有任何漏洞,即使你发现了也无法补救。你只能让你的用户们放弃这个智能协议,然后转移到一个新的修复后的合约上。
所以我们在编写智能合约的时候尽量不要将一些地方写成硬代码(不可修改的代码),应该尽量写成函数(后续可修改)。
但这恰好也是智能合约的一大优势。代码说明一切。如果你去读智能合约的代码,并验证它,你会发现,一旦函数被定义下来,每一次的运行,程序都会严格遵照函数中原有的代码逻辑一丝不苟地执行,完全不用担心函数被人篡改而得到意外的结果。
\```
### 28. 省 gas 的招数:结构封装 (Struct packing)
通常情况下我们不会考虑使用 `uint` 变种,因为无论如何定义 `uint`的大小,Solidity 为它保留256位的存储空间。例如,使用 `uint8` 而不是`uint`(`uint256`)不会为你节省任何 gas。
除非,把 `uint` 绑定到 `struct` 里面。
如果一个 `struct` 中有多个 `uint`,则尽可能使用较小的 `uint`, Solidity 会将这些 `uint` 打包在一起,从而占用较少的存储空间。例如:
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);
所以,当 `uint` 定义在一个 `struct` 中的时候,尽量使用最小的整数子类型以节约空间。 并且把同样类型的变量放一起(即在 struct 中将把变量按照类型依次放置),这样 Solidity 可以将存储空间最小化。例如,有两个 `struct`:
uint c; uint32 a; uint32 b;` 和 `uint32 a; uint c; uint32 b;
前者比后者需要的gas更少,因为前者把`uint32`放一起了。
### 30. 时间单位
Solidity 使用自己的本地时间单位。
变量 now 将返回当前的unix时间戳(自1970年1月1日以来经过的秒数)。我写这句话时 unix 时间是 1515527488。
注意:Unix时间传统用一个32位的整数进行存储。这会导致“2038年”问题,当这个32位的unix时间戳不够用,产生溢出,使用这个时间的遗留系统就麻烦了。所以,如果我们想让我们的 DApp 跑够20年,我们可以使用64位整数表示时间,但为此我们的用户又得支付更多的 gas。真是个两难的设计啊!
Solidity 还包含秒(seconds),分钟(minutes),小时(hours),天(days),周(weeks) 和 年(years) 等时间单位。它们都会转换成对应的秒数放入 uint 中。所以 1分钟 就是 60,1小时是 3600(60秒×60分钟),1天是86400(24小时×60分钟×60秒),以此类推。
下面是一些使用时间单位的实用案例:
uint lastUpdated;
// 将‘上次更新时间’ 设置为 ‘现在’
function updateTimestamp() public {
lastUpdated = now;
}
// 如果到上次updateTimestamp
超过5分钟,返回 ‘true’
// 不到5分钟返回 ‘false’
function fiveMinutesHavePassed() public view returns (bool) {
return (now >= (lastUpdated + 5 minutes));
}
### 31. “view” 函数不花 “gas”
当玩家从外部调用一个view函数,是不需要支付一分 gas 的。
这是因为 view 函数不会真正改变区块链上的任何数据 - 它们只是读取。因此用 view 标记一个函数,意味着告诉 web3.js,运行这个函数只需要查询你的本地以太坊节点,而不需要在区块链上创建一个事务(事务需要运行在每个节点上,因此花费 gas)。
稍后我们将介绍如何在自己的节点上设置 web3.js。但现在,你关键是要记住,在所能只读的函数上标记上表示“只读”的“external view 声明,就能为你的玩家减少在 DApp 中 gas 用量。
注意:如果一个 view 函数在另一个函数的内部被调用,而调用函数与 view 函数的不属于同一个合约,也会产生调用成本。这是因为如果主调函数在以太坊创建了一个事务,它仍然需要逐个节点去验证。所以标记为 view 的函数只有在外部调用时才是免费的。
### 32. 异常处理
以下三个为错误处理的主要方法:
- revert ( )
- require ( )
- assert ( )
1.require(bool condition)
如果不满足条件,此方法调用将恢复到原始状态。此方法用于检查输入或外部组件的错误。
如:
function sayHiToPet(string _name) public returns (string) {
// 比较 _name 是否等于 “Pet”. 如果不成立,抛出异常并终止程序
// (敲黑板: Solidity 并不支持原生的字符串比较, 我们只能通过比较
// 两字符串的 keccak256 哈希值来进行判断)
require(keccak256(_name) == keccak256(“Pet”));
// 如果返回 true, 运行如下语句
return “Hi!”;
}
如果你这样调用函数 sayHiToPet(“Pet”) ,它会返回“Hi!”。而如果调用的时候使用了其他参数,它则会抛出错误并停止执行。
以下场景使用 require() :
1.验证用户输入,即: require(input<20);
2.验证外部合约响应,即: require(external.send(amount));
3.执行合约前,验证状态条件,即: require(block.number > SOME_BLOCK_NUMBER) 或者 require(balance[msg.sender]>=amount)
4.一般地,尽量使用 require 函数
5.一般地,require 应该在函数最开始的地方使用
以下场景使用 assert():
1.检查 溢出overflow/下溢underflow,即:c = a+b; assert(c > b)
2.检查非变量(invariants),即:assert(this.balance >= totalSupply);
3.验证改变后的状态
4.预防不应该发生的条件
5.一般地,尽量少使用 assert 调用
6.一般地,assert 应该在函数结尾处使用