泛型数据类型
我们可以使用泛型为像函数签名或结构体这样的项创建定义,这样它们就可以用于多种不同的具体数据类型。在Cairo中,我们可以在定义函数、结构、枚举、trait、实现和方法时使用泛型!在本章中,我们将看看在这些被提到的领域中如何有效地使用泛型。
在函数定义中使用泛型
当定义一个使用泛型的函数时,本来在函数签名中指定参数和返回值的类型的地方,会改用泛型来表示。例如,假设我们想创建一个函数,给定两个 Array
项,函数将返回最大的一个。如果我们需要对不同类型的列表进行这种操作,那么我们就必须每次都重新定义这个函数。幸运的是,我们可以使用泛型来实现这个函数,然后继续完成其他任务。
// Specify generic type T between the angulars fn largest_list<T>(l1: Array<T>, l2: Array<T>) -> Array<T> { if l1.len() > l2.len() { l1 } else { l2 } } fn main() { let mut l1 = ArrayTrait::new(); let mut l2 = ArrayTrait::new(); l1.append(1); l1.append(2); l2.append(3); l2.append(4); l2.append(5); // There is no need to specify the concrete type of T because // it is inferred by the compiler let l3 = largest_list(l1, l2); }
名为largest_list
函数比较了两个相同类型的列表,返回具有更多元素的那一个,并丢弃另一个。如果你编译前面的代码,你会注意到它会出错,说没有为丢弃一个泛型的数组定义trait。这是因为编译器没有办法保证在执行main
函数时,Array<T>
是可以丢弃的。为了丢弃一个T
的数组,编译器必须首先知道如何丢弃T
。可以通过在largest_list
的函数签名中规定T
必须实现drop trait来解决这个问题。largest_list
的正确函数定义如下:
#![allow(unused)] fn main() { fn largest_list<T, +Drop<T>>(l1: Array<T>, l2: Array<T>) -> Array<T> { if l1.len() > l2.len() { l1 } else { l2 } } }
新的largest_list
函数在其定义中包含了一个要求,即无论什么泛型被放在那里,它都必须是可丢弃的。main
函数保持不变,编译器足够聪明,可以得出正在使用的具体类型以及它是否实现了Drop
这个trait。
范型的约束
在定义泛型的时候,掌握关于它们的信息是很有用的。知道一个泛型实现了哪些trait,可以让我们在函数逻辑中更有效地使用它们,代价是限制了可以与函数一起使用的泛型。我们之前看到了一个例子,就是将TDrop
的实现作为largest_list
的泛型参数的一部分。虽然 TDrop
是为了满足编译器的要求而添加的,但我们也可以添加一些约束条件以有利于我们的函数逻辑。
想象一下,我们想,给定一个通用类型T
的元素列表,找到其中最小的元素。首先,我们知道要使一个T
类型的元素具有可比性,它必须实现PartialOrd
这个trait。由此产生的函数将是:
// Given a list of T get the smallest one. // The PartialOrd trait implements comparison operations for T fn smallest_element<T, +PartialOrd<T>>(list: @Array<T>) -> T { // This represents the smallest element through the iteration // Notice that we use the desnap (*) operator let mut smallest = *list[0]; // The index we will use to move through the list let mut index = 1; // Iterate through the whole list storing the smallest loop { if index >= list.len() { break smallest; } if *list[index] < smallest { smallest = *list[index]; } index = index + 1; } } fn main() { let mut list: Array<u8> = ArrayTrait::new(); list.append(5); list.append(3); list.append(10); // We need to specify that we are passing a snapshot of `list` as an argument let s = smallest_element(@list); assert(s == 3, 0); }
名为smallest_element
函数使用一个实现了PartialOrd
的trait的通用类型T
,接收一个Array<T>
的快照作为参数并返回其中最小元素的拷贝。因为参数是@Array<T>
的类型,我们不再需要在执行结束时丢弃它,所以我们不需要为T
实现Drop
特性。那为什么它不能编译呢?
当对list
进行索引时,其结果是对被索引的元素进行快照,除非@T
实现了PartialOrd
,否则我们需要使用 *
对元素进行解快照。*
操作需要从@T
复制到T
,这意味着T
需要实现Copy
特性。在复制了一个@T
类型的元素到T
之后,现在有T
类型的变量需要被删除,这就要求T
也要实现Drop
特性。然后我们必须同时添加Drop
和Copy
特性的实现,以使该函数正确。在更新smallest_element
函数后,产生的代码将是:
#![allow(unused)] fn main() { fn smallest_element<T, impl TPartialOrd: PartialOrd<T>, impl TCopy: Copy<T>, impl TDrop: Drop<T>>( list: @Array<T> ) -> T { let mut smallest = *list[0]; let mut index = 1; loop { if index >= list.len() { break smallest; } if *list[index] < smallest { smallest = *list[index]; } index = index + 1; } } }
Anonymous Generic Implementation Parameter (+
operator)
Until now, we have always specified a name for each implementation of the required generic trait: TPartialOrd
for PartialOrd<T>
, TDrop
for Drop<T>
, and TCopy
for Copy<T>
.
However, most of the time, we don't use the implementation in the function body; we only use it as a constraint. In these cases, we can use the +
operator to specify that the generic type must implement a trait without naming the implementation. This is referred to as an anonymous generic implementation parameter.
For example, +PartialOrd<T>
is equivalent to impl TPartialOrd: PartialOrd<T>
.
We can rewrite the smallest_element
function signature as follows:
#![allow(unused)] fn main() { fn smallest_element<T, +PartialOrd<T>, +Copy<T>, +Drop<T>>(list: @Array<T>) -> T { let mut smallest = *list[0]; let mut index = 1; loop { if index >= list.len() { break smallest; } if *list[index] < smallest { smallest = *list[index]; } index = index + 1; } } }
结构体定义中的泛型
我们也可以使用类似于函数定义的<>
语法来定义结构,它包含一个或多个泛型参数类型字段。首先,必须在结构体名称后面的尖括号中声明泛型参数的名称,接着在结构体定义中可以指定具体数据类型的位置使用泛型类型。下一个代码示例显示了 Wallet<T>
的定义,它有一个 balance
字段,类型为 T
。
#[derive(Drop)] struct Wallet<T> { balance: T } fn main() { let w = Wallet { balance: 3 }; }
上述代码自动为Wallet
类型派生Drop
trait。这效果等同于手动编写以下代码:
struct Wallet<T> { balance: T } impl WalletDrop<T, +Drop<T>> of Drop<Wallet<T>>; fn main() { let w = Wallet { balance: 3 }; }
应该避免使用derive
宏来实现Wallet
的Drop
,而是定义我们自己的WalletDrop
实现。注意,我们必须像定义函数一样,为WalletDrop
定义一个额外的泛型T
并且也实现了Drop
特性。这基本上是在说,只要T
也是可丢弃的,那么钱包<T>
这个结构就是可丢弃的。
最后,如果我们想给Wallet
添加一个代表其Cairo地址的字段,并且我们希望这个字段是与T
不同的另一个泛型,我们可以简单地通过在<>
之间添加另一个泛型来实现:
#[derive(Drop)] struct Wallet<T, U> { balance: T, address: U, } fn main() { let w = Wallet { balance: 3, address: 14 }; }
我们在Wallet
结构定义中添加一个新的泛型U
,然后将这个类型分配给新的字段成员address
。
注意派生属性的 Drop
的trait在新的泛型 U
上同样起作用。
枚举定义中的泛型
和结构体类似,枚举也可以在成员中存放泛型数据类型。例如,Cairo核心库提供的Option<T>
枚举:
enum Option<T> {
Some: T,
None,
}
如你所见 Option<T>
是一个拥有泛型 T
的枚举,它有两个成员:Some
,它存放了一个类型 T
的值,和不存在任何值的None
。通过 Option<T>
枚举可以表达有一个可能的值的抽象概念,同时因为 Option<T>
是泛型的,无论这个可能的值是什么类型都可以使用这个抽象。
枚举也可以拥有多个泛型类型,比如核心库提供的Result<T, E>
枚举的定义:
enum Result<T, E> {
Ok: T,
Err: E,
}
Result<T, E>
枚举有两个泛型类型,T
和E
,以及两个成员:Ok
,存放T
类型的值,Err
,存放E
类型的值。这个定义使得我们可以在任何地方使用Result
枚举,该操作可能成功(返回T
类型的值)或失败(返回E
类型的值)。
方法定义中的泛型
我们可以在结构和枚举上实现方法,也可以在其定义中使用泛型。在之前定义的Wallet<T>
结构体上为其定义一个balance
方法:
#[derive(Copy, Drop)] struct Wallet<T> { balance: T } trait WalletTrait<T> { fn balance(self: @Wallet<T>) -> T; } impl WalletImpl<T, +Copy<T>> of WalletTrait<T> { fn balance(self: @Wallet<T>) -> T { return *self.balance; } } fn main() { let w = Wallet { balance: 50 }; assert(w.balance() == 50, 0); }
我们首先定义了WalletTrait<T>
trait,使用一个泛型T
,它定义了一个方法,从Wallet
中返回字段address
的快照。然后我们在WalletImpl<T>
中给出该trait的实现。请注意,你需要在trait的定义和实现中都包含一个泛型。
在定义类型上的方法时,我们也可以指定对泛型的约束。例如,我们可以只为Wallet<u128>
实例而不是Wallet<T>
实现方法。在代码示例中,我们为钱包定义了一个实现,这些钱包的balance
字段的具体类型为u128
。
#[derive(Copy, Drop)] struct Wallet<T> { balance: T } /// Generic trait for wallets trait WalletTrait<T> { fn balance(self: @Wallet<T>) -> T; } impl WalletImpl<T, +Copy<T>> of WalletTrait<T> { fn balance(self: @Wallet<T>) -> T { return *self.balance; } } /// Trait for wallets of type u128 trait WalletReceiveTrait { fn receive(ref self: Wallet<u128>, value: u128); } impl WalletReceiveImpl of WalletReceiveTrait { fn receive(ref self: Wallet<u128>, value: u128) { self.balance += value; } } fn main() { let mut w = Wallet { balance: 50 }; assert(w.balance() == 50, 0); w.receive(100); assert(w.balance() == 150, 0); }
新的方法receive
增加了Wallet<u128>
的实例的余额大小。请注意,我们改变了main
函数,使w
成为一个可变的变量,以便它能够更新其余额。如果我们通过改变balance
的类型来改变w
的初始化,那么之前的代码就不能编译了。
Cairo也允许我们在泛型trait中定义泛型方法。在之前的 Wallet<U, V>
的实现上定义一个trait,用来选取两个不同泛型的钱包,并创建一个拥有两者泛型新的钱包。首先,让我们重写结构体定义:
struct Wallet<T, U> {
balance: T,
address: U,
}
接下来,我们将初步地定义混合trait和其实现:
// This does not compile!
trait WalletMixTrait<T1, U1> {
fn mixup<T2, U2>(self: Wallet<T1, U1>, other: Wallet<T2, U2>) -> Wallet<T1, U2>;
}
impl WalletMixImpl<T1, U1> of WalletMixTrait<T1, U1> {
fn mixup<T2, U2>(self: Wallet<T1, U1>, other: Wallet<T2, U2>) -> Wallet<T1, U2> {
Wallet { balance: self.balance, address: other.address }
}
}
我们正在创建一个traitWalletMixTrait<T1, U1>
,其中有mixup<T2, U2>
方法,给定一个Wallet<T1, U1>
和Wallet<T2, U2>
的实例,创建一个新的Wallet<T1, U2>
。正如mixup
签名所指定的,self
和other
都在函数的结尾处被丢弃,这就是这段代码不能编译的原因。如果你从开始到现在都跟上了课程,你会知道我们必须为所有的泛型添加一个Drop
trait的实现,以便编译器知道如何丢弃Wallet<T, U>
的实例。更新后的实现如下:
#![allow(unused)] fn main() { trait WalletMixTrait<T1, U1> { fn mixup<T2, +Drop<T2>, U2, +Drop<U2>>( self: Wallet<T1, U1>, other: Wallet<T2, U2> ) -> Wallet<T1, U2>; } impl WalletMixImpl<T1, +Drop<T1>, U1, +Drop<U1>> of WalletMixTrait<T1, U1> { fn mixup<T2, +Drop<T2>, U2, +Drop<U2>>( self: Wallet<T1, U1>, other: Wallet<T2, U2> ) -> Wallet<T1, U2> { Wallet { balance: self.balance, address: other.address } } } }
我们在 WalletMixImpl
"的声明中添加了 T1
和 U1
的可丢弃trait。然后我们对T2
和U2
做同样的处理,这次是作为mixup
签名的一部分。现在我们可以尝试使用mixup
函数了:
fn main() {
let w1 = Wallet { balance: true, address: 10 };
let w2 = Wallet { balance: 32, address: 100 };
let w3 = w1.mixup(w2);
assert(w3.balance == true, 0);
assert(w3.address == 100, 0);
}
我们首先创建两个实例:一个是 Wallet<bool, u128>
,另一个是Wallet<felt252, u8>
。然后,我们调用mixup
并创建一个新的Wallet<bool, u8>
实例。