在Rust中使用结构的基本原理

248 阅读7分钟

什么是结构?

了解面向对象编程是任何开发人员的必修课。面向对象的编程涉及到创建类,这些类作为对象的描述或蓝图。该对象通常是由几个变量或函数组成的。

在C、Go和Rust等语言中,类不是一个特征。相反,这些语言使用结构体,它只定义一组属性。虽然结构体不允许你定义方法,但Rust和Go都以提供访问结构体的方式定义函数。

在本教程中,我们将学习Rust中结构体操作的基本知识。让我们开始吧!

什么是Rust?

Rust是Mozilla创建的一种编程语言,它是一种快速、低级的语言,使用现代语法模式和中央包管理器,扮演着与C类似的角色。

在Rust中编写一个结构

在下面的代码中,我们将为Cat 类型写一个简单的结构,包括属性nameage 。一旦我们定义了我们的结构,我们将定义我们的主函数。

我们将创建一个新的string 和一个新的结构实例,将nameage 属性传给它。我们将打印整个结构,并将其属性插值在一个字符串中。对于name ,我们将使用Scratchy 。对于age ,我们将使用4

// This debug attribute implements fmt::Debug which will allow us
// to print the struct using {:?}
#[derive(Debug)]
// declaring a struct
struct Cat {
 // name property typed as a String type
 name: String,
 // age typed as unsigned 8 bit integer
 age: u8
}
fn main() {
 // create string object with cat's name
 let catname = String::from("Scratchy");
 // Create a struct instance and save in a variable
 let scratchy = Cat{ name: catname, age: 4 };

请注意,我们正在使用derive 属性(我们将在后面详细介绍),以自动实现我们结构上的某些特质。由于我们导出了debug 特质,我们可以使用{:?} 来打印整个结构。

// using {:?} to print the entire struct
 println!("{:?}", scratchy);

 // using individual properties in a String
 println!("{} is {} years old!", scratchy.name, scratchy.age);
}

在这一节中,有几件重要的事情需要注意。首先,与Rust中的任何值一样,结构中的每个属性必须是types 。此外,一定要考虑字符串(字符串对象或结构)和&str (指向字符串的指针)之间的区别。由于我们使用的是字符串类型,所以我们必须从一个合适的字符串字面创建一个字符串。

derive 属性

默认情况下,结构体是不可打印的。一个结构必须实现stc::fmt::debug 函数才能使用{:?} 格式化的println! 。然而,在我们上面的代码示例中,我们使用了derive(Debug) 属性,而不是手动实现一个特质。这个属性使我们能够打印出结构,以方便调试。

