首页 最新 热门 推荐

  • 首页
  • 最新
  • 热门
  • 推荐

【区块链】Solidity学习

  • 25-04-24 16:22
  • 2674
  • 8998
blog.csdn.net

1 Hello Web3

开发工具:Remix

我们将使用 Remix 运行 Solidity 合约。Remix 是以太坊官方推荐的智能合约集成开发环境(IDE),适合新手,可以在浏览器中快速开发和部署合约,无需在本地安装任何程序。

网址:https://remix.ethereum.org

在 Remix 中,左侧菜单有按钮,对应文件(编写代码)、编译(运行代码)和部署(将合约部署到链上)。

点击“创建新文件”(Create New File)按钮,即可创建一个空白的 Solidity 合约。

将文件命名为HelloWeb3.sol

第一个 Solidity 程序

  1. // SPDX-License-Identifier: MIT
  2. pragma solidity ^0.8.21;
  3. contract HelloWeb3{
  4. string public _string = "Hello Web3!";
  5. }

第 1 行是注释,说明代码所使用的软件许可(license),这里使用的是 MIT 许可。如果不写许可,编译时会出现警告(warning),但程序仍可运行。Solidity 注释以“//”开头,后面跟注释内容,注释不会被程序执行。

第 2 行声明源文件所使用的 Solidity 版本,因为不同版本的语法有差异。这行代码表示源文件将不允许小于 0.8.21 版本或大于等于 0.9.0 的编译器编译(第二个条件由 ^ 提供)。Solidity 语句以分号(;)结尾。

第 3-4 行是合约部分。第 3 行创建合约(contract),并声明合约名为 HelloWeb3。第 4 行是合约内容,声明了一个 string(字符串)变量 _string,并赋值为 "Hello Web3!"。

编译并部署代码

在 Remix 编辑代码的页面,按 Ctrl + S 即可编译代码,非常方便。

编译完成后,点击左侧菜单的“部署”按钮,进入部署(Deploy)页面。

默认情况下,Remix 会使用 Remix 虚拟机(以前称为 JavaScript 虚拟机)来模拟以太坊链,运行智能合约,类似在浏览器里运行一条测试链。Remix 还会为你分配一些测试账户,每个账户里有 100 ETH(测试代币),随意使用。点击 Deploy(黄色按钮),即可部署我们编写的合约。

部署成功后,在下方会看到名为 HelloWeb3 的合约。点击 _string,即可看到 "Hello Web3!"。

2 值类型

  1. 值类型(Value Type) :包括布尔型,整数型等等,这类变量赋值时候直接传递数值。

  2. 引用类型(Reference Type) :包括数组和结构体,这类变量占空间大,赋值时候直接传递地址(类似指针)。

  3. 映射类型(Mapping Type) : Solidity中存储键值对的数据结构,可以理解为哈希表

1. 布尔型

布尔型是二值变量,取值为 true 或 false。

  1. // 布尔值
  2. bool public _bool = true;

布尔值的运算符包括:

  • ! (逻辑非)
  • && (逻辑与,"and")
  • || (逻辑或,"or")
  • == (等于)
  • != (不等于)

值得注意的是: && 和 || 运算符遵循短路规则,这意味着,假如存在 f(x) || g(y) 的表达式,如果 f(x) 是 true,g(y) 不会被计算,即使它和 f(x) 的结果是相反的。假如存在f(x) && g(y) 的表达式,如果 f(x) 是 false,g(y) 不会被计算。 所谓“短路规则”,一般出现在逻辑与(&&)和逻辑或(||)中。 当逻辑与(&&)的第一个条件为false时,就不会再去判断第二个条件; 当逻辑或(||)的第一个条件为true时,就不会再去判断第二个条件,这就是短路规则。

2. 整型

整型是 Solidity 中的整数,最常用的包括:

  1. // 整型
  2. int public _int = -1; // 整数,包括负数
  3. uint public _uint = 1; // 无符号整数
  4. uint256 public _number = 20220330; // 256位无符号整数

常用的整型运算符包括:

  • 比较运算符(返回布尔值): <=, <,==, !=, >=, >
  • 算术运算符: +, -, *, /, %(取余),**(幂)

