likes
comments
collection
share

Rust快速学习开发(六)-使用结构体组织相关联的数据

作者站长头像
站长
· 阅读数 23

结构体的定义和实例化

结构体和元组类似,他们都包含多个相关的值,和元组一样,结构体的每一部分可以是不同类型, 但不同于元组,结构体需要命名各部分数据以便能清除的表明其值的意义。有了这些名字,结构体比元组 更灵活;不需要依赖顺序来指定或访问实例中的值。

struct User {//结构体   结构体名字
	//... 字段
	active:bool,
	username:String,
	email:String,
	sign_in_count:u64
}
fn main() {
 /*
	定义结构体:使用struct关键字并为整个结构体提供一个名字,结构体的名字需要描述它所组合的数据的意义。
接着,在大括号中,定义每一部分数据的名字和类型,我们称之为字段
*/
	let mut user1= User {//结构体   结构体名字
	//... 字段
	active:true,
	username:String::from("米粉"),
	email:String::from("mifen@qq.com"),
	sign_in_count:100
	};
}

改变User实例email字段的值

注意整个实例必须是可变的,rust并不允许只将某个字段标记为可变,另外需要注意同其他任何表达式一样,可以在函数体的最后一个表达式中构造一个结构体的新实例, 来隐式地返回这个实例。 build_user 函数使用了字段初始化简写语法,因为 username 和 email 参数与结构体字段同名

...
fn main (){
  ...
         /*
	创建User结构体的实例:
	*/
	//1.从结构体中获取值可以使用点号
	user1.email=String::from("1958766541@qq.com");//如果结构体带是可变的可以通过点字段名为字段赋值
  build_user(String::from("小黑子@qq.com"),String::from("小黑子"));
}

fn build_user(email:String,username:String)-> User{
	User {
		active:true,
		//字段初始化简写语法
		username,//username:username,
		email,//email:email,
		sign_in_count:1,
	}
}

使用结构体更新语法从其他实例创建实例

...
fn main (){
    ...
    //方法1:逐一赋值创建新 User 实例
	let user2=User{
        active: user1.active,
        username: user1.username,
        email: String::from("another@example.com"),
        sign_in_count: user1.sign_in_count,
	};
	//方法2:使用 user1 中的一个值创建一个新的User 实例
	let user2=User{
		email:String::from("1245411@qq.com"),
		..user1//..user1 必须放在最后,以指定其余的字段应从 user1 的相应字段中获取其值
	};

}

请注意,结构更新语法就像带有 = 的赋值,因为它移动了数据,就像我们在“变量与数据交互的方式(一):移动”部分讲到的一样。在这个例子中, 总体上说我们在创建 user2 后不能就再使用 user1 了,因为 user1 的 username 字段中的 String 被移到 user2 中。 如果我们给 user2 的 email 和 username 都赋予新的 String 值,从而只使用 user1 的 active 和 sign_in_count 值,那么 user1 在创建 user2 后仍然有效。 active 和 sign_in_count 的类型是实现 Copy trait 的类型,所以我们在“变量与数据交互的方式(二):克隆” 部分讨论的行为同样适用。

使用没有命名字段的元组结构体来创建不同的类型

定义元组结构体:以struct关键字和结构体名开头后跟元组中的类型。例下面两个分别叫做Color和Point元组结构体的定义和用法:

struct Color(i32,i32,i32);
struct Point(i32,i32,i32);
fn main(){
	let black=Color(0,0,0);
	let origin=Point(0,0,0);
}

没有任何字段的类单元结构体

可以定义一个没有任何字段的结构体,被称为 “类单元结构体”,类似于()。即“元组类型”中提到的 unit 类型。 类单元结构体常常在你想要的某个类型上实现trait但不需要在类型中存储数据时候发挥作用

struct AlwayEqual;
fn main(){
   /*
   定义 AlwaysEqual,我们使用 struct 关键字,我们想要的名称,然后是一个分号。不需要花括号或圆括号!然后,我们可以以类似的方式在 subject 变量中获得 AlwaysEqual 的实例:
   */
   let subject=AlwayEqual;
}

结构体数据的所有权

