10、初识Rust – 结构体
Rust中的结构体和元组类似,它们都可以声明许多相关的值,每一部分也可以是不同的类型。但是,和元组不一样的是,结构体需要命名每个部分数据以便能清楚表明这个值的意义。由于这些值有名字,所以结构体比元组更加灵活,不需要依赖顺序或者索引去访问实例中的值。类似固定类型的JSON
struct User { name: String, passwd: String, status: bool }
定义一个节奏体需要使用 struct 关键字,并为结构体提供一个名字。接着在大括号中定义每一部分数据的名字和类型,称为 字段( field )。如上代码就展示了一个存储用户账号的结构体。
定义结构体之后,为了使用它,通过为每个字段定义的具体值来创建这个结构体的实例,创建一个实例需要以结构体的名字开头,接着在 : 使用指定类型的key 来为实例附值。
#[derive(Debug)] struct User { name: String, passwd: String, status: bool } fn main() { let a = User { name: String::from("fcy"), passwd: String::from("xxxxxx"), status: true }; println!("{:?}",a); }
如上,为User 结构体赋予不同类型的值,并打印到终端。
struct User { name: String, passwd: String, status: bool } fn main() { let a = User { name: String::from("fcy"), passwd: String::from("xxxxxx"), status: true }; println!("{},{},{}",a.name,a.passwd,a.status); }
像json一样使用结构体变量 . 字段名称来获取值,但是需要注意,如果该字段不存在,就会抛出错误。
struct User { name: String, passwd: String, status: bool } fn main() { let mut a = User { name: String::from("fcy"), passwd: String::from("xxxxxx"), status: true }; a.name = String::from("newName"); println!("{},{},{}",a.name,a.passwd,a.status); }
如果需要修改其中的值,同样也需要在变量名称中加入mut,否则就会报错。
struct User { name: String, passwd: String, status: bool } fn main() { let a = test_function(String::from("fcy"),String::from("xxxxxx")); println!("{},{},{}",a.name,a.passwd,a.status); } fn test_function(name: String, passwd: String) -> User{ User { name, passwd, status: true } }
如上是一个字段初始化的简写语法,我们可以将User的初始化写在一个函数中,并直接将值传入函数中,注意函数传入值的名称与结构体中字段的名称一样,这样我们就可以不必去填写 字段名称: 字段值这样的写法,编译器会自动处理。
struct User { name: String, passwd: String, status: bool } fn main() { let a = User { name: String::from("fcy"), passwd: String::from("xxxxxx"), status: true }; let b = User { status: false, passwd: String::from("zzzzz"), ..a }; println!("{},{},{}",b.name,b.passwd,b.status); }
如上是一种结构体更新语法,它可以从其旧实例创建新实例并更新部分值。需要注意的是,..a必须写在结构体尾部,这样就能自动赋予剩余未显示设置字段的值到新实例对应的值,同时,该操作也是一个移动操作,所以在附值后,原实例的值会失效,只有部分没有赋予的值才会依旧存在。
除了有字段的结构体外,也可以创建没有命名字段的结构体。
#[derive(Debug)] struct xyz(i8,i8,i8); fn main() { let a = xyz(2,3,4); println!("{:?},{},{}",a,a.0,a.2); }
如上代码,我们声明一个xyz的位置结构体,并在main代码块中附值并绑定变量a然后打印。从打印的语句中我们可以发现,没有字段的结构体可以使用 a.0 , a.1这样的方式来访问其中的值。
除了无字段的结构体外,我们也可以创建没有任何字段的类单元结构体。
struct xyz; fn main() { let a = xyz; }
不过,由于部分内容这里未学习到,所以不在此讲此结构体的作用。并且目前我们声明的结构体都使用了自身拥有所有权的string类型,并不是&str类型。因为目前使用引用的话会报错并提示需要生命周期标识符,这涉及到后续的 生命周期 功能,所以这里也不具体说明,只使用String这类类型暂替。
那么,我们为什么需要使用到结构体呢?因为结构体相对于元组和一般变量来说,更加容易被人看明白。如下代码,我们将用三种方式来书写
//使用变量 fn main() { let width = 20; let height = 20; println!("{}",area(width,height)); } fn area(width: i32, height: i32) -> i32 { width * height }
//使用元组 fn main() { let rect = (20,20); println!("{}",area(rect)); } fn area(rect: (i32,i32)) -> i32 { rect.0 * rect.1 }
// 使用结构体 struct Rect { width: i32, height: i32 } fn main() { let rect = Rect { width: 20, height: 20 }; println!("{}",area(rect)); } fn area(rect: Rect) -> i32 { rect.width * rect.height }
结构体相对于变量的优势是,它很好的关联了数据,使代码更加易懂也更加容易处理。而相对于元组,可读性的优势将更加明显。同时派生 trait之后,可以像JSON一样直接打印出整个结构。
同时,我们也可以在结构体内定义方法。
struct Rect { width: i32, height: i32 } impl Rect { fn area(&self) -> i32 { self.width * self.height } } fn main() { let rect = Rect { width: 20, height: 20 }; println!("{}",rect.area()); }
为了使 area函数定义在Rect上下文中,需要开始一个impl块(impl
是 implementation 的缩写),这个impl 块中的所有内容都将和Rect类型相关联。接着将area函数移动到impl大括号中,并将签名中的第一个参数,也是唯一一个参数 &self,self参数用来代替rect: Rect,&self
实际上是 self: &Self
的缩写。接着就可以在main块中声明Rect的宽高,并调用定义的area函数直接返回面积。
->
运算符到哪去了?在 C/C++ 语言中,有两个不同的运算符来调用方法:
.
直接在对象上调用方法,而->
在一个对象的指针上调用方法,这时需要先解引用(dereference)指针。换句话说,如果object
是一个指针,那么object->something()
就像(*object).something()
一样。Rust 并没有一个与
->
等效的运算符;相反,Rust 有一个叫 自动引用和解引用(automatic referencing and dereferencing)的功能。方法调用是 Rust 中少数几个拥有这种行为的地方。它是这样工作的:当使用
object.something()
调用方法时,Rust 会自动为object
添加&
、&mut
或*
以便使object
与方法签名匹配。也就是说,这些代码是等价的:第一行看起来简洁的多。这种自动引用的行为之所以有效,是因为方法有一个明确的接收者————
self
的类型。在给出接收者和方法名的前提下,Rust 可以明确地计算出方法是仅仅读取(&self
),做出修改(&mut self
)或者是获取所有权(self
)。事实上,Rust 对方法接收者的隐式借用让所有权在实践中更友好。
除了单&self参数外,也可以进行多参数函数。
struct Rect { width: i32, height: i32 } impl Rect { fn area(&self) -> i32 { self.width * self.height } fn contrast(&self,other: &Rect) -> bool { let rect = &self.width * &self.height; let other_rect = &other.width * &other.height; rect < other_rect } } fn main() { let rect1 = Rect { width: 20, height: 20 }; let rect2 = Rect { width: 22, height: 18 }; let rect3 = Rect { width: 30, height: 30 }; println!("{}",rect1.contrast(&rect2)); println!("{}",rect1.contrast(&rect3)); println!("{}",rect1.area()); }
如上就增加了一个对比面积函数 contrast 通过传入不同的Rect结构体来对比任意结构体的大小并返回bool类型。
同时,我们也可以去掉&self参数来定义一个关联函数,它接收一个参数并同时作为宽高,这样可以更好的创建一个正方形。
#[derive(Debug)] struct Rect { width: i32, height: i32 } impl Rect { fn area(other: &Rect) -> i32 { other.width * other.height } fn square(size: i32) -> Rect { Rect { width: size, height: size } } } fn main() { let rect = Rect::square(10); let a = Rect::area(&rect); println!("{:?}",&rect); println!("{}",a); }
所有在 impl
块中定义的函数被称为 关联函数(associated functions),因为它们与 impl
后面命名的类型相关。我们可以定义不以 self
为第一参数的关联函数(因此不是方法),因为它们并不作用于一个结构体的实例。我们已经使用了一个这样的函数:在 String
类型上定义的 String::from
函数。使用结构体名和 ::
语法来调用这个关联函数:比如 let rect = Rect::square(10);
。这个函数位于结构体的命名空间中:::
语法用于关联函数和模块创建的命名空间。
#[derive(Debug)] struct Rect { width: i32, height: i32 } impl Rect { fn area(other: &Rect) -> i32 { other.width * other.height } } impl Rect { fn square(size: i32) -> Rect { Rect { width: size, height: size } } } fn main() { let rect = Rect::square(10); let a = Rect::area(&rect); println!("{:?}",&rect); println!("{}",a); }
并且每个结构体impl块允许拥有多个,哪怕写成如上那样没有什么意义的多impl,它也是有效的。结构体让你可以创建出在你的领域中有意义的自定义类型。通过结构体,我们可以将相关联的数据片段联系起来并命名它们,这样可以使得代码更加清晰。在 impl
块中,你可以定义与你的类型相关联的函数,而方法是一种相关联的函数,让你指定结构体的实例所具有的行为。