3. 地址类型

地址类型(address)有两类:

  • 普通地址(address): 存储一个 20 字节的值(以太坊地址的大小)。
  • payable address: 比普通地址多了 transfer 和 send 两个成员方法,用于接收转账。
  1. // 地址
  2. address public _address = 0x7A58c0Be72BE218B41C608b7Fe7C5bB630736C71;
  3. address payable public _address1 = payable(_address); // payable address,可以转账、查余额

4. 定长字节数组

字节数组分为定长和不定长两种:

  • 定长字节数组: 属于值类型,数组长度在声明之后不能改变。根据字节数组的长度分为 bytes1, bytes8, bytes32 等类型。定长字节数组最多存储 32 bytes 数据,即bytes32。
  • 不定长字节数组: 属于引用类型,数组长度在声明之后可以改变,包括 bytes 等。
  1. // 固定长度的字节数组
  2. bytes32 public _byte32 = "MiniSolidity";
  3. bytes1 public _byte = _byte32[0];

在上述代码中,字符串 MiniSolidity 以字节的方式存储进变量 _byte32。如果把它转换成 16 进制,就是:0x4d696e69536f6c69646974790000000000000000000000000000000000000000

_byte 变量的值为 _byte32 的第一个字节,即 0x4d。

5. 枚举 enum

枚举(enum)是 Solidity 中用户定义的数据类型。它主要用于为 uint 分配名称,使程序易于阅读和维护。它与 C 语言 中的 enum 类似,使用名称来代替从 0 开始的 uint:

  1. // 用enum将uint 0, 1, 2表示为Buy, Hold, Sell
  2. enum ActionSet { Buy, Hold, Sell }
  3. // 创建enum变量 action
  4. ActionSet action = ActionSet.Buy;

枚举可以显式地和 uint 相互转换,并会检查转换的无符号整数是否在枚举的长度内,否则会报错:

  1. // enum可以和uint显式的转换
  2. function enumToUint() external view returns(uint){
  3. return uint(action);
  4. }

在 Remix 上运行

  • 部署合约后可以查看每个类型的变量的数值:

enum 和 uint 转换的示例:

3 函数

我们先看一下 Solidity 中函数的形式(方括号中的是可写可不写的关键字):

  1. function <function name>([parameter types[, ...]]) {internal|external|public|private} [pure|view|payable] [virtual|override] [<modifiers>]
  2. [returns (<return types>)]{ <function body> }
  1. ([parameter types[, ...]]):圆括号内写入函数的参数,即输入到函数的变量类型和名称。
  2. {internal|external|public|private}:函数可见性说明符,共有4种。

    • public:内部和外部均可见。
    • private:只能从本合约内部访问,继承的合约也不能使用。
    • external:只能从合约外部访问(但内部可以通过 this.f() 来调用,f是函数名)。
    • internal: 只能从合约内部访问,继承的合约可以用。

    注意 1:合约中定义的函数需要明确指定可见性,它们没有默认值。

    注意 2:public|private|internal 也可用于修饰状态变量。public变量会自动生成同名的getter函数,用于查询数值。未标明可见性类型的状态变量,默认为internal。

  3. [pure|view|payable]:决定函数权限/功能的关键字。payable(可支付的)很好理解,带着它的函数,运行的时候可以给合约转入 ETH。

  4. [virtual|override]: 方法是否可以被重写,或者是否是重写方法。virtual用在父合约上,标识的方法可以被子合约重写。override用在自合约上,表名方法重写了父合约的方法。

  5. : 自定义的修饰器,可以有0个或多个修饰器。

  6. [returns ()]:函数返回的变量类型和名称。

到底什么是 Pure 和View?

刚开始学习 solidity 时,pure 和 view 关键字可能令人费解,因为其他编程语言中没有类似的关键字。solidity 引入这两个关键字主要是因为 以太坊交易需要支付气费(gas fee)。合约的状态变量存储在链上,gas fee 很贵,如果计算不改变链上状态,就可以不用付 gas。包含 pure 和 view 关键字的函数是不改写链上状态的,因此用户直接调用它们是不需要付 gas 的(注意,合约中非 pure/view 函数调用 pure/view 函数时需要付gas)。

