上一篇介绍了Rust的所有权特性,今天就把剩下一些之前没介绍但项目中常用的内容总结一下.
- 结构体
- 泛型
- trait
1 结构体
和c语言一样,Rust使用struct
关键字来定义一个结构体,结构体可以将不同的类型数据进行整合,加快内存访问速度.
1.1 结构体定义
struct Test{
username:String,
id:u64,
alive:bool,
}
和c语言类似,我们可以像上面这样将不同的数据类型以及对应字段名封装为一个结构体.当然,和Rust的基本语法一样,这里的类型依然是后置的.
1.2 实例化结构体
let test1=Test{
username:String::from("aaa"),
id:1,
alive:true,
};
我们可以像上面这样直接对结构体中的字段进行赋值,从而创建一个结构体对象.当然也可以像下面这样写一个函数对结构体进行初始化,返回这个结构体就好.
fn new_test(username:String,id:u64)->Test{
Test{
username:username,
id:id,
alive:true,
}
}
let test2=new_test(String::from("bbb"),2);
为了能够顺利打印,我们在结构体定义上方加入一句#[derive(Debug)]
,这是为结构体添加一个attribute(中文不太好翻译,属性?)使得编译器能够自动为带有这个属性的结构体实现Debug
trait.关于trait的内容之前提到过一些,等后期会慢慢总结.有了Debug
trait,我们的结构体才能通过fmt::Debug
顺利输出内容.
当然,Rust的编译器也是足够聪明的.在我们写下new_test
函数之后,编译器自动提示我们需要改进简化代码
编译器提示可以使用初始化缩写,也就是说如果参数名与字段名相同,编译器是能够自动匹配的,这样可以简化为下图
如果我们某个对象与其他对象大部分属性相同,仅仅有某几个属性不同,这时候也不用重新一个个属性去创建,直接使用现有对象属性值就好.
let test3=Test{
id:3,
..test2
};
println!("test3={:#?}",test3);
1.3 结构体的生命周期
之前已经强调过,在Rust中出于内存安全设计很看重变量的生命周期,在结构体中也不例外.除了基础类型,其他类型如果被封装在结构体中,那么必须要考虑数据有效性与生命周期是否一致.当然,我们可以提前定义一个变量然后传入引用,从而延长生命周期.关于生命周期以及生命周期注解相关的内容,稍后会集中讲解.
1.4 定义结构体方法
上面我们已经实现了一个简单的结构体,但这还远远不够.我们希望能为这些结构体定义相应的结构体方法,从而使用结构体内部的成员实现某些功能,比如为上面的结构体实现一个自我介绍功能.
impl Test {
fn introduce(&self)->String{
String::from("My name is:")+ &self.username
}
}
println!("{}",test3.introduce())
已经多次提过生命周期在Rust中的重要性,这里还得再次提醒.为了约束定义的结构体方法在结构体上下文中,所以结构体方法的声明都在impl
包起来的块中.在方法实现时,我们也可以直接传入&self
来代替一个明确的结构体对象或指针,因为方法就在上下文中编译器可以自己推断类型.这里&
表示方法可以拥有结构体的所有权,当然默认依然是不可变借用.
另外一个值得注意的点,不是所有在impl
中定义的都是结构体方法,如果不带有&self
作为参数,那这些函数称为关联函数.
impl Test{
fn from(username:String)->Test{
Test{username, id: 0, alive: false }
}
}
let test4=Test::from(String::from("ccc"));
println!("{:#?}",test4);
所以本质上关联函数依旧是一种函数而不是结构体方法,并不直接作用于某一个结构体对象实例,使用时也需要用::
语法来调用关联函数,最常见的应该就是String::from()
.
2 泛型
对于强类型语言来说,我们在声明某个变量时必须设置对应的数据类型(或者依靠编译器推断).在之前Go(1.18之前)就饱受不支持泛型的困扰,为了要支持不同的传入数据类型就得重复声明同样逻辑的函数.我学Go的时候还是大二,那个时候技术能力不够也就体会不到不支持泛型的痛苦,越到后面写起一些项目同样的逻辑函数仅仅为了支持不同类型就得重复,好在去年Go1.18支持泛型并且整体使用体验还不错.
说回正题,泛型函数往往在定义时不知道传入的类型实参,只有编译调用的时候才能被真正定义传入类型,然后将泛型定义替换为对应的具体类型.
2.1 泛型函数
最常见的应该就是泛型函数,将相同处理逻辑的函数适应不同的输入类型,如下面的demo
fn max_number<T: std::cmp::PartialOrd>(a:T,b:T)->T{
return if a > b {
a
} else {
b
}
}
fn add<T: std::ops::Add<Output=T>>(a:T,b:T)->T{
a+b
}
fn main(){
println!("{} + {} = {}",1,2,add(1,2));
println!("{} + {} = {}",3.14,9.99,add(3.14,9.99));
println!("{} and {},max is {}",1,2,max_number(1,2));
println!("{} and {},max is {}",10.3,2.2,max_number(10.3,2.2));
}
当然上面的demo也说明了泛型并不是随意的.比如求最大值就需要输入的类型能进行比较,也就是说对输入类型进行类型限制,而在Rust中添加类型限制需要一些trait
辅助,关于trait
的内容别急会在下面总结.
2.2 泛型结构体
不止普通函数,结构体的字段类型也可以用泛型定义.比如我们创建一个结构体代表坐标点,就无须纠结坐标到底属于整型还是浮点(当然用浮点数往往更通用)
#[derive(Debug)]
struct Point<T>{
x:T,
y:T,
}
impl<T>Point<T>{
fn swap(&mut self) -> &mut Point<T> {
std::mem::swap(&mut self.x,&mut self.y);
return self;
}
}
fn main(){
let mut point1 =Point{x:1,y:3};
let mut point2 =Point{x:3.4,y:1.2};
let point3=Point{x:1.33,y:1.22};
println!("point1={:#?},point2={:#?},point3={:#?}",point1,point2,point3);
println!("swap point1={:#?},swap point2={:#?}",point1.swap(),point2.swap());
}
这个demo里首先创建了一个泛型结构体Point,里面x
,y
分别代表横纵坐标.此外,我们还为结构体实现了一个swap
方法,用来交换坐标值.但是仍然不满足,我还想实现计算两点之间距离的方法,于是有了下面的demo
trait Distance<T>{
fn distance(&self,other:&T)->f64;
}
impl<T> Distance<Point<T>> for Point<T>
where
T:Sub<Output=T>+Mul<Output=T>+Add<Output=T>+Into<f64>+Copy,
{
fn distance(&self, other: &Point<T>) -> f64 {
let dx=self.x-other.x;
let dy=self.y-other.y;
let dis=dx*dx+dy*dy;
return dis.into().sqrt();
}
}
println!("distance of point2 and point3={}",point2.distance(&point3));
我们定义了一个trait实现distance
方法去计算距离,然后将这个trait指定结构体去具体实现.
Rust不仅泛型支持良好,更令人激动的是使用泛型几乎不会影响性能.因为在编译过程中,编译器会将泛型代码单态化(填入具体类型),因此在运行时不会有任何虚函数调用影响性能.
3 trait
之前的很多demo都使用到了trait,在这个章节就正式来解释一下Rust里常常出现的trait到底是什么.
如果了解其他语言,一定听过接口这个词.那其实trait就是Rust中的接口,只不过相比起普通意义上的接口,Rust中的trait更加灵活并且可以设置默认方法,下面用一个demo来看看trait的常见使用.
3.1 定义trait
trait Callable{
fn call(&self)->String;
}
这里定义了一个Callable
trait,其中方法签名call
要求返回一个String.
3.2 为结构体实现trait
struct Person{
name:String,
age:u8,
sex:String,
}
impl Callable for Person{
fn call(&self) -> String {
format!("my name is {},age:{},sex:{}",self.name,self.age,self.sex)
}
}
fn main(){
let person=Person{name:String::from("aa"),age:18,sex:String::from("male")};
println!("{}",person.call());
}
3.3 trait bound
在定义并实现了trait之后,我们也可以将trait作为参数.特别是在泛型编程中,我们可以通过trait约束传入的类型参数
fn Test<T:Callable>(item:&T){}
并且在上面泛型的demo中也展现了trait是“可加的”,如果需要多种约束,只需要在尖括号中<T:trait1+trait2>
.当然如果对不同参数需要进行不同约束,也可以用where
分别定义trait,这个就和上面demo一样了.
3.4 trait作为返回值
我们可以通过impl trait
作为返回值说明函数返回了某个类型且该类型实现了这个trait,不过这种方法返回只能有一个具体类型.
fn return_callable()->impl Callable{
Person{
name:String::from("call"),
age:11,
sex:String::from("female"),
}
}
let call_person=return_callable();
println!("{:#?}",call_person.call());
struct Dog{
name:String,
}
impl Callable for Dog{
fn call(&self) -> String {
format!("I am a dog,my name is {}",self.name)
}
}
fn return_bad_callable(flag:bool)->impl Callable{
if flag{
Person{
name:String::from("call"),
age:11,
sex:String::from("female"),
}
}else{
Dog{
name:String::from("Wang")
}
}
}
为了解决上面只能匹配某一个实现了trait类型的问题,我们可以用到特征对象.特征对象用dyn
关键字进行类型声明,对比起动态语言中的鸭子类型,Rust这种特征对象没有运行时的检查或者异常,在编译期就能够保证所有特征对象都实现了所定义的trait,否则也不能够顺利编译.
fn create(x:&dyn Callable) -> String {
x.call()
}
fn return_various_call(flag:bool){
if flag{
let tmp_call=Person{
name:String::from("tmp_call"),
age:11,
sex:String::from("female"),
};
println!("{}",create(&tmp_call));
}else{
let tmp_call=Dog{
name:String::from("tmp_Wang")
};
println!("{}",create(&tmp_call));
}
}
像这样我们使用特征对象来构建条件选择,只要我们实现了相应的特征就可以包装为特征对象进行使用.使用特征对象也就意味着是动态分发的,也就是只有运行时才知道具体对应的调用类型.
总结一下,通过trait我们可以定义很多特定了方法并且为每一种类型分别自定义这些trait的实现.但是Rust已经封装好了很多常用的trait,所以更多情况下,我们只需要#[derive(Trait)]
就可以使用了.