开始使用Rust—结构

124 阅读10分钟

开始使用Rust:结构

在几乎所有的编程语言中,你都可以创建数据结构--比如类或记录--将一组东西打包在一起。Rust也不例外,它的结构也是如此。

本文将告诉你如何在Rust中使用结构体。

在本文结束时,你会知道:

  • 如何定义和实例化一个结构。
  • 如何为结构派生出特性。
  • 什么是结构方法,以及如何使用它们。

这篇文章的所有代码都可以在这里找到。

什么是Rust中的结构?

在Rust中,结构是一种自定义的数据类型,可以保存多个相关值。

让我们直接进入我们的第一个例子,定义Point --两个坐标的封装器:

// [1]   [2]
struct Point {
// [3] [4]
    x: i32,
    y: i32,
}

// We
// [1]: tell Rust that we want to define a new struct
// [2]: named "Point"
// [3]: that contains two fields with names "x" and "y",
// [4]: both of type "i32".

元组结构

你可以使用元组结构将多个值组合在一起,而不需要对它们进行命名。

元组结构是这样定义的:

//     [1]   [2]  [3]
struct Point(i32, i32);

// [1]: Struct name.
// [2]: First component / field of type i32.
// [3]: Second component of type i32.

如何创建一个结构的实例?

以下是我们之前定义的结构:

// Ordinary struct
struct Point {
    x: i32,
    y: i32,
}

// Tuple struct
struct PointTuple(i32, i32);

现在我们可以创建这些结构的实例--类型为PointPointTuple 的对象:

//             [1]
let p_named = Point {
// [2] [3]
    x: 13,
    y: 37,
};

// [1]: Struct name.
// [2]: Field name.
// [3]: Field value.

//               [1]       [2] [3]
let p_unnamed = PointTuple(13, 37);

// [1]: Struct name.
// [2], [3]: Field values.

正如你所看到的,初始化语法是非常直接的。这就像定义一个JSON对象,其中键被字段名所取代。

有两件事要记住:

  • 所有字段都必须有值。Rust中没有默认参数。
  • 字段-值对的顺序并不重要。如果你愿意,你可以写let p_strange_order = Point { y: 37, x: 13 };

如何访问结构字段?

获取一个值

你可以通过点符号来查询字段的值:

let x = p_named.x;
let y = p_named.y;

有人可能会问:获取元组结构中某个特定字段的值的语法是什么?幸运的是,Rust选择了最简单的方法,即从0开始对元组组件进行索引:

let x = p_unnamed.0;
let y = p_unnamed.1;

设置一个值

使用点符号可以改变一个字段的值,但是结构变量必须定义为可变

let mut p = PointTuple(1, 2);
//  ^^^
p.0 = 1000;
assert_eq!(p.0, 1000);

与其他一些语言不同,Rust不允许将结构的特定字段标记为可变或不可变的:

struct Struct {
    mutable: mut i32,
//           ^^^
//           Expected type, found keyword `mut`.
    immutable: bool,
}

如果你对一个结构有一个不可变的绑定,你就不能改变它的任何字段。如果你有一个可变的绑定,你可以设置你想要的任何字段。

减少模板

在初始化结构字段时,有两个特点可以减少模板代码:

  • 结构更新语法
  • 字段初始化简写

为了说明这一点,我们首先需要定义一个新的结构,并使用常规语法创建一个实例:

struct Bicycle {
    brand: String,
    kind: String,
    size: u16,
    suspension: bool,
}

let b1 = Bicycle {
    brand: String::from("Brand A"),
    kind: String::from("Mtb"),
    size: 56,
    suspension: true,
};

结构更新语法

经常发生的情况是,您想复制一个结构的实例并修改其部分(但不是全部)值。想象一下,一个不同的自行车品牌生产了一个具有相同参数的模型,我们需要创建一个该模型的实例。

我们可以手动移动所有的值:

let b2 = Bicycle {
    brand: String::from("Other brand"),
    kind: b1.kind,
    size: b1.size,
    suspension: b1.suspension,
};

但这种方法涉及太多的代码。结构更新语法可以提供帮助:

let b2 = Bicycle {
    brand: String::from("Other brand"),
    ..b1
//  ^^ struct update syntax
};

..b1 告诉Rust,我们要把剩余的 '的字段从 。b2 b1

字段初始化简写

如果值是一个变量(或函数参数),其名称与字段相匹配,你可以在field: value 初始化语法中省略字段名称:

fn new_bicycle(brand: String, kind: String) -> Bicycle {
    Bicycle {
        brand,
//      ^^^ instead of "brand: brand"
        kind,
//      ^^^ instead of "kind: kind"
        size: 54,
        suspension: false,
    }
}

特质

好吧,让我们试着对我们的结构做一些事情。例如,打印它:

let p = Point { x: 0, y: 1 };
println!("{}", p);
//            ^^^
//             Compile error

突然,编译器说"Point" doesn't implement "std::fmt::Display" ,并建议我们使用{:?} ,而不是{}

好吧,为什么不呢?

let p = Point { x: 0, y: 1 };
println!("{:?}", p);
//              ^^^
//              Compile error

不幸的是,这并没有帮助。我们仍然得到错误信息。"Point" doesn't implement "Debug".

但我们也得到了一个有用的说明:

note: add `#[derive(Debug)]` to `Point` or manually `impl Debug for Point`

出现这个错误的原因是,Debugstd::fmt::Display 都是特性。我们需要实现它们,以使Point 打印出该结构。

什么是trait?

特质类似于Java中的接口或Haskell中的类型分类。它定义了我们可能期望从一个给定类型中获得的某些功能。

通常情况下,trait声明了一个方法列表,这些方法可以在实现该trait的类型上调用。

这里有一个来自标准库的常用特质的列表:

  • Debug.用于格式化(和打印)一个数值,使用{:?}.
  • Clone.用来获取一个值的副本。
  • Eq.用来比较数值是否相等。

在我们的例子中,Point 需要实现Debug trait,因为println!() 宏需要它。

有两种方法来实现一个特性:

  • 派生实现:编译器能够通过#[derive] 宏为固定的特质列表提供基本的实现。
  • 手动编写实现。

让我们从第一个选项开始。我们需要在Point 的定义前添加#[derive] ,并列出我们想要派生的特性:

#[derive(Debug, PartialEq, Eq)]
struct Point {
    x: i32,
    y: i32,
}

let p = Point { x: 0, y: 1 };
println!("{:?}", p);

现在编译器没有抱怨,所以让我们运行我们的程序:

> cargo run
Point { x: 0, y: 1 }

我们没有自己写任何与格式化Point ,但我们已经有了一个看起来不错的输出。很好!"。

EqPartialEq

你可能已经注意到,我们还派生了EqPartialEq 特质。

正因为如此,我们可以比较两个Points是否相等:

let a = Point { x: 25, y: 50 };
let b = Point { x: 100, y: 100 };
let c = Point { x: 25, y: 50 };

assert!(a == c);
assert!(a != b);
assert!(b != c);

手动实现特质

性状派生有其缺点:

  • 只有少量的特征是可以派生的。虽然,程序性的宏允许创建自定义的derive 属性。
  • 有时派生的实现并不符合你的需求。

这就是为什么我们可以自己实现一个特质。

实现Display

当我们试图使用"{}" 打印一个Point 实例时,编译器说Point 没有实现std::fmt::Display 。这个特质与std::fmt::Debug 相似,但它有一些不同之处:

  • 它必须被手动实现。
  • 它应该以更漂亮的方式格式化数值,不包含任何不必要的信息。

你可以把DebugDisplay 分别看作是程序员和用户的格式化器。

让我们添加一个手动实现的Display 。在括号里,我们必须定义特质中声明的每个函数。在我们的例子中,只有一个--fmt

impl std::fmt::Display for Point {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "({}, {})", self.x, self.y)
    }
}

现在我们可以为开发者和用户打印我们的Point 结构。

let p = Point::new(0, 1);

assert_eq!(format!("{:?}", p), "Point { x: 0, y: 0 }");
assert_eq!(format!("{}", p), "(0, 0)");

结构方法

就像在Java或C++中一样,你可以定义与某个特定类型相关的方法。这是在impl 块中完成的:

