我一度觉得自己不知何时变成了一个浮躁的人,一度不想受外界干扰的我被干扰了,再无法平静的去看一本书,但我仍旧希望我能够克服这些,压抑着自己直到所有的冲动和奇怪的思想都无法再左右我行动。
自律会让你更加自律,放纵会让你更加放纵。
做人做事,都是如此。
简单的道理却要克服很多东西。做正确的事情而不是我想做的事情。发乎情,止乎理。
速成班
1全世界人们入门的统一标准hello world
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.21;
contract HelloWeb3{string public _string = "Hello Web3!";
}
前两行类似于说明文件,contract说明你开始编写一个合约,这个合约的名字叫做helloweb3,合约内声明了一个字符串public _string叫做 “Hello Web3!”
这个hello world我编译失败了
莫名其妙又成功了,把编译的篮子放在了右边就成功了?
2 值类型
bool类型
// 声明一个对外可访问的bool
bool public _bool = true;// 布尔运算
bool public _bool1 = !_bool; // 取非
bool public _bool2 = _bool && _bool1; // 与
bool public _bool3 = _bool || _bool1; // 或
bool public _bool4 = _bool == _bool1; // 相等
bool public _bool5 = _bool != _bool1; // 不相等
整型
// 整型
int public _int = -1; // 整数,包括负数
uint public _uint = 1; // 正整数
uint256 public _number = 20220330; // 256位正整数// 整数运算
uint256 public _number1 = _number + 1; // +,-,*,/
uint256 public _number2 = 2**2; // 指数
uint256 public _number3 = 7 % 2; // 取余数
bool public _numberbool = _number2 > _number3; // 比大小
地址类型
地址类型(address)有两类:
普通地址(address): 存储一个 20 字节的值(以太坊地址的大小)。
payable address: 比普通地址多了 transfer 和 send 两个成员方法,用于接收转账。
// 地址
address public _address = 0x7A58c0Be72BE218B41C608b7Fe7C5bB630736C71;
address payable public _address1 = payable(_address); // payable address,可以转账、查余额
// 地址类型的成员
uint256 public balance = _address1.balance; // balance of address
我有点不是太理解这个payable address
定长字节数组
// 固定长度的字节数组
bytes32 public _byte32 = "MiniSolidity";
bytes1 public _byte = _byte32[0];
32个字节组成的数组,也就是可以容纳32个元素,很明显后面的并没有使用,u8类型就是32个数字,hex
64类型就是32的元素组成的数组,0-f
枚举enum
// 用enum将uint 0, 1, 2表示为Buy, Hold, Sell
enum ActionSet { Buy, Hold, Sell }
// 创建enum变量 action
ActionSet action = ActionSet.Buy;
枚举的使用方法和各种语言的枚举方式几乎是相同的。
但是这段代码我有点没看明白。
// enum可以和uint显式的转换
function enumToUint() external view returns(uint){return uint(action);
}
在 Solidity 中,external 是一个函数修饰符,用于定义函数的可见性。函数的可见性决定了函数可以从哪里被调用。也就是external和public其实是同级的。
3函数function
function <function name>(<parameter types>) {internal|external|public|private} [pure|view|payable] [returns (<return types>)]
看着有一些复杂,让我们从前往后逐个解释(方括号中的是可写可不 写的关键字):
function:声明函数时的固定用法。要编写函数,就需要以 function 关键字开头。
:函数名。
():圆括号内写入函数的参数,即输入到函数的变量类型和名称。
{internal|external|public|private}:函数可见性说明符,共有4种。
public:内部和外部均可见。
private:只能从本合约内部访问,继承的合约也不能使用。
external:只能从合约外部访问(但内部可以通过 this.f() 来调用,f是函数名)。
internal: 只能从合约内部访问,继承的合约可以用。
注意 1:合约中定义的函数需要明确指定可见性,它们没有默认值。
注意 2:public|private|internal 也可用于修饰状态变量。public变量会自动生成同名的getter函数,用于查询数值。未标明可见性类型的状态变量,默认为internal。
[pure|view|payable]:决定函数权限/功能的关键字。payable(可支付的)很好理解,带着它的函数,运行的时候可以给合约转入 ETH。pure 和 view 的介绍见下一节。
[returns ()]:函数返回的变量类型和名称。
有关于pure 和view
刚开始学习 solidity 时,pure 和 view 关键字可能令人费解,因为其他编程语言中没有类似的关键字。solidity 引入这两个关键字主要是因为 以太坊交易需要支付气费(gas fee)。合约的状态变量存储在链上,gas fee 很贵,如果计算不改变链上状态,就可以不用付 gas。包含 pure 和 view 关键字的函数是不改写链上状态的,因此用户直接调用它们是不需要付 gas 的(注意,合约中非 pure/view 函数调用 pure/view 函数时需要付gas)。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.21;
contract FunctionTypes{uint256 public number = 5;constructor() payable {}// 函数类型// function (<parameter types>) {internal|external} [pure|view|payable] [returns (<return types>)]// 默认functionfunction add() external{number = number + 1;}// pure: 纯纯牛马// 没有读取权,因此什么东西都是别人传给他的// 瞎子function addPure(uint256 _number) external pure returns(uint256 new_number){new_number = _number+1;}// view: 看客function addView() external view returns(uint256 new_number) {new_number = number + 1;}// internal: 内部函数function minus() internal {number = number - 1;}// 合约内的函数可以调用内部函数function minusCall() external {minus();}// payable: 递钱,能给合约支付eth的函数function minusPayable() external payable returns(uint256 balance) {minus(); balance = address(this).balance;}
}
internal和external
internal是只可以被内部调用的函数方法,external是可以被外部调用的。
4函数return
返回值:return 和 returns
Solidity 中与函数输出相关的有两个关键字:return和returns。它们的区别在于:
returns:跟在函数名后面,用于声明返回的变量类型及变量名。
return:用于函数主体中,返回指定的变量。
// 返回多个变量
function returnMultiple() public pure returns(uint256, bool, uint256[3] memory){return(1, true, [uint256(1),2,5]);
}
这里uint256[3]声明了一个长度为3且类型为uint256的数组作为返回值。因为[1,2,3]会默认为uint8(3),因此[uint256(1),2,5]中首个元素必须强转uint256来声明该数组内的元素皆为此类型。数组类型返回值默认必须用memory修饰。
我有点没看明白这个memory
和rust有一个类似的地方,可以不接受不需要的参数
(, _bool2, ) = returnNamed();
5函数存储和作用域
引用类型:包括数组(array)和结构体(struct),由于这类变量比较复杂,占用存储空间大,我们在使用时必须要声明数据存储的位置。
Solidity数据存储位置有三类:storage,memory和calldata。不同存储位置的gas成本不同。storage类型的数据存在链上,类似计算机的硬盘,消耗gas多;memory和calldata类型的临时存在内存里,消耗gas少。大致用法:
storage:合约里的状态变量默认都是storage,存储在链上。
memory:函数里的参数和临时变量一般用memory,存储在内存中,不上链。尤其是如果返回数据类型是变长的情况下,必须加memory修饰,例如:string, bytes, array和自定义结构。
calldata:和memory类似,存储在内存中,不上链。与memory的不同点在于calldata变量不能修改(immutable),一般用于函数的参数。
我的感觉就是calldata是栈数据,虽然不知道为什么不可变,然后memory就类似于堆数据的可变数据
function fCalldata(uint[] calldata _x) public pure returns(uint[] calldata){//参数为calldata数组,不能被修改// _x[0] = 0 //这样修改会报错return(_x);
}
变量的作用域
合约里定义的变量就是所谓的状态变量,修改这些状态变量是昂贵的
contract Variables {uint public x = 1;uint public y;string public z;function bar() external pure returns(uint){uint xx = 1;uint yy = 3;uint zz = xx + yy;return(zz);
}
}
全局变量是全局范围工作的变量,都是solidity预留关键字。他们可以在函数内不声明直接使用:
function global() external view returns(address, uint, bytes memory){address sender = msg.sender;uint blockNum = block.number;bytes memory data = msg.data;return(sender, blockNum, data);
}
例子里,我们使用了3个常用的全局变量:msg.sender,block.number和msg.data,他们分别代表请求发起地址,当前区块高度,和请求数据。
以太单位
Solidity中不存在小数点,以0代替为小数点,来确保交易的精确度,并且防止精度的损失,利用以太单位可以避免误算的问题,方便程序员在合约中处理货币交易。
wei: 1
gwei: 1e9 = 1000000000
ether: 1e18 = 1000000000000000000
function weiUnit() external pure returns(uint) {assert(1 wei == 1e0);assert(1 wei == 1);return 1 wei;
}function gweiUnit() external pure returns(uint) {assert(1 gwei == 1e9);assert(1 gwei == 1000000000);return 1 gwei;
}function etherUnit() external pure returns(uint) {assert(1 ether == 1e18);assert(1 ether == 1000000000000000000);return 1 ether;
}
6引用类型 数组和结构体
这里不是很难,但是我不是很理解这个memory的array和正常的到底有啥子区别?
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.21;
contract ArrayTypes {// 固定长度 Arrayuint[8] array1;bytes1[5] array2;address[100] array3;// 可变长度 Arrayuint[] array4;bytes1[] array5;address[] array6;bytes array7;// 初始化可变长度 Arrayuint[] array8 = new uint[](5);bytes array9 = new bytes(9);// 给可变长度数组赋值function initArray() external pure returns(uint[] memory){uint[] memory x = new uint[](3);x[0] = 1;x[1] = 3;x[2] = 4;return(x);} function arrayPush() public returns(uint[] memory){uint[2] memory a = [uint(1),2];array4 = a;array4.push(3);return array4;}
}pragma solidity ^0.8.21;
contract StructTypes {// 结构体 Structstruct Student{uint256 id;uint256 score; }Student student; // 初始一个student结构体// 给结构体赋值// 方法1:在函数中创建一个storage的struct引用function initStudent1() external{Student storage _student = student; // assign a copy of student_student.id = 11;_student.score = 100;}// 方法2:直接引用状态变量的structfunction initStudent2() external{student.id = 1;student.score = 80;}// 方法3:构造函数式function initStudent3() external {student = Student(3, 90);}// 方法4:key valuefunction initStudent4() external {student = Student({id: 4, score: 60});}
}pragma solidity ^0.8.21;
contract EnumTypes {// 将uint 0, 1, 2表示为Buy, Hold, Sellenum ActionSet { Buy, Hold, Sell }// 创建enum变量 actionActionSet action = ActionSet.Buy;// enum可以和uint显式的转换function enumToUint() external view returns(uint){return uint(action);}
}
7映射 hash
// 这样可以声明一个hashmap
mapping(uint => address) public idToAddress; // id映射到地址
mapping(address => address) public swapPair; // 币对的映射,地址到地址
// 如果希望使用这个map
value = idToAddress[key]
// 如果希望添加一个数据
idToAddress[key1] = value
8初始化变量值
bool public _bool; // false
string public _string; // ""
int public _int; // 0
uint public _uint; // 0
address public _address; // 0x0000000000000000000000000000000000000000enum ActionSet { Buy, Hold, Sell}
ActionSet public _enum; // 第1个内容Buy的索引0function fi() internal{} // internal空白函数
function fe() external{} // external空白函数 // delete操作符
bool public _bool2 = true;
function d() external {delete _bool2; // delete 会让_bool2变为默认值,false
}
9常数constant和immutable
// constant变量必须在声明的时候初始化,之后不能改变
uint256 constant CONSTANT_NUM = 10;
string constant CONSTANT_STRING = "0xAA";
bytes constant CONSTANT_BYTES = "WTF";
address constant CONSTANT_ADDRESS = 0x0000000000000000000000000000000000000000;// immutable变量可以在constructor里初始化,之后不能改变
uint256 public immutable IMMUTABLE_NUM = 9999999999;
// 在`Solidity v8.0.21`以后,下列变量数值暂为初始值
address public immutable IMMUTABLE_ADDRESS;
uint256 public immutable IMMUTABLE_BLOCK;
uint256 public immutable IMMUTABLE_TEST;
构造函数和修饰器
构造函数每个合约只能定义一个,我认为这个就类似rust里的懒加载lazy_Static,用来初始化一些合约的参数。
address owner; // 定义owner变量// 构造函数
constructor(address initialOwner) {owner = initialOwner; // 在部署合约的时候,将owner设置为传入的initialOwner地址
}
看看完全的代码
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.21;contract Owner {address public owner; // 定义owner变量// 构造函数constructor(address initialOwner) {owner = initialOwner; // 在部署合约的时候,将owner设置为传入的initialOwner地址}// 定义modifiermodifier onlyOwner {require(msg.sender == owner); // 检查调用者是否为owner地址_; // 如果是的话,继续运行函数主体;否则报错并revert交易}// 定义一个带onlyOwner修饰符的函数function changeOwner(address _newOwner) external onlyOwner{owner = _newOwner; // 只有owner地址运行这个函数,并改变owner}
}
筑基期
首先看一个存储合约示例
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.16 <0.9.0;contract SimpleStorage {uint storedData;function set(uint x) public {storedData = x;}function get() public view returns (uint) {return storedData;}
}
第一行告诉您,源代码是根据GPL3.0版本授权的。 在发布源代码是默认的情况下,机器可读的许可证说明是很重要的。
下一行指定源代码是为Solidity 0.4.16版本编写的,或该语言的较新版本,直到但不包括0.9.0版本。 这是为了确保合约不能被新的(有重大改变的)编译器版本编译,在那里它可能会有不同的表现。 Pragmas 是编译器关于如何处理源代码的常用指令 (例如, pragma once )。
这两行类似于配置,contract就类似于结构体或者类了。
我们在类中定义了一个unit数据,并设置了set和get函数来获取这个值。我一度很好奇这个view,同时这个public 放置的位置也很让我疑惑。搜索后发现
public 关键字用于声明智能合约的状态变量。当一个状态变量被声明为 public 时,
它会自动生成一个同名的getter函数,允许外部访问这个状态变量的值。
此外,public 状态变量的值会被存储在区块链上,并且可以通过合约的接口进行访问。
view 关键字用于修饰智能合约的函数。一个被标记为 view 的函数表示这个函数不会修改区块链上的任何状态,
它只用于读取数据。调用这样的函数不会产生交易,也就是说,
它不会消耗gas(以太坊网络的费用),因为它不会引起状态变化。这使得 view 函数非常适合用于查询操作。
我目前把这个public理解为普遍的public,因为我不知道他在智能合约里起到一个什么样的作用。
该合约能完成的事情并不多(由于以太坊构建的基础架构的原因), 它能允许任何人在合约中存储一个单独的数字,并且这个数字可以被世界上任何人访问, 且没有可行的办法阻止您发布这个数字。 当然,任何人都可以再次调用 set ,传入不同的值,覆盖您的数字, 但是这个数字仍会被存储在区块链的历史记录中。
神奇之处来了,修改的历史居然会被保存。
接下来我们继续看下一个例子:
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.4;contract Coin {// 关键字 "public" 使变量可以从其他合约中访问。address public minter;mapping(address => uint) public balances;// 事件允许客户端对您声明的特定合约变化做出反应event Sent(address from, address to, uint amount);// 构造函数代码只有在合约创建时运行constructor() {minter = msg.sender;}// 向一个地址发送一定数量的新创建的代币// 但只能由合约创建者调用function mint(address receiver, uint amount) public {require(msg.sender == minter);balances[receiver] += amount;}// 错误类型变量允许您提供关于操作失败原因的信息。// 它们会返回给函数的调用者。error InsufficientBalance(uint requested, uint available);// 从任何调用者那里发送一定数量的代币到一个地址function send(address receiver, uint amount) public {if (amount > balances[msg.sender])revert InsufficientBalance({requested: amount,available: balances[msg.sender]});balances[msg.sender] -= amount;balances[receiver] += amount;emit Sent(msg.sender, receiver, amount);}
}
address public minter; 这一行声明了一个可以被公开访问的 address 类型的状态变量。 address 类型是一个160位的值,且不允许任何算数操作。 这种类型适合存储合约地址或 外部账户 的密钥对。