在以太坊中,以下语句被视为修改链上状态:

  1. 写入状态变量。
  2. 释放事件。
  3. 创建其他合约。
  4. 使用 selfdestruct.
  5. 通过调用发送以太币。
  6. 调用任何未标记 view 或 pure 的函数。
  7. 使用低级调用(low-level calls)。
  8. 使用包含某些操作码的内联汇编。

将合约中的状态变量(存储在链上)比作碧琪公主,三种不同的角色代表不同的关键字。

代码

1. pure 和 view

我们在合约里定义一个状态变量 number,初始化为 5。

定义一个 add() 函数,每次调用会让 number 增加 1。

  1. // SPDX-License-Identifier: MIT
  2. pragma solidity ^0.8.21;
  3. contract FunctionTypes{
  4. uint256 public number = 5;
  5. // 默认function
  6. function add() external{
  7. number = number + 1;
  8. }
  9. }

如果 add() 函数被标记为 pure,比如 function add() external pure,就会报错。因为 pure 是不配读取合约里的状态变量的,更不配改写。那 pure 函数能做些什么?举个例子,你可以给函数传递一个参数 _number,然后让他返回 _number + 1,这个操作不会读取或写入状态变量。

  1. // SPDX-License-Identifier: MIT
  2. pragma solidity ^0.8.21;
  3. contract FunctionTypes{
  4. uint256 public number = 5;
  5. // 默认function
  6. function add() external{
  7. number = number + 1;
  8. }
  9. // pure: 纯纯牛马
  10. function addPure(uint256 _number) external pure returns(uint256 new_number){
  11. new_number = _number + 1;
  12. }
  13. }

如果 add() 函数被标记为 view,比如 function add() external view,也会报错。因为 view 能读取,但不能够改写状态变量。我们可以稍微改写下函数,读取但是不改写 number,返回一个新的变量。

  1. // view: 看客
  2. function addView() external view returns(uint256 new_number) {
  3. new_number = number + 1;
  4. }

2. internal v.s. external

  1. // internal: 内部函数
  2. function minus() internal {
  3. number = number - 1;
  4. }
  5. // 合约内的函数可以调用内部函数
  6. function minusCall() external {
  7. minus();
  8. }

我们定义一个 internal 的 minus() 函数,每次调用使得 number 变量减少 1。由于 internal 函数只能由合约内部调用,我们必须再定义一个 external 的 minusCall() 函数,通过它间接调用内部的 minus() 函数。

4 函数输出

返回值:return 和 returns

Solidity 中与函数输出相关的有两个关键字:return和returns。它们的区别在于:

  • returns:跟在函数名后面,用于声明返回的变量类型及变量名。
  • return:用于函数主体中,返回指定的变量。
  1. // 返回多个变量
  2. function returnMultiple() public pure returns(uint256, bool, uint256[3] memory){
  3. return(1, true, [uint256(1),2,5]);
  4. }

在上述代码中,我们利用 returns 关键字声明了有多个返回值的 returnMultiple() 函数,然后我们在函数主体中使用 return(1, true, [uint256(1),2,5]) 确定了返回值。

这里uint256[3]声明了一个长度为3且类型为uint256的数组作为返回值。因为[1,2,3]会默认为uint8(3),因此[uint256(1),2,5]中首个元素必须强转uint256来声明该数组内的元素皆为此类型。数组类型返回值默认必须用memory修饰.

命名式返回

我们可以在 returns 中标明返回变量的名称。Solidity 会初始化这些变量,并且自动返回这些变量的值,无需使用 return。

  1. // 命名式返回
  2. function returnNamed() public pure returns(uint256 _number, bool _bool, uint256[3] memory _array){
  3. _number = 2;
  4. _bool = false;
  5. _array = [uint256(3),2,1];
  6. }

在上述代码中,我们用 returns(uint256 _number, bool _bool, uint256[3] memory _array) 声明了返回变量类型以及变量名。这样,在主体中只需为变量 _number、_bool和_array 赋值,即可自动返回。