属性的作用是向编译器发出指令,写出模板。在Rust中还有几个内置的derive 属性,我们可以用它来让编译器为我们实现某些特征。

  • [#derive(hash)] :将结构体转换为哈希值
  • #derive(clone) :添加了一个克隆方法来复制该结构
  • [#derive(eq)] :实现了eq 特质,将平等设定为所有属性的值都相同。

结构特质

我们可以创建一个带有属性的结构,但如何才能像其他语言中的类那样将其与函数绑定呢?

Rust使用了一种叫做traits的功能,它为结构定义了一捆要实现的函数。特质的一个好处是你可以用它们来进行类型化。你可以创建可以被任何实现相同trait的结构使用的函数。从本质上讲,只要你实现了正确的特质,你就可以在结构中建立方法。

使用特质来提供方法可以实现一种叫做组合的做法,这种做法在Go中也被使用。任何结构都可以混合和匹配它所需要的特质,而不是从一个父类中继承方法,而不是使用层次结构。

编写特质

让我们继续上面的例子,定义一个CatDog 结构。我们希望两者都有一个BirthdaySound 函数。我们将在一个名为Pet 的trait中定义这些函数的签名。

在下面的例子中,我们将使用Spot 作为Dogname 。我们用goes Ruff 作为Sound ,用0 作为age 。对于Cat ,我们将用goes Meow 作为声音,用1 作为age 。生日的函数是self.age += 1;

// Create structs
#[derive(Debug)]
struct Cat { name: String, age: u8 }
#[derive(Debug)]
struct Dog { name: String, age: u8 }
// Declare the struct
trait Pet {
 // This new function acts as a constructor
 // allowing us to add additional logic to instantiating a struct
 // This particular method belongs to the trait
 fn new (name: String) -> Self;
// Signature of other functions that belong to this trait
 // we include a mutable version of the struct in birthday
 fn birthday(&mut self);
 fn sound (&self);
}

// We implement the trait for cat
// we define the methods whose signatures were in the trait
impl Pet for Cat {

 fn new (name: String) -> Cat {
   return Cat {name, age: 0};
 }

 fn birthday (&mut self) {
   self.age += 1;
   println!("Happy Birthday {}, you are now {}", self.name, self.age);
 }

 fn sound(&self){
   println!("{} goes meow!", self.name);
 }
}

// We implement the trait for dog
// we only define sound. Birthday and name are already defined
impl Pet for Dog {

 fn new (name: String) -> Dog {
   return Dog {name, age: 0};
 }

 fn birthday (&mut self) {
   self.age += 1;
   println!("Happy Birthday {}, you are now {}", self.name, self.age);
 }

 fn sound(&self){
   println!("{} goes ruff!", self.name);
 }
}

注意,我们定义了一个新的方法,就像一个构造函数。我们不需要像之前的片段那样创建一个新的Cat ,而是可以直接输入我们的新变量!

当我们调用构造函数时,它将使用该特定类型结构的新实现。因此,DogCat 将能够使用BirthdaySound 函数。

fn main() {
 // Create structs using the Pet new function
 // using the variable typing to determine which
 // implementation to use
 let mut scratchy: Cat = Pet::new(String::from("Scratchy"));
 let mut spot: Dog = Pet::new(String::from("Spot"));

 // using the birthday method
 scratchy.birthday();
 spot.birthday();

 // using the sound method
 scratchy.sound();
 spot.sound();
}

关于特质,有几件重要的事情需要注意。首先,你必须为每个实现特质的结构定义该函数。你可以通过在trait定义中创建默认定义来做到这一点。

我们使用mut 关键字来声明结构,因为结构可以被函数变异。例如,birthday 可以增加年龄。因为birthday 会突变结构的属性,所以我们将参数作为一个可突变的引用传递给结构(&mut self)

在这个例子中,我们使用了一个静态方法来初始化一个新的结构,这意味着新变量的类型是由结构的类型决定的。

返回结构体

有时,一个函数可能会返回几个可能的结构,这发生在几个结构实现相同特征的情况下。要编写这种类型的函数,只需输入实现所需特征的结构的返回值。

让我们继续前面的例子,在一个Box 对象内返回Pet

// We dynamically return Pet inside a Box object
fn new_pet(species: &str, name: String) -> Box<dyn Pet> {

在上面的例子中,我们使用Box 类型作为返回值,这使我们能够为任何实现Pet 特质的结构分配足够的内存。我们可以在我们的函数中定义一个返回任何类型的Pet 结构的函数,只要我们把它包裹在一个新的Box

我们创建一个函数,通过传递一个类型为Petname 的字符串来实例化我们的Pet ,而不指定age 。使用if 语句,我们可以确定要实例化什么类型的Pet

if species == "Cat" {
   return Box::new(Cat{name, age: 0});
 } else {
   return Box::new(Dog{name, age: 0});
 }
}

该函数返回一个Box 类型,这代表内存被分配给一个实现Pet 的对象。当我们创建ScratchySpot ,我们不再需要对变量进行类型化。我们在函数中明确规定了逻辑,在那里将返回一个DogCat

fn main() {
 // Create structs using the new_pet method
 let mut scratchy = new_pet("Cat", String::from("Scratchy"));
 let mut spot = new_pet("Dog", String::from("Spot"));

 // using the birthday method
 scratchy.birthday();
 spot.birthday();

 // using the sound method
 scratchy.sound();
 spot.sound();
}

总结

关于Rust中的结构,我们已经学到了以下内容。

  1. 结构允许我们在一个单一的数据结构中组合属性
  2. 使用特质,我们可以在一个结构上实现不同的方法
  3. 使用特质类型,我们可以编写可以接收和返回结构的函数
  4. 派生属性允许我们在结构中轻松地实现某些特质

现在,我们可以在Rust中使用组合而非继承来实现典型的面向对象的设计模式。

The post Fundamentalsfor using structs in Rust appeared first onLogRocket Blog.