序
上一期我们学习了rust的所有权、引用、借用、字符串切片类型。这一期我们来讲一讲结构体,可能大部分小伙伴多多少少都听过,比如C语言
中就存在结构体。当然在我们的typescript 中 的class
类在这里就与rust的结构体特别相似。也就是你可以把它当做对象或者类来看待。
定义与实例化
在rust定义结构体,需要使用 struct
关键字并为整个结构体提供一个名字。就像typescript中定义一个接口interface
一样。
struct User {
active: bool,
username: String,
email: String,
sign_in_count: u64,
}
实例化一个结构体,只需要像下面一样,实例中字段的顺序不需要和它们在结构体中声明的顺序一致。这里和javascript的对象行为一致。
fn main() {
let mut user1 = User {
active: true,
username: String::from("someusername123"),
email: String::from("someone@example.com"),
sign_in_count: 1,
};
}
取值也和javascript一致,使用.
操作符,因为这里我们添加mut
,所以可以直接为结构体的字段赋值。
user1.email = String::from("anotheremail@example.com");
注意:
- 没有
user1["XXX"]
这样的取值方式。 - 整个实例必须是可变的;Rust 并不允许只将某个字段标记为可变。
前面我们提到了函数默认返回最后一个表达式,在这里我们也可以返回结构体.
fn build_user(email: String, username: String) -> User {
User {
active: true,
username,
email,
sign_in_count: 1,
}
}
这里我们用到了javascript中object
的特性,当字段名称一致时可以省略,如:email
而不是 email: email
结构体更新
rust为了简便,使用 ..
语法指定了剩余未显式设置值的字段应有与给定实例对应字段相同的值。这里与我们javascript中的...
扩展运算符如出一辙。看下面的例子。
struct User {
active: bool,
username: String,
email: String,
sign_in_count: u64,
}
fn main() {
// --snip--
let user1 = User {
email: String::from("someone@example.com"),
username: String::from("someusername123"),
active: true,
sign_in_count: 1,
};
let user2 = User {
email: String::from("another@example.com"),
..user1
};
}
这里我们将结构体实例 user1
扩展到user2
中,不过要注意这里和javascript不同的地方
- rust使用
..
,javascript使用...
- rust不可以像javascript一样后面的值覆盖前面的值,rust只能覆盖
未显式设置值的字段
,而且需要值类型一致。 - 并且
..user1
必须放在最后。注意没有结尾,
结构更新语法就像带有 = 的赋值,它移动了数据,总体上说我们在创建 user2 后不能就再使用 user1 了,当然这里是因为username是我们前面讲到的复杂类型String
。
如果我们给 user2
的 email
和 username
都赋予新的 String
值,从而只使用 user1
的 active
和 sign_in_count
值,那么 user1
在创建 user2
后仍然有效。active
和 sign_in_count
的类型是实现 Copy
trait 的类型。
所以我们需要记住:在Rust中,基础标量类型(如i32、f64等)默认实现了Copy trait
,因此在进行所有权转移时,会自动使用Copy
语义,而不是Move
语义。但是,如果一个类型没有实现Copy trait
,则在进行所有权转移时会使用Move
语义。需要注意的是,使用Move
语义的变量在所有权转移后会被drop
掉,不能再次使用该变量。
元组结构体
元组结构体有着结构体名称提供的含义,但没有具体的字段名,只有字段的类型。这里相当于给元组取了一个别名。
struct Color(i32, i32, i32);
struct Point(i32, i32, i32);
fn main() {
let black = Color(0, 0, 0);
let origin = Point(0, 0, 0);
}
当然Color
与Point
尽管值一致,但他们是两个不同的数据结构。所以 black
和 origin
值的类型不同的。
既然叫元组结构体,它就像元组一样,你可以将它们解构为单独的部分,也可以使用 .
后跟索引来访问单独的值。
类单元结构体
我们直接来看一个例子,我们仅仅使用了struc xxxx;
,不需要花括号或圆括号!,我们使用的使用也不需要。
struct AlwaysEqual;
fn main() {
let subject = AlwaysEqual;
}
它类似于 ()
,前面我们提到 unit
类型。
类单元结构体常常在你想要在某个类型上实现 trait
但不需要在类型中存储数据的时候发挥作用,这里的trait
我们后面会详细的讲解。
结构体数据的所有权
上面的代码,使用自身拥有所有权的 String
类型而不是 &str
字符串 slice 类型。这是一个有意而为之的选择,因为我们想要这个结构体拥有它所有的数据,为此只要整个结构体是有效的话其数据也是有效的。
当然也可以使结构体存储被其他对象拥有的数据的引用,不过这么做的话需要用上 生命周期(lifetimes)这里我们后面也会详细讲
通过派生 trait 增加实用功能
下面我们使用println!
宏,试图打印时,程序会抛出错误,{}
默认告诉 println!
使用被称为 Display
的格式:意在提供给直接终端用户查看的输出。目前为止见过的基本类型都默认实现了 Display
格式化输出
这里我们补充一下rust格式化输出的知识,这里是rust几种控制台输出的方法。
format!
:将格式化文本写到字符串。print!
:与format!
类似,但将文本输出到控制台(io::stdout)。println!
: 与print!
类似,但输出结果追加一个换行符。eprint!
:与print!
类似,但将文本输出到标准错误(io::stderr)。eprintln!
:与eprint!
类似,但输出结果追加一个换行符。
这些宏都以相同的做法解析文本。有个额外优点是格式化的正确性会在编译时检查。
std::fmt
包含多种 trait
(特质)来控制文字显示,其中重要的两种 trait 的基本形式如下:
fmt::Debug
:使用{:?}
标记。格式化文本以供调试使用。fmt::Display
:使用{}
标记。以更优雅和友好的风格来格式化文本。
fn main() {
// 通常情况下,`{}` 会被任意变量内容所替换。
// 变量内容会转化成字符串。
println!("{} days", 31);
// 不加后缀的话,31 就自动成为 i32 类型。
// 你可以添加后缀来改变 31 的类型(例如使用 31i64 声明 31 为 i64 类型)。
// 用变量替换字符串有多种写法。
// 比如可以使用位置参数。
println!("{0}, this is {1}. {1}, this is {0}", "Alice", "Bob");
// 可以使用命名参数。
println!("{subject} {verb} {object}",
object="the lazy dog",
subject="the quick brown fox",
verb="jumps over");
// 可以在 `:` 后面指定特殊的格式。
println!("{} of {:b} people know binary, the other half don't", 1, 2);
// 你可以按指定宽度来右对齐文本。
// 下面语句输出 " 1",5 个空格后面连着 1。
println!("{number:>width$}", number=1, width=6);
// 你可以在数字左边补 0。下面语句输出 "000001"。
println!("{number:>0width$}", number=1, width=6);
// println! 会检查使用到的参数数量是否正确。
println!("My name is {0}, {1} {0}", "Bond");
// 改正 ^ 补上漏掉的参数:"James"
// 创建一个包含单个 `i32` 的结构体(structure)。命名为 `Structure`。
#[allow(dead_code)]
struct Structure(i32);
// 但是像结构体这样的自定义类型需要更复杂的方式来处理。
// 下面语句无法运行。
println!("This struct `{}` won't print...", Structure(3));
// 改正 ^ 注释掉此行。
}
我们回到正文,结构体并没有提供一个 Display
实现来使用 println!
与 {}
占位符。也没有提供Debug
实现来使用 println!
与 {:?}
占位符。
struct Rectangle {
width: u32,
height: u32,
}
fn main() {
let rect1 = Rectangle {
width: 30,
height: 50,
};
println!("rect1 is {}", rect1);
}
为此,在结构体定义之前加上外部属性 #[derive(Debug)]
,我们就会在控制台输出rect1 is Rectangle { width: 30, height: 50 }
,当然我们可以用更漂亮输出格式{:#?}
,这个占位符,为给我们格式化一下,相当于我们json,格式化一样。
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
fn main() {
let rect1 = Rectangle {
width: 30,
height: 50,
};
println!("rect1 is {:?}", rect1);
}
另一种使用 Debug
格式打印数值的方法是使用 dbg!
宏。dbg!
宏接收一个表达式的所有权(与 println!
宏相反,后者接收的是引用),打印出代码中调用 dbg! 宏时所在的文件和行号,以及该表达式的结果值,并返回该值的所有权。
调用
dbg!
宏会打印到标准错误控制台流(stderr
),与println!
不同,后者会打印到标准输出控制台流(stdout
)
方法语法
前面我们提到了结构体与typescript类相似,但是到目前为止我们都没有在结构体都看见方法的定义,一开始我说过,结构体的定义使用的方式和typescript的interface
接口一样。我们看一个例子
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
impl Rectangle {
fn area(&self) -> u32 {
self.width * self.height
}
}
fn main() {
let rect1 = Rectangle {
width: 30,
height: 50,
};
println!(
"The area of the rectangle is {} square pixels.",
rect1.area()
);
}
我们用 impl
关键字(impl
是 implementation 的缩写),这个 impl
块中的所有内容都将与 Rectangle
类型相关联。
这里和typescript中的implements
有着异曲同工之妙。
在方法中我们可以在第一个参数用self
、&self
,获取到当前结构体的所有权,或者引用。当然我们也可以传更多的参数。
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
impl Rectangle {
fn area(&self) -> u32 {
self.width * self.height
}
fn can_hold(&self, other: &Rectangle) -> bool {
self.width > other.width && self.height > other.height
}
}
fn main() {
let rect1 = Rectangle {
width: 30,
height: 50,
};
let rect2 = Rectangle {
width: 10,
height: 40,
};
let rect3 = Rectangle {
width: 60,
height: 45,
};
println!("Can rect1 hold rect2? {}", rect1.can_hold(&rect2));
println!("Can rect1 hold rect3? {}", rect1.can_hold(&rect3));
}
关联函数
所有在 impl
块中定义的函数被称为 关联函数(associated functions)
注意:方法与函数是不同的,因为它们在结构体的上下文中被定义(或者是枚举或 trait 对象的上下文
我们可以定义不以 self
为第一参数的关联函数(因此不是方法),因为它们并不作用于一个结构体的实例。在前面的学习中我们使用过,在 String
类型上定义的 String::from
函数。
不是方法的关联函数经常被用作返回一个结构体新实例的构造函数。这些函数的名称通常为 new
,但 new
并不是一个关键字。
使用结构体名和 ::
语法来调用这个关联函数,这个函数位于结构体的命名空间中:::
语法用于关联函数和模块创建的命名空间
这里就像typescript class
中的静态方法
一样.
多个 impl 块
每个结构体都允许拥有多个 impl
块。
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
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
}
}
fn main() {
let rect1 = Rectangle {
width: 30,
height: 50,
};
let rect2 = Rectangle {
width: 10,
height: 40,
};
let rect3 = Rectangle {
width: 60,
height: 45,
};
println!("Can rect1 hold rect2? {}", rect1.can_hold(&rect2));
println!("Can rect1 hold rect3? {}", rect1.can_hold(&rect3));
}
结语
我们这里留下来两个没有解决的新概念trait
、生命周期
,后面我们会详细讲到,到底有什么特别之处。
这一期我们,学习结构体的相关操作,整体感觉和typescript的class类似,我们可以把结构体当做class来使用。