Solidity学习笔记

Solidity Learning Road

  • 简单代币合约
    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];
    }

关键字

  • contract: 合约相当于面向对象的类
  • library/using: 库。他和合约很像,但又有些不同,就我们的目的而言,库允许我们使用 using 关键字,它可以自动把库的所有方法添加给一个数据类型。具体的使用方法后面会有介绍
  • function: 声明一个函数
  • event: 声明一个事件(当完成一个操作后调用event,用于生成日志)
  • require: 用于验证 使用方式: require( a == 1); 不满足则抛出错误停止执行。经常与msg.sender一起使用对函数调用者进行验证
  • modifier: 用于声明定义一个自定义修饰符。用法后面会进行详细介绍

常用的一些内置的变量

  • msg.sender 调用者.
  • now 获取当前时间戳。将返回当前的unix时间戳(自1970年1月1日以来经过的秒数).

下面是一些使用时间单位的实用案例:

uint lastUpdated;

// 将‘上次更新时间’ 设置为 ‘现在’
function updateTimestamp() public {
  lastUpdated = now;
}

// 如果到上次`updateTimestamp` 超过5分钟,返回 'true'
// 不到5分钟返回 'false'
function fiveMinutesHavePassed() public view returns (bool) {
  return (now >= (lastUpdated + 5 minutes));
}
  • msg.sender 调用者。经常用于验证来确定调用者是否有足够的权限(获取变量或者调用函数)

数据类型

  • address:地址类型变量
  • bool/整形:int,unit/
  • string: 字符串类型
  • 数据类型转换方法
    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;
  • Mapping

映射 如下的定义方式我们可以定义一个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。

  • pure/view
    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;
    }

还有一种就是函数既不读取也不修改合约里的数据,就如上边这个函数就是求两个参数的乘积。不需要读取更不需要修改任何数据。

  • 自定义函数修饰符
    定义一个onlyOwner的修饰符,用关键字modifier进行声明,除此之外我们可以看到“_;”的形式表示如果验证通过就继续回来修饰符调用之后继续执行。
/**
 * @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();
}

节省gas的一些技巧

  • 使用结构体封装

通常我们只声明一个变量时用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’ 函数

当用户从外部调用一个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/

你可能感兴趣的:(区块链)