结构体的定义和实例化
结构体和元组类似,他们都包含多个相关的值,和元组一样,结构体的每一部分可以是不同类型, 但不同于元组,结构体需要命名各部分数据以便能清除的表明其值的意义。有了这些名字,结构体比元组 更灵活;不需要依赖顺序来指定或访问实例中的值。
rust
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 参数与结构体字段同名
rust
...
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,
}
}
使用结构体更新语法从其他实例创建实例
rust
...
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元组结构体的定义和用法:
rust
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但不需要在类型中存储数据时候发挥作用
rust
struct AlwayEqual;
fn main(){
/*
定义 AlwaysEqual,我们使用 struct 关键字,我们想要的名称,然后是一个分号。不需要花括号或圆括号!然后,我们可以以类似的方式在 subject 变量中获得 AlwaysEqual 的实例:
*/
let subject=AlwayEqual;
}
结构体数据的所有权
在以上代码 中的 User 结构体的定义中,我们使用了自身拥有所有权的 String 类型而不是 &str 字符串 slice 类型。 这是一个有意而为之的选择,因为我们想要这个结构体拥有它所有的数据,为此只要整个结构体是有效的话其数据也是有效的。 可以使结构体存储被其他对象拥有的数据的引用,不过这么做的话需要用上 生命周期(lifetimes),这是生命周期讨论的 Rust 功能。 生命周期确保结构体引用的数据有效性跟结构体本身保持一致。如果你尝试在结构体中存储一个引用而不指定生命周期将是无效的,比如这样:
rust
struct User2 {
active: bool,
username: &str,//报错 &str,^ expected named lifetime parameter
email: &str,
sign_in_count: u64,
}
生命周期会讲到如何修复这个问题以便在结构体中存储引用,不过现在,我们会使用像 String 这类拥有所有权的类型来替代 &str 这样的引用以修正这个错误。
结构体示例程序
为了理解何时会需要使用结构体,让我们编写一个计算长方形面积的程序。我们会从单独的变量开始,接着重构程序直到使用结构体替代他们为止。 使用 Cargo 新建一个叫做 rectangles 的二进制程序,它获取以像素为单位的长方形的宽度和高度,并计算出长方形的面积。
rust
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 本应该计算一个长方形的面积,不过函数却有两个参数。这两个参数是相关联的,不过程序本身却没有表现出这一点。将长度和宽度组合在一起将更易懂也更易处理。 可以重构为元组
rust
//用元组重构
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 。结构体胜在更清晰明了。
rust
#[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增加实用功能
rust
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,它代表调用该方法的结构体实例。
rust
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 的一部分
rust
impl Rectangle{
fn width (&self) -> bool{
self.width>0
}
}
#[derive(Debug)]
struct User{
username:String,
age:u32,
}
带更多参数的方法
rust
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 而不必指定两次同样的值:
rust
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 块中,你可以定义与你的类型相关联的函数,而方法是一种相关联的函数,让你指定结构体的实例所具有的行为。