在以上代码 中的 User 结构体的定义中,我们使用了自身拥有所有权的 String 类型而不是 &str 字符串 slice 类型。 这是一个有意而为之的选择,因为我们想要这个结构体拥有它所有的数据,为此只要整个结构体是有效的话其数据也是有效的。 可以使结构体存储被其他对象拥有的数据的引用,不过这么做的话需要用上 生命周期(lifetimes),这是生命周期讨论的 Rust 功能。 生命周期确保结构体引用的数据有效性跟结构体本身保持一致。如果你尝试在结构体中存储一个引用而不指定生命周期将是无效的,比如这样:

struct User2 {
    active: bool,
    username: &str,//报错 &str,^ expected named lifetime parameter
    email: &str,
    sign_in_count: u64,
}

生命周期会讲到如何修复这个问题以便在结构体中存储引用,不过现在,我们会使用像 String 这类拥有所有权的类型来替代 &str 这样的引用以修正这个错误。

结构体示例程序

为了理解何时会需要使用结构体,让我们编写一个计算长方形面积的程序。我们会从单独的变量开始,接着重构程序直到使用结构体替代他们为止。 使用 Cargo 新建一个叫做 rectangles 的二进制程序,它获取以像素为单位的长方形的宽度和高度,并计算出长方形的面积。

fn main() {
    let width1=30;
    let height1=50;
    println!("The area of the rectangle is {} square pixels",area(height1,width1));
}
fn area(height:u32,width:u32) -> u32 {
   width * height 
}

用元组重构

函数 area 本应该计算一个长方形的面积,不过函数却有两个参数。这两个参数是相关联的,不过程序本身却没有表现出这一点。将长度和宽度组合在一起将更易懂也更易处理。 可以重构为元组

//用元组重构
fn main(){
    let rect1=(30,50);
    println!("The area of th rectangle if {} square pixels",area_stup(rect1))
}
fn area_stup(dimensions:(u32,u32))->u32{
    dimensions.0*dimensions.1
}

在某种程度上说,这个程序更好一点了。元组帮助我们增加了一些结构性,并且现在只需传一个参数。 不过在另一方面,这个版本却有一点不明确了:元组并没有给出元素的名称,所以计算变得更费解了,因为不得不使用索引来获取元组的每一部分: 在计算面积时将宽和高弄混倒无关紧要,不过当在屏幕上绘制长方形时就有问题了!我们必须牢记 width 的元组索引是 0,height 的元组索引是 1。 如果其他人要使用这些代码,他们必须要搞清楚这一点,并也要牢记于心。很容易忘记或者混淆这些值而造成错误,因为我们没有在代码中传达数据的意图。

使用结构体重构:赋予更多的意义

这里我们定义了一个结构体并称其为 Rectangle。在大括号中定义了字段 width 和 height,类型都是 u32。接着在 main 中,我们创建了一个具体的 Rectangle 实例,它的宽是 30,高是 50。

函数 area 现在被定义为接收一个名叫 rectangle 的参数,其类型是一个结构体 Rectangle 实例的不可变借用。第四章讲到过,我们希望借用结构体而不是获取它的所有权,这样 main 函数就可以保持 rect1 的所有权并继续使用它,所以这就是为什么在函数签名和调用的地方会有 &。 如下代码: area 函数访问 Rectangle 实例的 width 和 height 字段(注意,访问对结构体的引用的字段不会移动字段的所有权,这就是为什么你经常看到对结构体的引用)。 area 的函数签名现在明确的阐述了我们的意图:使用 Rectangle 的 width 和 height 字段,计算 Rectangle 的面积。 这表明宽高是相互联系的,并为这些值提供了描述性的名称而不是使用元组的索引值 0 和 1 。结构体胜在更清晰明了。


#[derive(Debug)]
struct Rectamgle{
    width:u32,
    height:u32
}
fn main(){
    let  rectamgle=Rectamgle {
        width:30,
        height:50,
    };
    println!("The area of th rectangle if {} square pixels",area_strcut(&rectamgle));

}
fn area_strcut(rectamgle:&Rectamgle)-> u32{
    rectamgle.width*rectamgle.height
}

通过派生trait增加实用功能

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

    //显示rect1方法一
    /*
    Rust 确实 包含了打印出调试信息的功能,不过我们必须为结构体显式选择这个功能。为此,在结构体定义之前加上外部属性 #[derive(Debug)],括号替换为{:?},就能打印出信息
    */
    println!("rect1 is {:?}", rect1);//报错,println! 宏不能处理结构体,结构体并没有提供一个 Display 实现来使用 println! 与 {} 占位符。

    //显示rect1方法2 另一种使用 Debug 格式打印数值的方法是使用 dbg! 宏。dbg! 宏接收一个表达式的所有权(与 println! 宏相反,后者接收的是引用),打印出代码中调用 dbg! 宏时所在的文件和行号,以及该表达式的结果值,并返回该值的所有权。
    //注意:调用 dbg! 宏会打印到标准错误控制台流(stderr),与 println! 不同,后者会打印到标准输出控制台流(stdout)
    dbg!(&rect1);
}

