这里有一段 Rust 的函数签名:
上一篇我们讲了生命周期相关的东西,这次继续。
Box
为了实现面向对象的写法, Rust 里有 trait 语法,就和其他语言类似的 Interface。
面向对象
传统的面向对象语言,以 C# 为例子,会提供抽象类,接口,类可以继承父类,抽象类,实现接口来达到面向对象。但是人们这种方式渐渐的不能够满足编程要求了,这种面向对象的方式太沉重。
举个例子,我抽象了一个动物类 Animal,它有一方法 Run,表示所有继承这个抽象类的都是动物,都能够跑。
(Abstract)Animal - Run
Human -> Animal
随后我发现需要编写一个海豚类,它是动物,需要继承 Animal 抽象类,但是问题是海豚不会跑。无奈之下我又抽象了一个海洋动物类 SeaAnimal,有方法 Swim,表示海洋动物,可以游泳;和一个陆地动物 LandAnimal,有方法 Run;将原本 Animal 里的 Run 方法去除。海豚继承 SeaAnimal,人类继承 LandAnimal。
你能够意识到当功能越多,类越多的时候会修改越来越多的代码,而一旦父级的抽象类受到修改,将影响到每一个继承它的子类。
所以化繁为简,近年来新的语言不再提供抽象类,继承的语法,像 Rust 的 trait,Dart 的 mixin,go 等。取而代之的是实现接口,如果对象需要跑,就实现 Run 接口;如果海豚需要游泳,就实现 Swim 接口;如果人类需要游泳,就实现 Swim 接口。
且可以将接口作为参数,只要实现了这个接口的对象都能够接收,这就是多态。
Rust 里的 trait 就用于这里。
多态
但是 Rust 里多态的用法没有其他语言一样透明。
以 TS 为例,接口 IRun,类 Human 实现了 IRun,有一个方法入参类型为 IRun,
function run(input: IRun) {}
let human = new Human();
run(human);
human
可以直接传递给 run
,很直观。但是在 Rust 里不能这样写,因为一个 trait,无法在编译时期就知道大小有多大,如果需要在函数里传递 trait,需要使用 impl:
trait Run {}
struct Human;
impl Run for Human {}
fn run(input: impl Run) {}
let human = Human;
run(human);
必须在入参里使用 impl
标注是一个 trait,其实这样也能接收对吧。但是如果你要在 struct
里的一个字段类型是 trait,就不能这么写。
struct Human {
va: impl Run // compile error
}
因为编译器无法推断出大小。你得用泛型静态分配或者是使用 Box
动态分配。
动态分配
Box
可以将一个对象放在堆上,并使用一个指针指向它。如果使用 dyn 关键字,后跟着一个 trait,那么这个 Box
就能够持有任何实现了这个 trait 的对象。
struct Human {
va: Box<dyn Run>
}
let human = Human {
va: Box::new(RunFast)
}
就能够在 struct 持有一个 trait 的字段,以实现多态。Box
的方式也能够在函数入参里使用。但 Box
的动态分配并不能动态分配所有 trait,在下面会讲到。
Async
Rust 的 async
、await
语法其实是 Future
的语法糖。图片里的函数是 async_trait
生成的 Future
函数,你能看到它的返回值其实是 Pin<Box<Future>>
而不是 Future
,这是因为 Rust 并没有对异步做到很舒服的支持。
在 1.75 版本以前 Rust 还不支持在 trait 中定义异步方法。
// 1.75 版本以前无效
trait Run {
async fn run();
}
但是我们仍然有这个需求,所以需要使用 async_trait
crate 来支持 trait 里的 async 方法,具体看 async_trait
包。
而 async_trait
所做的就是将返回值变成 Pin<Box<dyn Future<...>>>
。Box 的作用我们前面讲了,配合 dyn
关键字可以实现多态。而 Pin
是什么。
被 Pin
包装的对象不能移动在内存中的位置。这是因为异步方法可能会被移动带其他线程中,而改变位置,此时如果有一个指向原来位置的指针,会因为对象被 runtime 移动了而变成一个指向错误位置的指针。所以需要使用 Pin
使它不能被移动。
现在的版本都是 1.85 了,早就支持 trait 里的 async 语法了,为什么还要使用 async_trait
呢?这是因为定义了异步方法的 trait 无法在 Box 里动态分配。
trait Run {
async fn run(&self);
}
struct Human {
va: Box<dyn Run>
}
编译器会抱怨
the trait `Run` cannot be made into an object
consider moving `run` to another trait
Run 无法转变成一个对象,很迷惑的错误信息。让我们来脱糖一下这个 async 方法,会变成:
fn run(&self) -> impl Future<Output = ()>;
脱糖后的返回值是一个 trait,编译器无法在编译阶段就知道其大小,无法编译(你 TM!!!)。所以无法被 Box 持有。
而 async_trait
做的的就是将返回值的 Future 包装进 Box 里,让返回值变成一个在编译阶段就可知的指向堆的指针,当然还有 Pin。
这就是为什么图片里的返回值是 Pin<Box<dyn Future<Output = ...>>>
。
Marker
在返回值的 Box 里的 Future 还跟着一个 Send
约束,这个 Send 是一个 marker,被 Send 标记的对象表示可以被移动到其他线程。因为异步对象可能会被移动到其他线程,如果 Future 没有 Send 标记的话是不能移动到其他线程的,所以需要 Send
标记。
Rust 里所有 primitive 类型 都是 Send 的,如果一个 struct 里所有字段都是 Send 的,那么这个 struct 也自动变成 Send 的。
随后还跟着一个 'async_traic
生命周期注解,在上一篇我们见过生命周期,这个'async_traic
标记在 Box 里,表示 Box 持有的借用生命周期不能小于 'async_traic
,或者持有所有权。
到此,就构成了图片里的函数签名。
这篇关于Rust编程语言中高级特性的文章内容丰富且具有深度,涵盖了面向对象编程、多态性以及异步编程等关键概念。以下是对该文的详细分析及评论:
结构清晰,逻辑连贯:作者从传统OOP的问题入手,引出Rust中的trait机制,并逐步深入到Box、async_trait以及Future的使用。这一结构使读者能够循序渐进地理解复杂的概念。
基础概念讲解到位:对于Rust的新手而言,文章对trait的定义及其在多态性中的作用进行了清晰的阐述。通过示例代码展示了如何利用Box<dyn Trait>实现动态分配,这使得抽象概念更具具体性。
深入浅出的技术细节:在解释Pin和Future时,作者使用了简明易懂的语言,避免了过于晦涩的术语,使读者能够理解异步编程中的内存不变性需求。同时,对async_trait crate的作用进行了合理分析,说明其在支持trait异步方法上的必要性。
示例代码生动实用:文中配套的代码片段有效地帮助读者理解理论知识,并能在实际项目中应用这些概念。例如,通过Box<dyn Run>的使用展示了如何构建多态结构,这对学习者尤为重要。
Marker trait和生命周期解析透彻:Send标记和生命周期注释是Rust并发编程中的关键点。作者通过实际场景说明了它们的作用和意义,帮助读者理解这些抽象概念在实际中的应用。
对未来发展的展望:虽然文中没有明确提及,但随着Rust版本的更新,可以预见原生支持异步trait会逐渐增强。或许可以补充说明未来的可能改进,帮助读者更好地把握语言演进方向。
潜在改进空间:部分技术细节的解释略显简略,如Pin与Future脱糖后的具体实现。增加更多背景信息或对比分析,可以提升内容的深度。此外,添加实际应用场景如网络服务器或分布式系统中的例子,将进一步帮助读者理解这些概念的实用性。
总体而言,这篇文章在解释Rust高级特性方面做得非常出色,是学习该语言不可多得的优质资源。希望作者能继续深挖更多技术细节,为读者带来更丰富的知识点分享。