impl Point {
    // Method that can't modify the struct instance
    fn has_same_x(&self, other: &Self) -> bool {
        self.x == other.x
    }

    // Method that can modify the struct instance
    fn shift_right(&mut self, dx: i32) {
        self.x += dx;
    }
}

您可以通过点符号调用这些方法:

// Mutable binding
let mut point = Point { x: 25, y: 25 };
assert!(!point.has_same_x(&Point { x: 30, y: 25 }));

// Calling a method that changes the object state
point.shift_right(3);
assert_eq!(point.x, 28);

方法可以突变它们所关联的结构实例,但这需要&mut self 作为第一个参数。这样一来,它们就只能通过可变的绑定来调用。

自我

你可能已经注意到,方法的第一个参数是self 。它代表了方法被调用的结构的实例,类似于Python中的做法。这里涉及到一些语法糖。让我们分两步来分解它。

1fn has_same_x(&self, other: &Self)fn shift_right(&mut self, dx: i32)
2fn has_same_x(self: &Self, other: &Self)fn shift_right(self: &mut Self, dx: i32)
3fn has_same_x(self: &Point, other: &Point)fn shift_right(self: &mut Point, dx: i32)

不要把selfSelf 混淆。后者是impl 块的类型的别名。在我们的例子中,它是Point

关联函数

你也可以使用impl 块来定义不占用Self 实例的函数,比如Java中的静态函数。它们通常用于构造类型的新实例。

impl Point {
    // Associated function that is not a method
    fn new(x: i32, y: i32) -> Point {
        Point { x, y }
    }
}

你可以通过使用双分号来调用这些函数 (::)。

let point = Point::new(1, 2);

为什么使用方法而不是普通函数?

方法对于使你的代码变得更好是相当有用的。让我们来看看一些例子。

漂亮的点符号

为了调用一个类型的方法,我们使用点符号。

除了漂亮的语法,点符号还提供了一个额外的属性--自动引用,这意味着你可以写point.shift_right(3); ,而不是(&mut point).shift_right(3);

而函数则没有这个优势,你总是需要引用。

shift_right(&mut point, 3);

代码组织

所有的方法都放在一个或多个impl 块内。这意味着两件事。

首先,你不需要导入方法:

use my_module::MyStruct;

...
let s = MyStruct::new();
s.do_stuff();
s.do_other_stuff();

如果没有方法,就会像这样:

use my_module::{MyStruct, do_stuff, do_other_stuff};  // ugly

...
let s = MyStruct::new();
do_stuff(&s);
do_other_stuff(&s);

第二,方法是有名字的:你不会有跨类型的名字冲突:

my_rectangle.area()
my_circle.area()

如果没有方法,你就需要写成这样:

rectangle_area(my_rectangle)
circle_area(my_circle)

方法与特质

方法和特质有些相似,所以初学者可以把它们混为一谈。

对于初学者来说,何时使用哪种方法的一般规则很简单:

  • 如果你正在实现一个存在特质的普通功能(转换为字符串、比较等),请尝试实现该特质。
  • 如果你正在做一些你的应用程序的特殊功能,可能在常规的impl 块中使用方法。

一个方法只能在它所定义的类型上被调用。而特质则克服了这一限制,因为它们通常是为了被多种不同的类型所实现。

所以特质允许某些函数被泛化。这些函数并不是只接受一种类型,而是接受由特质约束的一组类型。

我们已经看到了这样的例子:

  • println!("{:?}", ...) 不关心我们要打印什么样的对象,只要它实现了 特质。Debug
  • assert_eq!(...) 允许比较任何类型的对象,只要它们实现了 。PartialEq

总结

在这篇文章中,我们介绍了Rust中结构体的基础知识。我们探讨了定义、初始化以及为结构和元组结构添加实现块的方法。我们还研究了特征以及如何派生或实现它们。

结构不是创建自定义类型的唯一方法。Rust也有枚举。虽然我们在这篇博文中没有涉及它们,但方法、特征和派生等概念也可以以类似的方式应用于它们。

如果你想阅读更多关于Rust的初学者文章,请务必在Twitter上关注我们,或通过下面的表格订阅我们的通讯。