当然,你也可以在命名式返回中用 return 来返回变量:

  1. // 命名式返回,依然支持return
  2. function returnNamed2() public pure returns(uint256 _number, bool _bool, uint256[3] memory _array){
  3. return(1, true, [uint256(1),2,5]);
  4. }

解构式赋值

Solidity 支持使用解构式赋值规则来读取函数的全部或部分返回值。

  • 读取所有返回值:声明变量,然后将要赋值的变量用,隔开,按顺序排列。

    1. uint256 _number;
    2. bool _bool;
    3. uint256[3] memory _array;
    4. (_number, _bool, _array) = returnNamed();

    读取部分返回值:声明要读取的返回值对应的变量,不读取的留空。在下面的代码中,我们只读取_bool,而不读取返回的_number和_array:

    1. bool _bool2;
    2. (, _bool2, ) = returnNamed();
  1. // 读取返回值,结构式赋值
  2. function readReturn() public pure{
  3. uint256 _number;
  4. bool _bool;
  5. bool _bool2;
  6. uint256[3] memory _array;
  7. (_number, _bool, _array) = returnNamed();
  8. // 读取部分返回值,解构式赋值
  9. (, _bool2, ) = returnNamed();
  10. }

在 Remix 上运行 

部署合约后查看三种返回方式的结果

5 变量数据存储和作用域

引用类型(Reference Type) :包括数组(array)和结构体(struct),由于这类变量比较复杂,占用存储空间大,我们在使用时必须要声明数据存储的位置。

数据位置

Solidity数据存储位置有三类:storage,memory和calldata。不同存储位置的gas成本不同。storage类型的数据存在链上,类似计算机的硬盘,消耗gas多;memory和calldata类型的临时存在内存里,消耗gas少。整体消耗gas从多到少依次为:storage > memory > calldata。大致用法:

  1. storage:合约里的状态变量默认都是storage,存储在链上。

  2. memory:函数里的参数和临时变量一般用memory,存储在内存中,不上链。尤其是如果返回数据类型是变长的情况下,必须加memory修饰,例如:string, bytes, array和自定义结构。

  3. calldata:和memory类似,存储在内存中,不上链。与memory的不同点在于calldata变量不能修改(immutable),一般用于函数的参数。例子:

  1. function fCalldata(uint[] calldata _x) public pure returns(uint[] calldata){
  2. //参数为calldata数组,不能被修改
  3. // _x[0] = 0 //这样修改会报错
  4. return(_x);
  5. }

注:本文转载自blog.csdn.net的轻闲一号机的文章"https://blog.csdn.net/weixin_52553215/article/details/146726432"。版权归原作者所有,此博客不拥有其著作权,亦不承担相应法律责任。如有侵权,请联系我们删除。
复制链接
复制链接
相关推荐
发表评论
登录后才能发表评论和回复 注册

/ 登录

评论记录:

未查询到任何数据!
回复评论:

分类栏目

后端 (14832) 前端 (14280) 移动开发 (3760) 编程语言 (3851) Java (3904) Python (3298) 人工智能 (10119) AIGC (2810) 大数据 (3499) 数据库 (3945) 数据结构与算法 (3757) 音视频 (2669) 云原生 (3145) 云平台 (2965) 前沿技术 (2993) 开源 (2160) 小程序 (2860) 运维 (2533) 服务器 (2698) 操作系统 (2325) 硬件开发 (2492) 嵌入式 (2955) 微软技术 (2769) 软件工程 (2056) 测试 (2865) 网络空间安全 (2948) 网络与通信 (2797) 用户体验设计 (2592) 学习和成长 (2593) 搜索 (2744) 开发工具 (7108) 游戏 (2829) HarmonyOS (2935) 区块链 (2782) 数学 (3112) 3C硬件 (2759) 资讯 (2909) Android (4709) iOS (1850) 代码人生 (3043) 阅读 (2841)

热门文章

136
区块链
关于我们 隐私政策 免责声明 联系我们
Copyright © 2020-2024 蚁人论坛 (iYenn.com) All Rights Reserved.
Scroll to Top