方法语法

方法与函数类似,它们使用fn关键字和名称声明,可以拥有参数和返回值,同时 包含在某处调用该方法时会执行的代码,不过方法与函数时不同的,因为它们在结构体的上下文中被定义,并且 它们第一个参数总是self,它代表调用该方法的结构体实例。

fn main() {
     let rect1 = Rectangle {
        width: 30,
        height: 50,
    };
    println!(
        "The area of the rectangle is {} square pixels.",
        rect1.area()//方法语法调用 1.3然后在 main 中将我们先前调用 area 方法并传递 rect1 作为参数的地方,改成使用 方法语法(method syntax)在 Rectangle 实例上调用 area 方法。方法语法获取一个实例并加上一个点号,后跟方法名、圆括号以及任何参数。
    );

    if rect1.width() {//true 字段变成私有的,但方法是公共的
        println!("rect1.width is lg 0 ---{}",rect1.width);
    };
}

#[derive(Debug)]
struct Rectangle {
    width: u32,
    height: u32,
}

impl Rectangle{//1.1为了使函数定义于 Rectangle 的上下文中,我们开始了一个 impl 块(impl 是 implementation 的缩写),这个 impl 块中的所有内容都将与 Rectangle 类型相关联
    // &self 来替代 rectangle: &Rectangle,&self 实际上是 self: &Self 的缩写
      fn area(&self) -> u32 {//1.2接着将 area 函数移动到 impl 大括号中,并将签名中的第一个(在这里也是唯一一个)参数和函数体中其他地方的对应参数改成 self。
        self.width * self.height
    }
}

定义方法可以和结构化里面的字段同名

这样的方法被称为 getters,Rust 并不像其他一些语言那样为结构字段自动实现它们。Getters 很有用,因为你可以把字段变成私有的,但方法是公共的,这样就可以把对字段的只读访问作为该类型公共 API 的一部分

impl Rectangle{
    fn width (&self) -> bool{
        self.width>0
    }
}

#[derive(Debug)]
struct User{
    username:String,
    age:u32,
}

带更多参数的方法

fn main(){
    let user1=User{
        username:String::from("小红"),
        age:30,
    };
      let user2=User{
        username:String::from("小黄"),
        age:29,
    };
      let user3=User{
        username:String::from("小白"),
        age:32,
    };
    println!("Can user1 hold user2? {}", user1.can_hold(&user2));//true
    println!("Can user1 hold user3? {}", user1.can_hold(&user3));//false
}
impl User {
    fn can_hold(&self,other:&User)->bool{
        self.age>other.age
    }
}

关联函数

所有在impl块中定义的函数被称为关联函数,因为它们与impl后面命名的类型相关,我们可以定义不以self为第一参数的关联函数(因此不是方法) 因为它们并不作用于一个结构体的实例。 不是方法的关联函数经常被用作返回一个结构体新实例的构造函数。这些函数的名称通常为 new ,但 new 并不是一个关键字。 例如我们可以提供一个叫做 square 关联函数,它接受一个维度参数并且同时作为宽和高,这样可以更轻松的创建一个正方形 Rectangle 而不必指定两次同样的值:

impl Rectangle {
    fn square(size:u32)->Self{
        Self {
            width:size,
            height:size
        }
    }
}

/*
    每个结构体都允许拥有多个impl块。但每个方法有其自己的 impl 块。
*/
impl Rectangle {
    fn area(&self) -> u32 {
        self.width * self.height
    }
}

impl Rectangle {
    fn can_hold(&self, other: &Rectangle) -> bool {
        self.width > other.width && self.height > other.height
    }
}

总结

结构体让你可以创建出在你的领域中有意义的自定义类型。通过结构体,我们可以将相关联的数据片段联系起来并命名它们,这样可以使得代码更加清晰。 在 impl 块中,你可以定义与你的类型相关联的函数,而方法是一种相关联的函数,让你指定结构体的实例所具有的行为。