Rust 一些笔记
阶段性笔记(理解)
- 引用和借用
- 泛型
- struct
- trait
引用和借用
引用和借用都有生命周期。引用是作为参数传递给函数的地址。
创建一个变量,借用/引用都是指向同一个内存地址。只不过所有权不一样。
借用&: let b = &3;
引用ref: let ref c: i32 = 2;
这两种类型都是一样的,都是&i32
。
声明变量时,都可以用&或者ref,两者的效果是一样的。但是如果声明类型时,就只能用&了。
struct T<'a>{
// 这里只用&,如果用 ref 'a i32, 则会报错
v: &'a i32,
}
*号用来解引用,解引用之后才能对变量做计算操作。
struct
struct的参数声明可以理解为__init__方法,在实例化的时候必须传值进去声明。
struct里的方法可以理解为不同的类,在不同的类中定义方法。
泛型
泛型的目的是为了抽象化代码,理解为将类型做为参数传进去(公共的模板)。
但是这样的话就意味着可以传入所有的类型参数进去,可能这个函数接收到这个类型,函数里的代码有些方法对于这个类型来说用不了(比如说比较),就会编译失败。此时就需要对传入的类型参数做出一些限制。
对一个函数的泛型做出限制: fn findMax<T: PartialOrd + Copy>(list: &[T]) -> T{}
在枚举和结构体中
在结构体中声明了泛型后,里面的字段就必须使用这个泛型,不能声明了一次都不用。
生命周期
为了保证引用总是有效的,不产生悬垂指针。
这里就会涉及到了rust借用,借用是指一块内存空间的引用。可以理解为一个指针指向了这块内存地址。rust有一条借用规则:借用方的生命周期不能比出借方的生命周期还长。
意思是如果被借用方已经被释放了,此时借用的状态必须是不存在的。杜绝悬垂指针的出现。
生命周期在函数中的使用
如果有一个函数,它的参数和返回值都是引用,此时这个函数的参数是出借方,函数返回值所绑定到的那个变量就是借用方,这种函数也要满足上述的借用规则。也就是说,返回值引用绑定的变量的生命周期不能大于函数参数的生命周期。
返回值引用绑定的变量 < 函数参数
fn max_num(x: &i32, y: &i32) -> &i32 {
if x > y {
&x
} else {
&y
}
}
fn main() {
let x = 1; // -------------+-- x start
let max; // -------------+-- max start
{ // |
let y = 8; // -------------+-- y start
max = max_num(&x, &y); // |
} // -------------+-- y over
println!("max: {}", max); // |
} // -------------+-- max, x over
为什么要手动添加生命周期标注?
如果不标记生命周期参数,那么编译器就不会知道这个函数返回的引用的生命周期是什么。
试想一下,如果在函数中,借用方的生命周期比出借方的高会发生什么
首先确定一点:这个函数的返回值是传入的参数。所以,假设如果参数已经被销毁了,但是这个函数的返回值却又指向了这个函数的参数,此时垂悬指针就发生了。
做了生命周期参数有什么用?
fn max_num<'a>(x: &'a i32, y: &'a i32) -> &'a i32 {
if x > y {
&x
} else {
&y
}
}
这里做了<'a>标记,作用是告诉编译器函数参数和函数返回值的生命周期一样长。
把x和y的生命周期和max_num的函数生命周期 'a 建立关联。但x和y的生命周期的长短是不一样的,其实是取两个参数中最短的生命周期作为关联。
函数的生命周期参数并不会改变生命周期的长短,只是用于编译来判断是否满足借用规则。
如果函数参数的生命周期参数与函数返回值的生命周期参数不建关联的话,那么生命周期参数就没有任何意义。
代码理解
也就是说,max的生命周期 = 函数返回的生命周期 = y的生命周期。即如果max离开了y所在的作用域后就会被销毁。
标识符的作用:fn max_num<'a>(x: &'a i32, y: &'a i32) -> &'a i32
- 第1个'a代表的意思是声明一个函数的生命周期参数。
- 第2个,第3个参数上的'a是代表使用这个生命周期标记(范围)'a。
- 第4个'a的意思是函数的返回值的声明周期也是'a,取参数间最短生命周期。
- 这个函数的返回值的生命周期也是'a
再复杂的一点代码
fn max_num<'a, 'b: 'a>(x: &'a i32, y: &'b i32) -> &'a i32 {
if x > y {
&x
} else {
&y
}
}
Self self
self表示调用方法的对象,类似python里的self。
它是函数的第一个参数,等同于self: Self
,&self = self &Self
如果使用self,函数就会被称为方法。如果没有函数,函数就被称为关联函数(静态方法)。
self代表的是实例化的对象。
Self表示调用者类型。
为什么使用&self而不是self
/*
doSometing: self
doSomething2: &self
*/
let obj1 = Foo{name: "achu".to_string()};
let obj2 = Foo{name: "ache".to_string()};
// 这里等价于 Foo::doSomething(obj1, 1)
obj1.doSomething(1);
// 这里传入的是引用
obj2.doSomething2(1);
因为在调用self方法的时候,实际上是将调用者的对象传入了进去。如果不使用引用的话(如doSomething),在调用这个方法后,obj1就会消失,因为所有权传入到里面了。
trait
其实这个就是类似于一个公共的继承类。这个类中有各种方法,当继承了这个类之后就可以重写里面的方法。
trait作为参数
fn printSomething(item: impl Foo){item.doSomething();}
必须要求printSomething的参数item有实现Foo trait接口。
默认方法
有了默认方法后,又重写方法后生效。
trait_bound
fn printSomething(item: impl Foo){item.doSomething();}
可以修改成以下:
fn printSomething<T: Foo>(item: T){item.doSomething();}
trait作为返回值只能返回一个结构体。
trait 其他
继承一个trait必须里面只有trait的方法。
对任何实现了特定trait的类型有条件的实现trait
因为:
struct的参数声明可以理解为__init__方法,在实例化的时候必须传值进去声明。
struct里的方法可以理解为不同的类,在不同的类中定义方法。
trait又可以理解为继承。
所以:
在trait里实现的方法,只要继承了这个trait就可以用。可以定义一个泛型T(这个T必须拥有GetName的trait),让这个T方法实现print_name函数。这样后来的结构体继承GetName之后就可以直接用到print_name函数(中转)。
struct Foo{
name: String,
}
trait GetName{
// 声明方法
fn get_name(&self) -> &String;
fn direct_print_name(&self, something: &String);
}
trait PrintName{
fn print_name(&self);
}
impl<T: GetName> PrintName for T{
// 这个泛型方法的作用为将PrintName trait附加给GetName trait
fn print_name(&self){
// 这里T要求必须有GetName trait,所以可以调用get_name()
println!("{}", self.get_name())
}
}
impl GetName for Foo{
fn get_name(&self) -> &String{
&(self.name)
}
fn direct_print_name(&self, something: &String){
self.print_name();
}
}
fn main(){
let obj = Foo{name: "achu".to_string()};
let result = obj.get_name();
obj.direct_print_name(&result);
obj.print_name();
}