Rust —— 高级篇(1)

闭包
Rust中闭包的定义是:可以作为值(函数值)赋值给变量,可以捕获作用域中的值。
let mut cache = Cacher::new(|s| String::from(s));
上面的|s| String::from(s)
就是一个闭包函数,它可以作为一个函数值进行传递。并且我们可以看到,s的类型并没有显示声明,因为相比于函数,闭包并不需要暴露给用户,因此它可以享受Rust编译器类型推导的功能。
PS:编译器推导出一种类型后就会一直用下去,闭包和泛型是不一样的。
接下来看看结构体中的闭包
struct Cacher<T, E>
where
T: Fn(E) -> E,
{
query: T,
value: Option<E>,
}
impl<T, E> Cacher<T, E>
where
T: Fn(E) -> E,
{
fn new(query: T) -> Cacher<T, E> {
Cacher { query, value: None }
}
fn get_cache(&mut self, arg: E) -> &Option<E> {
match &self.value {
Some(_) => &self.value,
None => {
let v = (self.query)(arg);
self.value = Some(v);
&self.value
}
}
}
}
上面的特征Fn
代表的就是闭包,这里他接受一个泛型参数E,并返回一个类型为E的值。
捕获作用域中的值
闭包可以直接访问作用域中的值
let x = 1;
let func = |y| y == x;
assert!(func(3));
当闭包从环境中捕获一个值时,会分配内存去存储这些值,在一些特殊场景中,这种额外的内存分配会成为一种负担。但是函数并不会去捕获环境中的值,因此函数就没有这方面的烦恼。
3种Fn特征
FnOnce
,这种类型的闭包会拿走被捕获变量的所有权
fn fn_once<F>(func: F)
where
F: FnOnce(usize) -> bool,
{
println!("{}", func(3));
println!("{}", func(4));
}
fn main() {
let x = vec![1, 2, 3];
fn_once(|z|{z == x.len()});
}
上面这段代码会报错,因为FnOnce在调用的时候会获取被捕获变量的所有权,因此在func(3)
完成调用之后,x已经被消耗掉了,对于func(4)
来说已经没有可用的环境,因此它只能调用一次。
如果我们希望让闭包强制获取环境变量的所有权,可以使用move关键字。这种做法通常用在闭包作用域比环境更大的情况下,比如将闭包返回或移入其他线程。
use std::thread;
let v = vec![1, 2, 3];
let handle = thread::spawn(move || {
println!("Here's a vector: {:?}", v);
});
handle.join().unwrap();
FnMut
,它会以可变借用的方式捕获环境中的值,因此它可以修改该值
let mut s = String::new();
let mut update_string = |str| s.push_str(str);
update_string("hello");
println!("{:?}", s);
Fn
,该特征以不可变借用的方式捕获环境变量中的值
fn test_closure() {
exec_two(|s| println!("{:?}", s))
}
fn exec_two<'a, F: Fn(String) -> ()>(f: F) {
f("hello".to_string());
}
实际上,闭包的真实类型取决于我们对捕获变量的操作,如果我们在闭包中没有对捕获变量进行修改,那么就实现了
Fn
,哪怕我们声明时将它当作FnMut
PS:如果闭包捕获的环境变量都实现了Copy
特征,那么该闭包也就实现了Copy
特征。
// 拿所有权
let s = String::new();
let update_string = move || println!("{}", s);
exec(update_string);
// exec2(update_string); // 不能再用了
// 可变引用
let mut s = String::new();
let mut update_string = || s.push_str("hello");
exec(update_string);
// exec1(update_string); // 不能再用了
由于上面的代码中第一部分将s的所有权传递给了闭包,因此不能进行Copy,也就无法传递给其他函数使用。第二部分传递了可变引用,而在同一作用域内,可变引用与不可变引用及其他可变引用无法同时存在,因此不能再次使用。
闭包作为返回值
fn fn_factory() -> impl Fn(i32) -> i32 {
let num = 1;
move |x| {x + num}
}
上面的代码返回了一个简单的闭包函数,注意这里使用到了move关键字,因为环境变量的生存周期比闭包要小,所以必须将它的所有权转移到闭包中才能正常工作。
如果要返回相同特征的不同实现,可以使用特征对象实现。
迭代器
Rust提供了IntoIterator
特征,所有实现该特征的都可以通过for
语法糖实现遍历
Rust中的迭代器采用惰性初始化,单纯的创建几乎没有任何性能开销,只有真正开始遍历的时候才会开始
如果一个迭代器想要取出元素,还必须实现对应的特征Iterator
,该特征中有一个next
方法,用于取出下一个元素。
pub trait Iterator {
type Item;
fn next(&mut self) -> Option<Self::Item>;
// 省略其余有默认实现的方法
}
我们总共有三种方式获取可迭代数据结构的迭代器:
into_iter
会夺取所有权iter
不可变借用iter_mut
可变借用
消费者与适配器
消费者适配器
消费者迭代器会消费掉迭代器上的元素,所有实现了next
方法的迭代器都是消费者迭代器。
迭代器适配器
迭代器适配器则不会直接消费掉迭代器,而是返回一个新的迭代器,这也是链式调用的关键。不过迭代器适配器是惰性的,也就是说它必须有个消费者迭代器来进行收尾。
下面是一个用迭代器适配器实现的HashMap构造
fn iter_test() {
let names = ["cxc", "nb"];
let ages = [1, 2];
let folk: HashMap<_,_> = names.into_iter().zip(ages.into_iter().skip(1)).collect();
println!("{:#?}", folk);
}
智能指针
Box
堆栈性能:
- 小型数据:栈上的分配性能和读取性能比堆上高
- 中型数据:栈上分配性能高,但读取性能并没有区别,因为无法利用CPU高速缓存和寄存器,必须要经过一次内存寻址。
- 大型数据:只建议分配在堆上
应用场景
避免栈上数据拷贝
实现了
Copy
特征的类型都可以在栈上拷贝,不涉及所有权转移。但如果栈上的数据比较大,比如说一个1000大小的数组,此时我们可能就不希望直接在栈上拷贝,而是转移所有权,此时就可以使用Boxfn main() { let arr = Box::new([0;1000]); let arr1 = arr; // 这里发生所有权转移 println!("{:?}", arr1.len()); }
将动态大小类型转换为固定大小类型
这个很好理解,很多对象只有在运行时才知道大小,无法分配在栈上,这时就需要用到指针了。
特征对象
Box::leak
Box
提供了一个非常有用的关联函数leak
,它可以直接消费掉Box并且让内部的指从内存中泄露。
fn gen_static_str() -> &'static str {
let mut s = String::new();
s.push_str("Hello");
Box::leak(s.into_boxed_str())
}
这么做的价值在于,我们有一个在运行时才能初始化的值,并且我们希望它能在全局生效。因为一旦从内存中泄露,这个值的生命周期就是整个程序运行周期。
一个简单的Box实现
use std::ops::Deref;
struct MyBox<T>(T);
impl <T> MyBox<T> {
fn new(x: T) -> MyBox<T> {
MyBox(x)
}
}
impl<T> Deref for MyBox<T> {
type Target = T;
fn deref(&self) -> &Self::Target {
&self.0
}
}
当我们用智能指针进行解引用时,rust的实际调用是*(y.deref())
,首先获取引用然后再通过*的解引用获取到值。这样可以做到获取值的同时,不拿走智能指针对值的所有权。
Deref
若一个类型实现了Deref
特征,那么它的引用在传递给函数或方法时,会根据参数签名来决定是否进行隐式的Deref
转换。(仅引用类型的实参才会触发自动解引用)
并且Deref
还支持多重的解引用,如果发现实参还可以解引用,并且与形参仍旧不匹配,那么编译器会自动尝试继续解引用。
3种Deref
转换:
- 当
T: Deref<Target=U>
,可以将&T
转换成&U
- 当
T: DerefMut<Target=U>
,可以将&mut T
转换成&mut U
。(通过实现DrefMut
特征实现) - 当
T: Deref<Target=U>
,可以将&mut T
转换成&U
。(Rust本身具备的隐式转换)
Drop
Rust通过所有权来实现无GC与内存安全,编译器会自动为几乎所有的类型实现Drop
特征,下面是一个手动实现Drop
特征的例子。
struct HasDrop1;
struct HasDrop2;
impl Drop for HasDrop1 {
fn drop(&mut self) {
println!("Dropping HasDrop1!");
}
}
impl Drop for HasDrop2 {
fn drop(&mut self) {
println!("Dropping HasDrop2!");
}
}
struct HasTwoDrops {
one: HasDrop1,
two: HasDrop2,
}
impl Drop for HasTwoDrops {
fn drop(&mut self) {
println!("Dropping HasTwoDrops!");
}
}
struct Foo;
impl Drop for Foo {
fn drop(&mut self) {
println!("Dropping Foo!")
}
}
fn main() {
let _x = HasTwoDrops {
two: HasDrop2,
one: HasDrop1,
};
let _foo = Foo;
println!("Running!");
}
注意drop
函数使用的是一个可变借用,也就是说他不会拿走所有权,drop操作是通过操作可变引用直接实现的。也就是说,如果我们手动调用这个drop函数,对应的操作对于其他变量来说是无感知的,他们依旧会认为自己可以访问这个已经被drop的值。
因此编译器禁止出现这类代码,如果确实需要手动回收,可以使用std::mem::drop
,这个函数会直接拿走所有权,避免了上述情况的发生。实际上这个函数做的操作仅仅是拿走所有权,真正的释放操作仍旧由Drop::drop
来执行,以此来避免二次析构。
实现1vN所有权机制
Rust所有权机制要求一个值只能有一个所有者,但是有些时候,我们确实需要让某个值可以被多个所有者持有。例如,多线程模式下的共享变量。
Rc<T>
Rc就是所谓的引用计数,这里Rust通过引用计数的方式实现1vN所有权机制,当引用计数归0之后,对应的数据就会被清除。
fn rc_test() {
let a = Rc::new(S);
let b = Rc::clone(&a);
println!("{:?}:count: {}", a, Rc::strong_count(&a));
}
上面是一个简单的使用例子,Rc::clone
会复制一个指针,而不是数据,a和b的底层数据是相同的。利用Rc::strong_count
则可以获取当前指向底层数据的引用个数。
事实上,
Rc
中存储的都是不可变引用,因为按照Rust的借用规则,同一作用域中只有不可变引用允许存在多个。
不过Rc
并不能在多线程环境下工作,因为它没有实现Send
特征(用于多线程中传递数据)。除此之外,Rc
对引用计数器的操作也不是原子的,因此它无法在并发环境下工作。
而在并发环境下,一般都是使用Arc
来实现相关操作,它实现了对应的原子操作,是线程安全的。
内部可变性
Rust通过严格的规则来保证所有权和借用的正确性,但这带来的问题就是,给程序员的压力变得很大,丧失了大量的灵活性。
因此Rust提供了Cell
和RefCell
来处理内部可变性。所谓的内部可变性就是对一个不可变的值进行可变借用,这显然是不符合要求的,但很多时候我们确实会有这种需求。因此需要通过在外层包裹Cell
或RefCell
来实现相关的功能。
Cell
与RefCell
的区别在于Cell
处理实现了Copy
特征的数据,RefCell
反之
fn cell_test() {
let c = Cell::new("abc");
let a = c.get();
c.set("obq");
println!("{}, {}", c.get(), a);
}
上面的代码中abc
是一个字符串切片,属于不可变引用,但是我们可以通过Cell
对其中的值进行修改。
RefCell
则可以用于所有没有实现Copy
特征的结构,但需要注意的是,他并没有彻底绕开借用规则,他只是能够实现编译期可变,而运行时依旧是不可变的。也就是说,他只是将错误推迟到了运行时发生。
而它存在的意义就是,程序员相信自己代码的正确性,编译器发生了误判,此时就可以通过RefCell
绕开限制。
trait Sender {
fn send(&self, msg: String);
}
struct MsgSender {
mail_box: Vec<String>,
}
impl Sender for MsgSender {
fn send(&self, msg: String) {
self.mail_box.push(msg);
}
}
上面的代码会报错,因为&self
获取的是不可变引用,send
方法中无法修改相关的数据结构。虽然换为&mut self
就可以解决问题,但假如Sender
特征是定义在外部库中,我们无法修改,这种方式就行不通了。
另一种解决方法就是用RefCell
包装Vec
,这样我们就可以获取到它的可变借用并进行修改,同时绕过编译器限制。
struct MsgSender {
mail_box: RefCell<Vec<String>>,
}
impl Sender for MsgSender {
fn send(&self, msg: String) {
self.mail_box.borrow_mut().push(msg);
}
}
还有一个使用场景是:由于Rust的mutable
特性,一个结构体中的字段要么全是可变要么全是不可变。但很多时候,我们只希望结构体中的部分字段可以被修改,这时就可以使用到Cell
和RefCell
了,只要用它们包装指定字段,就可以实现对特定字段开放修改。
struct Person {
name: String,
age: Cell<u32>,
}
let p = Person {name: "abc".to_string(), age: Cell::new(1)};
p.age.set(p.age.get() + 1);
上面的例子中,我们可以通过Cell
对年龄进行修改,但name
无法被修改。
循环引用
上面提到Rc
使用引用计数来处理1vN的所有权关系,而引用计数有一个非常严重的缺陷就是无法处理循环引用。而许多数据结构都依赖于循环引用,例如链表和树,如果不能正确处理引用计数的问题,在Rust中就很容易导致OOM。
因此Rust提供了一种新的结构,Weak
弱引用,他和Rc
的区别在于它不保证指向值的存在性,也就是说它也可能指向一个已经被drop
掉了的东西。换来的是Weak
不会增加引用计数,这就帮助我们解决了循环引用的问题。
Weak
的大致用法:面对父子引用的关系,可以使用Weak
来引用父节点,Rc
来引用子节点。(一个节点不一定会有父亲,但手底下的儿子必须能够是可以访问的)
生命周期
先来看一个特殊的生命周期示例
#[derive(Debug)]
struct Foo;
impl Foo {
fn mutate_and_share(&mut self) -> &Self {
&*self
}
fn share(&self) {}
}
fn main() {
let mut foo = Foo;
let loan = foo.mutate_and_share();
foo.share();
println!("{:?}", loan);
}
上面的代码再12行生成了一个可变对象,接着在13行获取了它的可变借用,并最终返回一个不可变借用,之后在14,15行分别获取了一次不可变借用。按道理来说这段代码是可以通过编译的,因为不可变借用仅发生在函数体里,不会影响到main函数。
但事实是,这段代码无法通过编译,我们下意识认为可变借用会在函数结束后消失,然而实际上,根据生命周期消除原则的第三点,当输入生命周期中包含self时,返回值会被赋予一个和self一样的生命周期。也就是说,编译器认为函数传入的可变借用的生命周期和返回出去的不可变生命周期相同,在main函数中继续使用不可变借用就违反了借用原则。
无界生命周期
不安全代码经常会凭空产生引用或生命周期,这些周期被称为是无界的。这里的凭空产生指的是,输入生命周期中根本没有这个生命周期,但在输出时他出现了,这往往是在解引用一个裸指针时出现的。
fn f<'a, T>(x: *const T) -> &'a T {
unsafe {
&*x
}
}
上面的代码就产生了一个无界生命周期,由于这种生命周期没有任何约束,因此它可大可小。
生命周期约束HRTB
'a:'b
表示'a >= 'b
,a至少活的跟b一样久
struct Ref<'a,'b:'a, T> {
r: &'a T,
s: &'b T
}
上面这段代码中,编译器可以从中了解到,s会活得比r更久
T:'a
表示类型T必须比'a
活得更久
struct Ref<'a, T: 'a> {
r: &'a T
}
这段代码表示T对应的值必然比r这个引用活得久,不过在新版本中,我们可以去掉T: 'a
的声明,编译器会自动进行相关的消除操作
Reborrow
#[derive(Debug)]
struct Point {
x: i32,
y: i32,
}
impl Point {
fn move_to(&mut self, x: i32, y: i32) {
self.x = x;
self.y = y;
}
}
fn main() {
let mut p = Point { x: 0, y: 0 };
let r = &mut p;
let rr: &Point = &*r;
println!("{:?}", rr);
r.move_to(10, 10);
println!("{:?}", r);
}
上面这段代码中的rr就是对r的再借用。乍一看,它破坏了可变引用与不可变引用不可同时存在的原则,但由于rr是再引用,它不会破坏借用规则,只要不在rr的生命周期内再使用r即可(类似变量遮蔽?)。
上面的例子中的用法可能看起来有点牵强,更经典的场景是函数体内对参数的二次借用
fn read_length(strings: &mut Vec<String>) -> usize {
strings.len()
}
这段代码中,read_length
获取到了strings的可变引用,他在调用len()
方法时传入了一个不可变借用,这里就发生了再借用。
&’static和T:’static
&'static
对生命周期的要求是:一个引用必须活得跟剩下的程序一样久,但对于该引用指向的对象来说,依旧遵循作用域原则。
T:'static
意思差不多,指的是T这个值(不管这个值是对象还是个指针)必须和程序活得一样久。这里可能会和所有权的概念搞混,既然变量超出作用域后会被回收,那怎么可能存在和程序活得一样久的东西。但实际上,变量在丢失所有权时实际被drop的是这个变量以及它指向的分配在堆上的空间。如果这个变量指向的是一个存储在静态区域的值,比如字符串字面值,那么超出作用域就仅仅是变量被drop了,对应的值依旧保留着。
T:'static
属于特征约束,表示T不会接收任何非static引用,对于接收方来说可以安全的持有T直到自己将其drop
深入类型
Rust中提供了as
关键字来实现强制类型转换,但它的限制是必须从范围大的对象转换成范围小的对象。
如果我们希望在类型转换上拥有完全的控制,那么可以考虑使用TryInto
。
let b: u16 = 1500;
let b_: u8 = match b.try_into() {
Ok(b1) => b1,
Err(e) => {
println!("{:?}", e.to_string());
0
}
};
println!("{b_}");
点操作符
进行方法调用的点操作符看似简单,实际上包含了大量的类型转换。
在具体介绍点操作符的底层原理之前,先了解一下完全限定语法:<Type as Trait>::function(receiver_if_method, next_arg, ...);
接下来介绍一下点操作符的具体操作:
- 首先,编译器检查他是否可以直接调用
T::foo(value)
,这称为值方法调用 - 如果上一步调用失败(例如类型错误,或没有针对Self的实现)。此时编译器会自动添加引用,并再次尝试调用,此时会尝试的调用如下:
<&T>::foo(value)
和<&mut T>::foo(value)
- 如果上面两个方法都不管用,那么就会尝试解引用T,这里使用了
Deref
特征,编译器会使用解引用的类型继续尝试调用 - 若T不能被解引用,且T是一个定长类型,则编译器会尝试将T转换为不定长类型,例如将
[i32;3]
变为[i32]
- 寄!!!
接下来通过一个例子来辅助理解
#[derive(Clone)]
struct Container<T>(Arc<T>);
fn clone_test<T>(foo: &Container<i32>, bar: &Container<T>) {
let foo_cloned = foo.clone();
let bar_cloned = bar.clone();
}
首先回想一下复杂类型派生Clone
的规则:内部所有子类型都实现了Clone
规则
这里i32实现了Clone规则,并且Container也实现了Clone规则。因此编译器可以直接进行值方法调用fn clone(&T) -> T
,所以foo_cloned
是Container类型。
但T这里没有实现Clone特征,因此编译器无法进行值方法调用,所以这里它会取引用,此时的方法签名为fn clone(&&T) -> &T
,可以进行调用(引用都是实现了Clone特征的)。因此bar_cloned
是一个引用类型
但如果我们自己手动实现Container<T>
的Clone特征,就可以确保最后能够产出一个Container类型而不是引用。
newtype
所谓的newtype
实际上就是用元组结构体的方式将已有类型包裹起来。例如struct Meters(u32)
。
表面上看这种做法好像是多此一举,但实际上它可以为我们提供许多方面的帮助。
为外部类型实现外部特征
Rust在特征实现上遵循孤儿原则,即如果要实现一个特征,那么对应的特征与实现特征的结构至少有一个处于当前作用域。换句话说,我们无法给一个外部类实现外部特征。
因此,这时候我们可以通过
newtype
创建一个包装类,内部存放着要实现特征的外部类,这样我们就可以实现需要的外部特征。struct Wrapper(Vec<String>); impl fmt::Display for Wrapper { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "[{}]", self.0.join(", ")) } }
更好的可读性
可读性这一点很好理解,
Meters
肯定比u32
更能直观的说明作用类型异化
对于
newtype
来说,即使它内部存储着相同类型的数据,对于编译器来说,他们也是不同的,也就是说我们可以更好地限定参数的传递等操作。
类型别名
类型别名也是一种修改类型名称的方法,它跟newtype
的区别在于,newtype
确确实实创建了一个新类型,而类型别名仅仅是一个别名而已。
type Meters = u32;
// x和y的类型实际上没有任何区别
let x: u32 = 5;
let y: Meters = 5;
类型别名一般用于简化一些冗长的声明,减少代码量提高可读性。
不定长类型DST
目前接触到的所有类型几乎都是定长类型,这里的定长指的是编译器在编译时就知道了类型值的大小,因此它可以直接确定在开辟函数栈时需要多少空间(不是指Vec
或数组这类可/不可伸缩类型)。因为那些可伸缩的数据结构都只需要将自己的引用存储在栈上,因此他们也是可知的。
所谓的不定长类型是指在运行时才能知道大小的类型,比如下面这段代码。
let n = 用户输入的值
let arr: [i32; n];
上面的这段代码会报错,因为n只有在运行时才知道值,也就是说编译器无法确定在进入这个函数时需要开辟多大的空间。
类似的数据结构还有切片和str
。str
是字符串和字符串切片的底层数据结构。除此之外还有特征对象,他们也是在编译期无法确定大小的DST
而如果我们想要使用DST,就必须通过引用的方式间接去进行调用。引用中会存储指向数据所在的内存地址,以及长度等信息。
整型与枚举的转换
- 标题: Rust —— 高级篇(1)
- 作者: Zephyr
- 创建于 : 2023-05-06 21:42:45
- 更新于 : 2023-05-16 23:37:37
- 链接: https://faustpromaxpx.github.io/2023/05/06/rust-ad1/
- 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。