结构体实例程序

为了理解何时会需要使用结构体,让我们编写一个计算长方形面积的程序。我们会从单独的变量开始,接着重构程序直到使用结构体替代他们为止。

Let’s make a new project with Scarb called rectangles that will take the width and height of a rectangle specified in pixels and calculate the area of the rectangle. Listing 5-8 shows a short program with one way of doing exactly that in our project’s src/lib.cairo.

文件名: src/lib.cairo

fn main() {
    let width = 30;
    let height = 10;
    let area = area(width, height);
    println!("Area is {}", area);
}

fn area(width: u64, height: u64) -> u64 {
    width * height
}

Listing 5-8: Calculating the area of a rectangle specified by separate width and height variables.

现在用scarb cairo-run运行该程序:

$ scarb cairo-run 
   Compiling listing_04_06_no_struct v0.1.0 (listings/ch05-using-structs-to-structure-related-data/listing_03_no_struct/Scarb.toml)
    Finished release target(s) in 2 seconds
     Running listing_04_06_no_struct
Area is 300
Run completed successfully, returning []

这段代码通过调用每个维度的area函数,成功地算出了矩形的面积,但我们仍然可以修改这段代码来使它的意义更加明确,并且增加可读性。

这段代码的问题在 area 的签名中很明显:

fn area(width: u64, height: u64) -> u64 {

The area function is supposed to calculate the area of one rectangle, but the function we wrote has two parameters, and it’s not clear anywhere in our program that the parameters are related. It would be more readable and more manageable to group width and height together. We’ve already discussed one way we might do that in the Tuple Section of Chapter 2.

Refactoring with Tuples

Listing 5-9 shows another version of our program that uses tuples.

文件名: src/lib.cairo

fn main() {
    let rectangle = (30, 10);
    let area = area(rectangle);
    println!("Area is {}", area);
}

fn area(dimension: (u64, u64)) -> u64 {
    let (x, y) = dimension;
    x * y
}

Listing 5-9: Specifying the width and height of the rectangle with a tuple.

在某种程度上说,这个程序更好一点了。元组帮助我们增加了一些结构性,并且现在只需传一个参数。不过在另一方面,这个版本却有一点不明确了:元组并没有给出元素的名称,所以计算变得更费解了,因为不得不使用索引来获取元组的每一部分。

混淆宽度和高度对于计算面积来说并不重要,但是如果我们想计算差值,那就很重要了。我们必须记住 width 是元组索引0height 是元组索引1。如果其他人要使用这些代码,他们必须要搞清楚这一点,并也要牢记于心。很容易忘记或者混淆这些值而造成错误,因为我们没有在代码中传达数据的意图。

Refactoring with Structs: Adding More Meaning

我们使用结构体为数据命名来为其赋予意义。我们可以将我们正在使用的元组转换成一个有整体名称而且每个部分也有对应名字的结构体。

文件名: src/lib.cairo

struct Rectangle {
    width: u64,
    height: u64,
}

fn main() {
    let rectangle = Rectangle { width: 30, height: 10, };
    let area = area(rectangle);
    println!("Area is {}", area);
}

fn area(rectangle: Rectangle) -> u64 {
    rectangle.width * rectangle.height
}

Listing 5-10: Defining a Rectangle struct.

这里我们定义了一个结构,并将其命名为 Rectangle。在大括号中,我们将字段定义为 widthheight,它们的类型都是 u64。然后,在main中,我们创建了一个Rectangle的特殊实例,它的宽度是30,高度是10。我们的 area函数现在定义了一个名为 rectangle参数,它是Rectangle结构类型。然后我们可以用点符号来访问实例的字段,它给这些值起了描述性的名字,而不是使用01的元组索引值。结构体胜在更清晰明了。

Conversions of Custom Types

We've already described how to perform type conversion on in-built types, see Data Types > Type Conversion. In this section, we will see how to define conversions for custom types.

Note: conversion can be defined for compound types, e.g. tuples, too.

Into

Defining a conversion for a custom type using the Into trait will typically require specification of the type to convert into, as the compiler is unable to determine this most of the time. However this is a small trade-off considering we get the functionality for free.

// Compiler automatically imports the core library, so you can omit this import
use core::traits::Into;

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

#[derive(Drop)]
struct Square {
    side_length: u64,
}

impl SquareIntoRectangle of Into<Square, Rectangle> {
    fn into(self: Square) -> Rectangle {
        Rectangle { width: self.side_length, height: self.side_length }
    }
}

fn main() {
    let square = Square { side_length: 5 };
    // Compiler will complain if you remove the type annotation
    let result: Rectangle = square.into();
    let expected = Rectangle { width: 5, height: 5 };
    assert!(
        result == expected,
        "A square is always convertible to a rectangle with the same width and height!"
    );
}

TryInto

Defining a conversion for TryInto is similar to defining it for Into.

// Compiler automatically imports the core library, so you can omit this import
use core::traits::TryInto;

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

#[derive(Drop, PartialEq)]
struct Square {
    side_length: u64,
}

impl RectangleIntoSquare of TryInto<Rectangle, Square> {
    fn try_into(self: Rectangle) -> Option<Square> {
        if self.height == self.width {
            Option::Some(Square { side_length: self.height })
        } else {
            Option::None
        }
    }
}

fn main() {
    let rectangle = Rectangle { width: 8, height: 8 };
    let result: Square = rectangle.try_into().unwrap();
    let expected = Square { side_length: 8 };
    assert!(
        result == expected,
        "Rectangle with equal width and height should be convertible to a square."
    );

    let rectangle = Rectangle { width: 5, height: 8 };
    let result: Option<Square> = rectangle.try_into();
    assert!(
        result.is_none(),
        "Rectangle with different width and height should not be convertible to a square."
    );
}