Appendix C - Derivable Traits

在本书的各个部分中,我们讨论了可应用于结构体和枚举定义的 derive 属性。derive 属性会在使用 derive 语法标记的类型上生成对应 trait 的默认实现的代码。

在这个附录中,我们提供了一个全面的参考,详细介绍了标准库中所有与derive属性兼容的trait。

These traits listed here are the only ones defined by the core library that can be implemented on your types using derive. Other traits defined in the standard library don’t have sensible default behavior, so it’s up to you to implement them in a way that makes sense for what you’re trying to accomplish.

Drop and Destruct

When moving out of scope, variables need to be moved first. This is where the Drop trait intervenes. You can find more details about its usage here.

Moreover, Dictionaries need to be squashed before going out of scope. Calling the squash method on each of them manually can quickly become redundant. Destruct trait allows Dictionaries to be automatically squashed when they get out of scope. You can also find more information about Destruct here.

Clone and Copy for Duplicating Values

Clone trait 提供了明确创建一个值的深度拷贝的功能。

派生 Clone 实现了 clone 方法,其为整个的类型实现时,在类型的每一部分上调用了clone 方法。这意味着类型中所有字段或值也必须实现了 Clone,这样才能够派生 Clone

Here is a simple example:

#[derive(Clone, Drop)]
struct A {
    item: felt252
}

fn main() {
    let first_struct = A { item: 2 };
    let second_struct = first_struct.clone();
    assert!(second_struct.item == 2, "Not equal");
}

Copy trait 允许你复制值而不需要额外的代码。你可以在任何部分都实现了Copy的类型上派生Copy

例子:

#[derive(Copy, Drop)]
struct A {
    item: felt252
}

fn main() {
    let first_struct = A { item: 2 };
    let second_struct = first_struct;
    // Copy Trait prevents first_struct from moving into second_struct
    assert!(second_struct.item == 2, "Not equal");
    assert!(first_struct.item == 2, "Not Equal");
}

Debug for Printing and Debugging

The Debug trait enables debug formatting in format strings, which you indicate by adding :? within {} placeholders.

It allows you to print instances of a type for debugging purposes, so you and other programmers using this type can inspect an instance at a particular point in a program’s execution.

For example, if you want to print the value of a variable of type Point, you can do it as follows:

#[derive(Copy, Drop, Debug)]
struct Point {
    x: u8,
    y: u8
}

fn main() {
    let p = Point { x: 1, y: 3 };
    println!("{:?}", p);
}
scarb cairo-run
Point { x: 1, y: 3 }

The Debug trait is required, for example, when using the assert_xx! macros in tests. Theses macros print the values of instances given as arguments if the equality or comparison assertion fails so programmers can see why the two instances weren’t equal.

Default for Default Values

The Default trait allows creation of a default value of a type. The most common default value is zero. All primitive types in the standard library implement Default.

If you want to derive Default on a composite type, each of its elements must already implement Default. If you have an enum type, you must declare its default value by using the #[default] attribute on one of its variants.

An example:

#[derive(Default, Drop)]
struct A {
    item1: felt252,
    item2: u64
}

#[derive(Default, Drop, PartialEq)]
enum CaseWithDefault {
    A: felt252,
    B: u128,
    #[default]
    C: u64,
}

fn main() {
    let defaulted: A = Default::default();
    assert!(defaulted.item1 == 0_felt252, "item1 mismatch");
    assert!(defaulted.item2 == 0_u64, "item2 mismatch");

    let default_case: CaseWithDefault = Default::default();
    assert!(default_case == CaseWithDefault::C(0_u64), "case mismatch");
}

PartialEq for Equality Comparisons

The PartialEq trait allows for comparison between instances of a type for equality, thereby enabling the == and != operators.

When PartialEq is derived on structs, two instances are equal only if all their fields are equal; they are not equal if any field is different. When derived for enums, each variant is equal to itself and not equal to the other variants.

You can write your own implementation of the PartialEq trait for your type, if you can't derive it or if you want to implement your custom rules. In the following example, we write an implementation for PartialEq in which we consider that two rectangles are equal if they have the same area:

#[derive(Copy, Drop)]
struct Rectangle {
    width: u64,
    height: u64,
}

impl PartialEqImpl of PartialEq<Rectangle> {
    fn eq(lhs: @Rectangle, rhs: @Rectangle) -> bool {
        (*lhs.width) * (*lhs.height) == (*rhs.width) * (*rhs.height)
    }

    fn ne(lhs: @Rectangle, rhs: @Rectangle) -> bool {
        (*lhs.width) * (*lhs.height) != (*rhs.width) * (*rhs.height)
    }
}

fn main() {
    let rect1 = Rectangle { width: 30, height: 50 };
    let rect2 = Rectangle { width: 50, height: 30 };

    println!("Are rect1 and rect2 equal? {}", rect1 == rect2);
}

The PartialEq trait is required when using the assert_eq! macro in tests, which needs to be able to compare two instances of a type for equality.

下面是一个例子:

#[derive(PartialEq, Drop)]
struct A {
    item: felt252
}

fn main() {
    let first_struct = A { item: 2 };
    let second_struct = A { item: 2 };
    assert!(first_struct == second_struct, "Structs are different");
}

Serializing with Serde

Serde为你的crate中定义的数据结构提供serializedeserialize函数的trait实现。它允许你将你的结构体转化为数组(或相反)。

Serialization is a process of transforming data structures into a format that can be easily stored or transmitted. Let's say you are running a program and would like to persist its state to be able to resume it later. To do this, you could take each of the objects your program is using and save their information, for example in a file. This is a simplified version of serialization. Now if you want to resume your program with this saved state, you would perform deserialization, which means loading the state of the objects from the saved source.

For example:

#[derive(Serde, Drop)]
struct A {
    item_one: felt252,
    item_two: felt252,
}

fn main() {
    let first_struct = A { item_one: 2, item_two: 99, };
    let mut output_array = array![];
    let serialized = first_struct.serialize(ref output_array);
    panic(output_array);
}

If you run the main function, the output will be:

Run panicked with [2, 99 ('c'), ].

We can see here that our struct A has been serialized into the output array. Note that the serialize function takes as argument a snapshot of the type you want to convert into an array. This is why deriving Drop for A is required here, as the main function keeps ownership of the first_struct struct.

Also, we can use the deserialize function to convert the serialized array back into our A struct.

下面是一个例子:

#[derive(Serde, Drop)]
struct A {
    item_one: felt252,
    item_two: felt252,
}

fn main() {
    let first_struct = A { item_one: 2, item_two: 99, };
    let mut output_array = array![];
    let mut serialized = first_struct.serialize(ref output_array);
    let mut span_array = output_array.span();
    let deserialized_struct: A = Serde::<A>::deserialize(ref span_array).unwrap();
}

Here we are converting a serialized array span back to the struct A. deserialize returns an Option so we need to unwrap it. When using deserialize we also need to specify the type we want to deserialize into.

Hashing with Hash

It is possible to derive the Hash trait on structs and enums. This allows them to be hashed easily using any available hash function. For a struct or an enum to derive the Hash attribute, all fields or variants need to be hashable themselves.

You can refer to the Hashes section to get more information about how to hash complex data types.

Starknet Storage with starknet::Store

The starknet::Store trait is relevant only when building on Starknet. It allows for a type to be used in smart contract storage by automatically implementing the necessary read and write functions.

You can find detailed information about the inner workings of Starknet storage in the Contract storage section.