注意:本篇因为主要讲理论知识,所以会有点枯燥,你们要静下心来看,并且为了让你们方便理解,我已经尽量浓缩了精华,还加了例子。
1、结构体
结构体可以在合约中作为变量、函数参数或返回值使用。可以使用点(.)操作符来访问结构体中的字段,类似于其他编程语言中的对象属性访问。通过使用结构体,可以更好地组织和管理复杂的数据,提高代码的可读性和可维护性。
例子:
在Solidity中,可以使用点(.)操作符来访问结构体中的字段。假设我们有一个名为Person的结构体,其中包含姓名(name)和年龄(age)两个字段,可以按照以下方式访问结构体中的字段:
声明结构体变量并赋值:
Person person;
person.name = "Alice";
person.age = 25;
在函数参数中使用结构体:
function updatePerson(Person memory _person) public {
_person.name = "Bob";
_person.age = 30;
}
在函数返回值中使用结构体:
function getPerson() public view returns (Person memory) {
Person memory person;
person.name = "Charlie";
person.age = 35;
return person;
}
2、函数修改器
在Solidity中,可以在合约中定义一个或多个函数修改器,并在需要的函数前面使用modifier关键字进行声明。然后,在函数定义中使用modifier关键字来指定应用的修改器。
函数修改器可以用于验证函数的调用者是否具有足够的权限、检查函数的输入参数是否合法、记录函数的执行日志等。
一般是和函数一起使用,在执行函数之前看一下是否符合条件,符合则执行函数,不符合则抛出错误。
通过使用函数修改器,可以提高代码的可读性和可维护性,同时也能够避免代码重复和错误。
3、事件 Event
事件可以被看作是合约与外部世界之间的通信工具。当合约中的某个条件满足时,可以触发一个事件,将相关的信息记录下来。这些信息可以被外部应用程序监听和处理,从而实现合约与外部世界的互动。
事件通常包含一些字段,用于记录与事件相关的数据。当事件被触发时,这些字段的值将被记录下来,并可以被外部应用程序读取和使用。
通过使用事件,可以实现合约的状态变化的可追溯性和可观察性,同时也能够方便地与外部应用程序进行交互和通信。
4、类型
1、布尔类型(bool):表示真或假的值。
2、整数类型(int、uint):表示有符号或无符号的整数值,可以指定位数(如int8、uint256)。
3、地址类型(address):表示以太坊网络上的账户地址。
4、字符串类型(string):表示字符串值。
5、字节数组类型(bytes):表示任意长度的字节数组。
6、固定长度字节数组类型(bytesN):表示固定长度的字节数组,其中N可以是1到32之间的整数。
7、动态数组类型(type[]):表示可变长度的数组,可以存储任意类型的元素。
8、结构体类型(struct):表示自定义的数据结构,可以包含多个字段。
9、枚举类型(enum):表示一组预定义的值。
5、引用类型
(1).存储位置
内存(memory):内存是临时存储数据的地方,用于存储函数参数、局部变量等。在函数调用中,参数默认存储在内存中。内存中的数据在函数执行完毕后会被清除。
存储器(storage):存储器是永久存储数据的地方,用于存储合约的状态变量。存储器中的数据在合约执行期间一直存在,可以被其他合约访问。
调用数据(calldata ):用来保存函数参数的特殊数据位置,是一个只读位置。
状态变量默认存储位置:在合约中声明的状态变量默认存储在存储器中。这些变量的值在合约执行期间一直存在,可以被合约的所有函数访问和修改。
(2).数组
Solidity中的数组是一种用于存储相同类型的多个元素的数据结构。数组可以是固定长度的,也可以是可变长度的。
固定长度数组:固定长度数组在声明时需要指定数组的长度,长度不能改变。例如,uint[3]表示一个包含3个无符号整数的固定长度数组。可以通过索引访问数组中的元素,索引从0开始。例如,myArray[0]表示访问数组myArray中的第一个元素。
可变长度数组:可变长度数组可以在声明时不指定数组的长度,长度可以根据需要进行动态调整。例如,uint[]表示一个可变长度的无符号整数数组。可以使用push()函数向可变长度数组中添加元素,使用length属性获取数组的长度。例如,myArray.push(10)将整数10添加到数组myArray的末尾,myArray.length将返回数组的长度。
数组切片:
数组切片是数组连续部分的视图,用法如:x[start:end] , start 和 end 是 uint256 类型(或结果为 uint256 的表达式)。 x[start:end] 的第一个元素是 x[start] , 最后一个元素是 x[end - 1]。
Solidity中的数组切片是指从一个数组中选取一部分元素组成一个新的数组。数组切片由两个索引值表示,分别是起始索引和结束索引(不包含在切片中)。例如,myArray[2:5]表示从数组myArray中选取从第二个元素到第四个元素(索引为2、3、4)组成一个新的数组。
下面是一个使用数组切片的例子:
uint[] myArray = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
uint[] mySlice = myArray[2:5];
在上面的例子中,我们定义了一个包含10个元素的无符号整数数组myArray,然后使用数组切片选取从第二个元素到第四个元素(索引为2、3、4)组成一个新的数组mySlice。mySlice中的元素为[3, 4, 5]。
length:
数组有 length 成员变量表示当前数组的长度。 一经创建,内存memory 数组的大小就是固定的(但却是动态的,也就是说,它可以根据运行时的参数创建)。
push():
动态的 存储storage 数组以及 bytes 类型( string 类型不可以)都有一个 push() 的成员函数,它用来添加新的零初始化元素到数组末尾,并返回元素引用. 因此可以这样: x.push().t = 2 或 x.push() = b.
push(x):
动态的 存储storage 数组以及 bytes 类型( string 类型不可以)都有一个 push(x) 的成员函数,用来在数组末尾添加一个给定的元素,这个函数没有返回值.
pop():
变长的 存储storage 数组以及 bytes 类型( string 类型不可以)都有一个 pop() 的成员函数, 它用来从数组末尾删除元素。 同样的会在移除的元素上隐含调用 delete ,这个函数没有返回值。
6、映射
在Solidity中,映射(Mapping)是一种键值对的数据结构,类似于其他编程语言中的字典或哈希表。映射允许将一个键(Key)与一个值(Value)相关联,可以通过键来访问和修改对应的值。
映射的声明格式为mapping(KeyType => ValueType) public myMapping;,其中KeyType表示键的类型,ValueType表示值的类型。
下面是一个使用映射的例子:
mapping(uint => string) public myMapping;
function setValue(uint key, string memory value) public {
myMapping[key] = value;
}
function getValue(uint key) public view returns (string memory) {
return myMapping[key];
}
在上面的例子中,我们声明了一个公共的映射myMapping,将无符号整数作为键,字符串作为值。函数setValue用于设置映射中指定键的值,函数getValue用于获取映射中指定键的值。
例如,我们可以通过调用setValue(1, "Hello")来设置键为1的值为"Hello",然后通过调用getValue(1)来获取键为1的值,将返回"Hello"。
映射提供了一种方便的方式来存储和检索键值对数据,可以用于实现各种功能,如存储用户信息、记录状态等。需要注意的是,映射中的键是唯一的,每个键只能对应一个值。
映射只能是 存储(storage) 的数据位置,因此只允许作为状态变量 或 作为函数内的 存储(storage) 引用 或 作为库函数的参数。 它们不能用合约公有函数的参数或返回值。
7、操作符
(1).三元运算符
在Solidity中,三元运算符是一种简洁的条件表达式,用于根据条件的真假选择不同的值。它的语法格式是condition ? value1 : value2,其中condition是一个布尔表达式,value1和value2是两个可能的值。
如果condition为真,则整个表达式的值为value1;如果condition为假,则整个表达式的值为value2。
下面是一个使用三元运算符的例子:
uint a = 10;
uint b = 5;
uint max = (a > b) ? a : b;
在上面的例子中,我们声明了两个无符号整数变量a和b,然后使用三元运算符比较它们的值。如果a大于b,则将max的值设置为a;否则将max的值设置为b。在这个例子中,a的值为10,b的值为5,因此max的值为10。
三元运算符在需要根据条件选择不同值的情况下非常有用,可以减少代码的复杂度并提高可读性。需要注意的是,三元运算符只能在赋值或返回语句中使用,不能用于控制流程或循环语句中。
(2). 复合操作及自增自减操作
如果 a 是一个 LValue(即一个变量或者其它可以被赋值的东西),以下运算符都可以使用简写:
a += e 等同于 a = a + e。其它运算符如 -=, *=, /=, %=, |=, &= , ^= , <<= 和 >>= 都是如此定义的。 a++ 和 a-- 分别等同于 a += 1 和 a -= 1,但表达式本身的值等于 a 在计算之前的值。 与之相反, --a 和 ++a 虽然最终 a 的结果与之前的表达式相同,但表达式的返回值是计算之后的值。
8、字面常量与基本类型的转换
Solidity支持将字面常量与基本类型进行转换。例如,可以将一个字面常量赋值给一个基本类型的变量,或者将一个基本类型的变量转换为另一种基本类型。
下面是一个例子,展示了字面常量与基本类型的转换:
uint8 a = 10; // 将字面常量10赋值给一个uint8类型的变量a
uint16 b = uint16(a); // 将uint8类型的变量a转换为uint16类型的变量b
在上面的例子中,我们首先将字面常量10赋值给一个uint8类型的变量a。然后,我们使用类型转换将uint8类型的变量a转换为uint16类型的变量b。
uint8 a = 12; // 可行
uint32 b = 1234; // 可行
9、合约
(1).状态变量可见性
状态变量有 3 种可见性:
public
对于 public 状态变量会自动生成一个 getter hanshu 函数(见下面)。 以便其他的合约读取他们的值。 当在用一个合约里使用是,外部方式访问 (如: this.x) 会调用getter 函数,而内部方式访问 (如: x) 会直接从存储中获取值。 Setter函数则不会被生成,所以其他合约不能直接修改其值。
internal
内部可见性状态变量只能在它们所定义的合约和派生合同中访问。 它们不能被外部访问。 这是状态变量的默认可见性。
private
私有状态变量就像内部变量一样,但它们在派生合约中是不可见的。
(2).函数可见性
由于 Solidity 有两种函数调用:外部调用则会产生一个 EVM 调用,而内部调用不会, 更进一步, 函数可以确定器被内部及派生合约的可访问性,这里有 4 种可见性:
external
外部可见性函数作为合约接口的一部分,意味着我们可以从其他合约和交易中调用。 一个外部函数 f 不能从内部调用(即 f 不起作用,但 this.f() 可以)。
public
public 函数是合约接口的一部分,可以在内部或通过消息调用。
internal
内部可见性函数访问可以在当前合约或派生的合约访问,不可以外部访问。
private
private 函数和状态变量仅在当前定义它们的合约中使用,并且不能被派生合约使用。
(3).构造函数
Solidity中的构造函数是一种特殊的函数,用于在合约实例化时初始化合约的状态变量或执行其他必要的操作。构造函数的名称必须与合约的名称相同,没有返回类型。通过构造函数,我们可以在合约被实例化时提供参数来初始化合约的状态。
例子:
contract MyContract {
uint public myNumber;
constructor(uint _number) {
myNumber = _number;
}
}
在上面的例子中,我们定义了一个名为MyContract的合约。合约中有一个公共的无符号整数变量myNumber。构造函数的参数是一个无符号整数_number,它被用来初始化myNumber的值。
当我们实例化MyContract合约时,我们需要提供一个无符号整数作为构造函数的参数。构造函数将使用该参数来初始化myNumber的值。例如,如果我们实例化合约并传入参数5,那么myNumber的初始值将被设置为5。可实现合约的灵活性和可重写性。
(4).抽象合约
抽象合约是一种不能被实例化的合约,它只能被其他合约继承并实现其定义的函数。抽象合约用于定义接口,并规定了继承合约需要实现的函数和事件。通过使用抽象合约,可以实现代码的模块化和重用。
abstract contract MyAbstractContract {
function myFunction() public virtual;
}
contract MyContract is MyAbstractContract {
function myFunction() public override {
// 实现函数的具体逻辑
}
}
在上面的例子中,我们定义了一个抽象合约MyAbstractContract,它声明了一个名为myFunction的函数。该函数没有具体的实现。
然后,我们定义了一个合约MyContract,它继承自抽象合约MyAbstractContract。在MyContract中,我们必须实现抽象合约中的myFunction函数,并提供具体的实现。
Virtual:一般要跟抽象合约使用,因为在抽象合约中不被实现,说明是被继承。
Override:说明是继承的,实现父合约中的函数和事件。
(5).接口
Solidity中的接口是一种抽象合约,它只定义了合约的函数声明,没有具体的实现。接口可以用来定义合约之间的通信规范。通过使用接口,我们可以确保合约之间的交互是符合规范的。
下面是一个简单的接口的例子:
interface MyInterface {
function myFunction() external;
}
contract MyContract {
function myFunction() external {
// 实现函数的具体逻辑
}
}
在上面的例子中,我们定义了一个接口MyInterface,它声明了一个名为myFunction的函数。接口中的函数没有具体的实现,只有函数的声明。
然后,我们定义了一个合约MyContract,它实现了接口MyInterface中的myFunction函数,并提供了具体的实现。
(6).继承
Solidity中的继承是一种实现代码重用的机制,它允许一个合约继承另一个合约的状态变量和函数。通过继承,我们可以减少代码的重复,实现代码的模块化和可维护性。
下面是一个简单的继承的例子:
contract ParentContract {
uint public parentVariable;
function parentFunction() public {
// 实现函数的具体逻辑
}
}
contract ChildContract is ParentContract {
uint public childVariable;
function childFunction() public {
// 实现函数的具体逻辑
}
}
在上面的例子中,我们定义了一个父合约ParentContract,它包含一个名为parentVariable的状态变量和一个名为parentFunction的函数。
然后,我们定义了一个子合约ChildContract,它继承自父合约ParentContract。在ChildContract中,我们定义了一个名为childVariable的状态变量和一个名为childFunction的函数。
通过继承,ChildContract继承了ParentContract中的parentVariable状态变量和parentFunction函数。这样,我们就可以在ChildContract中重用ParentContract中的代码,同时也可以扩展ChildContract的功能。
(7).控制结构
Solidity中的控制结构用于控制程序的执行流程,根据条件来决定执行不同的代码块。主要的控制结构包括条件语句(if、else)和循环语句(for、while)。
条件语句(if、else)用于根据条件来选择执行不同的代码块。如果条件为真,则执行if代码块中的语句;如果条件为假,则执行else代码块中的语句(如果有)。
下面是一个简单的例子,演示了使用if语句来判断一个数是否为正数:
function checkPositive(int num) public pure returns (string memory) {
if (num > 0) {
return "Number is positive";
} else if (num == 0) {
return "Number is zero";
} else {
return "Number is negative";
}
}
上述代码中,我们定义了一个函数checkPositive,它接收一个整数参数num。通过if语句,我们判断num的值是否大于0,如果是,则返回"Number is positive";如果等于0,则返回"Number is zero";否则,返回"Number is negative"。
循环语句(for、while)用于重复执行一段代码,直到满足特定条件为止。
下面是一个简单的例子,演示了使用for循环来计算1到10的和:
function calculateSum() public pure returns (uint) {
uint sum = 0;
for (uint i = 1; i <= 10; i++) {
sum += i;
}
return sum;
}
上述代码中,我们定义了一个函数calculateSum,它使用for循环来计算1到10的和。我们初始化一个变量sum为0,然后使用for循环从1到10遍历,每次将当前的数值加到sum上。最后,返回计算得到的sum值。
(8).异常处理
在Solidity中,异常处理用于处理错误和异常情况。当某个条件不满足或发生错误时,可以通过抛出异常来中断程序的执行,并在需要的地方捕获和处理异常。
Solidity中的异常处理机制主要通过断言(assert)和要求(require)语句来实现。断言用于检查代码中的条件是否满足,如果条件不满足,则抛出异常中断程序的执行。要求语句类似于断言,但它还可以在抛出异常之前提供一个错误信息。
下面是一个简单的例子,演示了使用断言和要求来处理异常:
function divide(uint a, uint b) public pure returns (uint) {
assert(b != 0); // 断言:确保除数不为0
require(a > b, "Dividend must be greater than divisor"); // 要求:确保被除数大于除数,否则抛出异常并提供错误信息
return a / b;
}
上述代码中,我们定义了一个函数divide,它接收两个无符号整数参数a和b。在函数体中,我们首先使用断言assert来确保除数b不为0,如果为0,则抛出异常中断程序的执行。接着,我们使用要求require来确保被除数a大于除数b,否则抛出异常并提供错误信息。最后,我们返回a除以b的结果。
通过异常处理机制,我们可以在代码中检查条件并处理异常情况,提高程序的健壮性和安全性。
(9). keccak256
在Solidity中,keccak256是一种哈希函数,用于将输入数据转换为固定长度的哈希值。它可以用来对任何数据进行哈希,包括字符串、整数、地址等。
最简单的方式解释keccak256是将输入数据混合并压缩成一个唯一的字符串。这个字符串是一个固定长度的十六进制数,通常为64个字符。
使用keccak256函数的一个常见用途是验证数据的完整性。通过将数据进行哈希,可以生成一个唯一的哈希值。如果数据发生了任何更改,即使是微小的更改,生成的哈希值也会完全不同。
因此,keccak256函数在Solidity中被广泛用于安全验证、加密和身份验证等方面。它提供了一种可靠的方法来确保数据的完整性和安全性。