AIOps 一场颠覆传统运维的盛筵
1329
2022-11-09
Rust类型系统学习笔记
本文为《Rust编程之道[1]》学习笔记。友情提示:文中代码可以使用Rust Playground[2]在线运行。
Rust是一门显式静态强类型的类型安全语言。
•静态表明它在编译期进行类型检查•强类型表明它不允许类型自动隐式转换,不同类型无法进行计算•类型安全表明它保证运行时的内存安全•显示是因为它的类型推导在某些时候需要显示指定
类型大小
动态大小类型
Rust中大部分类型都可以在编译期确定大小。
对于无法确定大小的类型(即DST, Dynamic Sized Type),只能在运行时分配,使用指针关联。
这样的指针存储了内存地址和长度信息,被称为胖指针。
例如&str字符串类型,它是一个引用类型,我们可以获取它指向的地址和长度信息:
fn main() { let str = "yuchanns"; let ptr = str.as_ptr(); let len = str.len(); println!("{:p}", ptr); println!("{:?}", len);}// 0x558706bd2000// 8
由于存储内存地址和长度的变量类型都为usize(在64位系统上为8字节),所以胖指针可在编译期确定为16字节,分配到栈上。
可以通过下面的代码清单查看胖指针&str的大小:
fn main() { println!("{}", std::mem::size_of::<&str>());}// 16
需要注意的是,并非所有指针都是胖指针。如果引用的类型大小可以确定,相应的指针大小仅为8字节:
fn main() { // 长度不确定,需要携带长度信息,是胖指针 println!("{}", std::mem::size_of::<&[u32]>()); // 长度确定,是普通指针 println!("{}", std::mem::size_of::<&[u32; 5]>());}// 16// 8
零大小类型
单元结构体(struct Foo)、单元类型(())和空枚举(enum Void {})在运行时不占用内存空间(ZST,Zero Sized Type)。
在只需要迭代次数的场合中利用Vec<()>可以提高性能。
底类型
!表示无,也可以等同于任何类型。
由于在Rust的流程控制中if属于表达式,两个分支需要返回相同类型的值,在无法返回的情况下可以使用底类型(Bottom Type):
// 该方法死循环,无法返回需要的String类型fn foo() -> ! { loop {println!("loop");}}fn main() { let msg = if false { foo(); } else { format!("failed") }; println!("{}", msg);}
类型推导
Rust部分情况下可以自动推导类型。
在无法自动推断时需要显式指定类型,帮助编译器确定推导的类型。
fn main() { let x = "1"; assert_eq!(x.parse::
上面的代码清单中,parse::
泛型
泛型可以节省人编码过程的工作量,提高语言抽象能力。
例如实现两数相加,没有泛型的情况下,我们需要为每个类型实现一遍加法,如fn add_i32(i32, i32) -> i32、fn add_i64(i64, i64) -> i64、fn add_u32(u32, u32) -> u32和fn add_u64(u64, u64) -> u64等。
而有了泛型的帮助,这些类型共用一个fn add
其中类型T在编译时根据传入的参数类型确定。
如果有多种类型分别调用了该函数,则会在编译期替我们为每个类型实现了一遍加法。
这在Rust中被称为单态化(Monomorphization) ,是零成本抽象的一种,缺点是生成的文件体积会变大。
Trait
Trait为Rust提供了零成本抽象能力。
接口抽象和泛型约束
人们常常在工程中提倡面向接口编程,即定义和实现分离,实现理想的解耦状态。
在Rust中可以使用trait定义接口,然后用impl for进行实现:
trait Shape { fn desc(&self) -> String;}struct Circle {}struct Triangle {}impl Shape for Circle { fn desc(&self) -> String { "this is a circle".to_string() }}impl Shape for Triangle { fn desc(&self) -> String { "this is a triangle".to_string() }}fn get_desc_from_shape
同时可以看到,函数get_desc_from_shape是通过
这一机制实现了Ad-hoc多态,又称为函数重载,同样在编译期展开,是零成本抽象。
此外,Trait还通过&dyn提供了非零成本的运行时抽象:
fn get_desc_from_shape_dyn(shape: &dyn Shape) { println!("describe: {:?}", shape.desc());}fn main() { let c = Circle{}; get_desc_from_shape_dyn(&c);}
trait还可以预先实现方法,节省具体类型的实现工作:
trait ShapePrinter { fn print_shape(&self) { println!("this is a type implemented ShapePrinter"); }}impl ShapePrinter for Circle {}fn print_shape
Trait继承扩展
trait具有继承扩展能力,结合泛型约束可以实现。
例如,我们要为上面小节中所有实现了Shape的结构体扩展ShapePrinter,其实不需要为每个结构体重新实现一遍:
trait ShapePrinter: Shape { fn print_shape(&self) { println!("this is a type implemented ShapePrinter"); }}impl
通过泛型约束为所有实现了Shape的泛型实现ShapePrinter,只需要写一次,就可以全部实现。
静态分发
如果不结合泛型约束,零成本抽象函数可以通过impl指定静态分发:
fn get_desc_from_shape_static(shape: impl Shape) { println!("describe: {:?}", shape.desc());}fn main() { let c = Circle{}; get_desc_from_shape_static(c);}
该关键字也可以在返回值中使用,如fn create_shape() -> impl Shape。
目前只能在入参和出参使用。
孤儿规则
Rust规定,如果要实现某个trait,那么该trait和要实现的类型至少有一个在当前的crate中定义,即Orphan Rule。
该规则保护了标准库和第三方库中定义的类型,避免使用者随意覆盖实现,引发难以预料的Bugs。
关联类型
Associate Types使用type关键字在trait中定义一个泛型,具体类型实现时指定。
关于关联类型的作用,我是参考了Associated Types[3]这篇文章才更好地理解了。
例如下列结构体:
struct Test { name: String,}
我们要定义一个接口,它含有一个test方法,其返回值根据具体实现决定。
使用普通泛型的代码思路:
trait TestB
这样在调用时显得怪异而冗长,还会增加理解难度:
而使用关联类型的代码:
trait TestA
在实现中直接定义了泛型类型,调用时简洁了很多,也体现了高内聚的工程性:
内置Trait
Rust在标准库中std::marker内置了5个重要的trait,它们分别是:
•Sized trait:实现了该trait的类型编译期可以确定大小•Unsize trait:实现了该trait的类型大小是动态的•Copy trait:实现了该trait的类型可以安全复制•Send trait:实现了该trait的类型可以安全地跨线程传递所有权•Sync trait:实现了该trait的类型可以安全地跨线程共享引用
其中Send和Sync保证了Rust在编译期就能排除数据竞争。
内置的trait当然不仅仅是这5个,此处暂且不提。对于内置的trait,可以通过#[derive]派生属性让编译器替用户实现:
#[derive(Debug)]struct Test {}fn main() { println!("{:?}", Test{});}
例如上面的代码清单中,我们要求编译器为结构体Test派生Debug,那么它就可以被以println!("{:?}", Test{})的形式打印。
类型转换
基本类型转换
Rust可以通过as关键字为基本类型进行转换:
fn main() { let a = 1u32; let b = a as u64;}
需要注意,从长类型转换为短类型,存在被截断的可能。
无歧义完全限定语法
在上面的Trait小节中,细心的读者可能早已发现,一个结构体在实现多个trait时,完全有可能存在同名的方法,调用可能存在歧义。
这时候结合as关键字可以避免歧义:
struct S(i32);trait A { fn test(&self, i: i32) { println!("from A: {:?}", i); }}trait B { fn test(&self, i: i32) { println!("from B: {:?}", i); }}impl A for S {}impl B for S {}fn main() { let s = S(1); ::test(&s, 1); ::test(&s, 1);}// from A: 1// from B: 1
其他转换
结合内置的各种trait,Rust还实现了诸如自动解引用等少数隐式转换.
由于目前学习掌握的信息不足,暂不深入了解,知道即可。
引用链接
[1] Rust编程之道: https://book.douban.com/subject/30418895/[2] Rust Playground: https://yuchanns.xyz/r/playground/playground-rust[3] Associated Types: http://web.mit.edu/rust-lang_v1.25/arch/amd64_ubuntu1404/share/doc/rust/html/book/first-edition/associated-types.html
发表评论
暂时没有评论,来抢沙发吧~