类型系统
基本类型
- 基本类型
- 复合类型
- 作用域
- 返回值
- Option<T>
- Result<T,E>
编程语言中不同的类型本质上是内存占用空间和编码方式的不同,Rust也不例外。
Rust中绝大部分类型都是在编译期可确定大小的类型(Sized Type),如u8,f64等
Rust也有少量的动态大小的类型(Dynamic Sized Type,DST),如str
对于动态类型,Rust提供了引用方式来解决,引用类型存在栈空间,具体内容存在堆空间。
例如:
包含了动态大小类型地址信息和携带了长度信息的指针,叫作胖指针(Fat Pointer),&str就是胖指针。
有如下代码
fn test(mut arr:[u32]){
arr[1] = 5; arr[2] = 1; } fn main(){
let arr:[u32]= [1,2,4,5,6]; test(arr); }
以上代码编译会出错
表示期望切片类型,找到的是数组类型,做如下修改可通过编译
fn test(mut arr:[u32;5]){
arr[1] = 5; arr[2] = 1; } fn main(){
let arr:[u32;5]= [1,2,4,5,6]; test(arr); println!("{:?}",arr);// 1 2 4 5 6 }
这样类型就匹配了,但是arr结果并没有被改变,其实是因为传入的参数通过shadow机制在栈空间创建了一个新的arr,所以并不会改变原来的值。可以类比C语言的值传递swap(a,b),结果并不会交换a,b。
c语言解决方法是传入a,b的指针,Rust也一样,传入可变引用,即胖指针即可
fn reverse(arr:&mut [u32]){
arr[0] = 3; arr[1] = 2; arr[2] = 1; println!("len : {}",arr.len()); } fn main(){
let mut arr= [1,2,3];//[u32;3] reverse(&mut arr);//传入的引用包含了大小,编译器解决 println!("{:?}",arr); }
可以查看占用内容大小
fn main(){
println!("{}",std::mem::size_of::<&mut [u32]>());//16 println!("{}",std::mem::size_of::<& [u32;3]>());//8 }
胖指针由来。
ZST类型
除了可确定大小类型和DST类型,Rust还支持零大小类型(Zero Sized Type,ZST),比如单元类型和单元结构体,大小都是零。
以下都为ZST类型
enum void {
} struct foo; struct Baz{
f:foo, q:(), buf:[u16;0], em:void, } fn main(){
println!("{}",std::mem::size_of::<void>());//0 println!("{}",std::mem::size_of::<foo>());//0 println!("{}",std::mem::size_of::<Baz>());//0 println!("{}",std::mem::size_of::<[();32]>());//0 }
底类型
如never类
zst是空,底类型是无。
- 没有值。
- 是其他任意类型的子类型
- 用!表示
以下返回底类型:
- 发散函数(Diverging Function)
- continue和break关键字
- loop循环
- 空枚举,比如enum Void{}
以下代码正常执行
fn foo()->!{
loop {
} } fn main(){
let a = if false{
foo() }else{
100 }; print!("{}",a); }
因为!是所有类型的子类型。
以下不可
enum Void {
} fn main(){
let res:Result<u32,Void> = Ok(0); let Ok(num) = res; }
Rust中使用Result类型来进行错误处理,强制开发者处理Ok和Err两种情况,但是有时可能永远没有Err,这时使用enum Void{}就可以避免处理Err的情况。
但是可惜的是,当前版本的Rust还不支持上面的语法,编译会报错。不过Rust团队还在持续完善中,在不久的将来Rust就会支持此用法。
类型推断
Rust支持类型推断,但其功能并不像Haskell那样强大,Rust只能在局部范围内进行类型推导
fn add(a:u32,b:u32)->u32{
a+b } fn main(){
let a = 1; let b = 2; add(a, b); }
Turbofish操作符
fn main(){
let x = "1"; print!("{:?}",x.parse::<i32>().unwrap()); }
::<>
就是Turbofish操作符
不完善
fn main(){
let x = 0; let flag = x.is_positive(); }
can’t call method is_positive
on ambiguous numeric type {integer}
泛型
泛型(Generic)是一种参数化多态。使用泛型可以编写更为抽象的代码,减少工作量。简单来说,泛型就是把一个泛化的类型作为参数,单个类型就可以抽象化为一簇类型。
即把类型作为参数。
泛型可以用在,函数,结构体,使用泛型必须声明<T>
struct Point<T>{
x:T, y:T, } fn get_x<T>(x:T) -> T{
x }
为泛型结构体实现方法impl<T>
struct Message<T>{
content:T, } impl <T> Message<T> {
fn new(content:T)->Self{
Message {
content } } }
Rust中的泛型属于静多态,它是一种编译期多态。在编译期,不管是泛型枚举,还是泛型函数和泛型结构体,都会被单态化
(Monomorphization)。单态化是编译器进行静态分发的一种策略
泛型及单态化是Rust的最重要的两个功能。单态化静态分发的好处是性能好,没有运行时开销;缺点是容易造成编译后生成的二进制文件膨胀。这个缺点并不影响使用Rust编程。
返回值推导
struct Foo(i32); struct Bar(i32,i32); trait Instance {
fn new(i:i32) -> Self; } impl Instance for Foo {
fn new(i:i32) -> Foo {
Foo(i) } } impl Instance for Bar{
fn new(i:i32)->Self{
//Self指结构体本身 Bar(i,i+10) } } fn foobar<T:Instance>(i:i32) -> T{
//表明T是实现了Instance trait的结构体 T::new(i) } fn main(){
let f:Foo = foobar(10); let b:Bar = foobar(20); }
深入Trait
可以说trait是Rust的灵魂。Rust中所有的抽象,比如接口抽象、OOP范式抽象、函数式范式抽象等,均基于trait来完成。同时,trait也保证了这些抽象几乎都是运行时零开销的。
从类型系统的角度来说,trait是Rust对Ad-hoc多态的支持。从语义上来说,trait是在行为上对类型的约束,这种约束可以让trait有如下4种用法:
- 接口抽象。对类型行为的统一约束。
- 泛型约束。泛型的行为被trait限定在更有限的范围内。
- 抽象类型。在运行时作为一种间接的抽象类型去使用,动态地分发给具体的类型。
- 标签trait。对类型的约束,直接作为标签使用。
接口抽象
特点:
- 接口中定义方法,并支持默认实现。
- 接口中不能实现另一个接口,但接口间可以继承。
- 同一个接口可以被多个类型实现,但不能被同一个类型实现。
- 使用impl关键字为类型实现接口方法。
- 使用trait关键字定义接口。
trait T_A{
fn say(msg:&str)->String; } struct A; struct B; enum C{
} impl T_A for A{
fn say(msg:&str)-> String{
let t = "A".to_string(); t + msg } } impl T_A for B{
fn say(msg:&str)->String{
let t = "B".to_string(); t + msg } } impl T_A for C{
fn say(msg:&str)->String{
let t = "C".to_string(); t + msg } } fn main(){
}
加减乘除等也是一种trait
trait Add<RHS,Output>{
fn my_add(self,rhs:RHS) -> Output; } impl Add<i32,i32> for i32 {
fn my_add(self,rhs:i32)->i32{
self + rhs } } impl Add<u32,i32> for u32{
fn my_add(self,rhs:u32) -> i32{
(self + rhs) as i32 } } fn main(){
let (a,b,c,d) = (1i32,2i32,3u32,4u32); let x:i32 = a.my_add(b); let y:i32 = c.my_add(d); assert_eq!(x,3i32); assert_eq!(y,7i32); }
RHS是+
的右侧值类型,Output是输出类型。
对于Add,加法的输入与输出值应该为同一类型,所以Add<RHS,Output>
的output有点多余,于是有以下写法
pub trait Add<RHS =Self >{
// 指定RHS的默认值为Self type Output; fn add(self,rhs:RHS) -> Self::Output; }
type Output 叫关联类型,Self是每个trait都带有的隐式类型参数。
impl Add<&str> for String{
type Output = String; fn add(mut self,other:&str) -> String{
self.push_str(other); self } }
trait一致性
可以实现操作符重载。
如想要实现u32+u64
use std::ops::Add; impl Add<u64> for u32{
type Output = u64; fn add(self,other:u64) -> Self::Output{
(self as u64) + other } } fn main(){
let a = 1u32; let b = 2u64; println!("{}",a+b); }
编译报错。
这是因为Rust遵循一条重要的规则:孤儿规则(Orphan Rule)。孤儿规则规定,如果要实现某个trait,那么该trait和要实现该trait的那个类型至少有一个要在当前crate中定义
Add trait和u32、u64都不是在当前crate中定义的,而是定义于标准库中的。如果没有孤儿规则的限制,标准库中u32类型的加法行为就会被破坏性地改写,导致所有使用u32类型的crate可能产生难以预料的Bug。
解决如下,讲Add trait 定义在当前crate就可以了,当然可以不一定叫Add,和add()
trait Add<RHS =Self >{
type Output; fn add(self,rhs:RHS) -> Self::Output; } impl Add<u64> for u32{
type Output = u64; fn add(self,other:u64) -> Self::Output{
(self as u64) + other } } fn main(){
let a = 1u32; let b = 2u64; println!("{}",a.add(b)); }
对其他类型实现trait
use std::ops::Add; #[derive(Debug)] struct Point{
x:i32, y:i32, } impl Add for Point {
type Output = Self; fn add(self,other:Point) -> Self::Output{
Point{
x:self.x+other.x, y:self.y+other.y, } } } fn main(){
let p1 = Point{
x:1, y:2, }; let p2 = Point{
x:2, y:3, }; println!("{:?}",p1+p2); }
trait 继承
Rust不支持传统面向对象的继承,但是支持trait继承。子trait可以继承父trait中定义或实现的方法。在日常编程中,trait中定义的一些行为可能会有重复的情况,使用trait继承可以简化编程,方便组合,让代码更加优美。
例如web中常用的分页
trait Page{
fn set_page(&self, p:i32){
println!("Page Default 1"); } } trait PerPage{
fn set_perpage(&self, num:i32){
println!("Per Page Default 10"); } } struct MyPaginate{
page:i32} impl Page for MyPaginate {
} impl PerPage for MyPaginate {
} fn main(){
let my_paginate = MyPaginate{
page:1}; my_paginate.set_page(2); my_paginate.set_perpage(100); }
给MyPaginate
实现了两个trait。
如果要再实现一个跳转功能,可以使用继承
trait Paginate: Page + PerPage{
//继承写法 :trait fn set_skip_page(&self,num:i32){
println!("skip to page {}",num); } } impl <T:Page+PerPage> Paginate for T {
}//T为泛型,类型为实现了Page+PerPage的类型,整句话表示为实现了T的类型实现Paginate fn main(){
let my_paginate = MyPaginate{
page:1}; my_paginate.set_page(2); my_paginate.set_perpage(100); my_paginate.set_skip_page(12);//skip to page 12 }
优点是添加了新功能再不影响之前功能的情况下。
trait约束
很多情况下,一个行为并不是为所有类型实现的。比如:
fn sum<T>(a:T,b:T)->T{
//编译不通过 a+b }
整型相加可以,字符串相加可以,但是整型与bool类型,就不行。
可以限制T为可加类型
use std::ops::Add; fn sum<T:Add<T,Output=T>>(a:T,b:T)->T{
//限制T的类型为实现了Add的类型 a+b } fn main(){
let a = 1; let b = 2; let c = sum(a, b); println!("{}",c); }
使用trait对泛型进行约束,叫作trait限定(trait Bound)。格式如下
fn generic<T: MyTrait + MyOtherTrait + SomeStandTrait>(t:T){
}
泛型限定是许多语言都有概念,是Structural Typing的变种,Rust中的trait限定也是Structural Typing的一种实现。
也可以从数学的角度理解trait限定,例如
trait Paginate: Page + PerPage{}
注意:
如果有trait A,B,C,A,B,C中的方法不能同名,
不能覆盖,
C:A+B,为某类型实现C必须要实现A和B。否则报错。
trait A {
fn getA(&self,i:i32){
println!("A : {}",i); } } trait B {
fn getB(&self,i:i32){
println!("B : {}",i); } } trait C:A+B {
fn getC(&self,i:i32){
println!("C : {}",i); } } impl A for Test {
} impl B for Test {
} // impl C for Test {} impl <T> C for T where T:A+B {
} struct Test{
a:i32, } fn main(){
let test = Test{
a:1}; test.getB(2); test.getA(1); test.getC(3); }
抽象类型
相对于具体类型而言,抽象类型无法直接实例化,它的每个实例都是具体类型的实例。
对于抽象类型而言,编译器可能无法确定其确切的功能和所占的空间大小。所以Rust目前有两种方法来处理抽象类型:trait对象和impl Trait。
trait对象
use std::fmt::Debug; #[derive(Debug)] struct Foo; #[derive(Debug)] struct Fun; trait Bar{
fn baz(&self); } impl Bar for Foo{
fn baz(&self){
println!("{:#?}",self); } } impl Bar for Fun{
fn baz(&self){
println!("{:#?}",self); } } fn static_dispatch<T>(t:&T) where T:Bar{
t.baz(); } fn dynamic_dispatch(t:&dyn Bar){
//动态分发 t.baz(); } fn main(){
let foo = Foo; let fun = Fun; static_dispatch(&foo); static_dispatch(&fun); dynamic_dispatch(&foo); dynamic_dispatch(&fun); }
动态分发
trait本身也是一种类型,但它的类型大小在编译期是无法确定的,所以trait对象必须使用指针。可以利用引用操作符&或 Box<T>来制造一个 trait 对象。trait 对象等价代码如下结构体
pub struct TraitObject{
pub data: *mut (), pub vtable: *mut (), }
TraitObject 在栈区,数据指针指向堆区数据部分,vtable 名称来自C++,保护了析构函数,大小,函数等信息。
在编译器TraitObject不知道调用哪个方法,但是指针的大小确定,在运行器通过trait_object.method()可以知道函数的指针,然后进行调用。
类比java继承父类可以调用子类的实现。
并不是每个trait都可以作为trait对象被使用,这依旧和类型大小是否确定有关系。每个trait都包含一个隐式的类型参数Self,代表实现该trait的类型。Self默认有一个隐式的trait限定?Sized,形如<Self:?Sized>,?Sized trait 包括了所有的动态大小类型和所有可确定大小的类型。Rust 中大部分类型都默认是可确定大小的类型,也就是<T:Sized>,这也是泛型代码可以正常编译的原因。
必须同时满足以下两条规则的trait才可以作为trait对象使用
简单来说,要将trait作为trait对象就不加限制,否则就加上Sized
trait A:Sized{
//code }
安全的trait对象实例
trait Bar{
fn bax(self,x:u32); fn baz(&mut self); }
不安全的trait对象实例
trait Foo{
fn bad<T>(&self,x:T); fn new() ->Self;//Self是unsized }
- 拆分
trait Foo{
fn bad<T>(&self,x:T); } trait Foo:Bar{
fn new() ->Self;//Self是Bar继承的 }
- 使用where限定
trait Foo{
fn bad<T>(&self,x:T); fn new()->Self where Self:Sized; }
impl Trait
在Rust 2018版本中,引入了可以静态分发的抽象类型impl Trait。如果说trait对象是装箱抽象类型(Boxed Abstract Type)的话,那么impl Trait就是拆箱抽象类型(Unboxed Abstract Type)。“装箱”和“拆箱”是业界的抽象俗语,其中“装箱”代表将值托管到堆内存,而“拆箱”则是在栈内存中生成新的值,目前impl Trait只可以在输入的参数和返回值这两个位置使用,在不远的将来,还会拓展到其他位置,比如let定义、关联类型等.
例如
use std::fmt::Debug; pub trait Fly {
fn fly(&self) ->bool; } #[derive(Debug)] struct Duck; #[derive(Debug)] struct Pig; impl Fly for Duck {
fn fly(&self)->bool{
return true; } } impl Fly for Pig{
fn fly(&self) -> bool{
return false; } } fn fly_static(s: impl Fly+Debug) ->bool{
s.fly() } fn can_fly(s:impl Fly+Debug) -> impl Fly{
if s.fly(){
println!("{:?} can fly",s); }else {
println!("{:?} can't fly",s); } s } fn main() {
let pig = Pig; assert_eq!(fly_static(pig),false); let pig = Pig; let pig = can_fly(pig); let duck = Duck; assert_eq!(fly_static(duck),true); let duck = Duck; let duck = can_fly(duck); }
将impl Trait语法用于参数位置的时候,等价于使用trait限定的泛型。
let a: impl Trait 是不允许的。
另外,impl Trait只能用于为单个参数指定抽象类型,如果对多个参数使用implTrait语法,编译器将报错
use std::ops::Add; fn sum<T>(a:impl Add<Output = T>,b:impl Add<Output = T>)->T{
a + b }
impl 与 dyn是对应的,一个静态,一个动态。
标签trait
trait 这种对行为约束的特性也非常适合作为类型的标签。
Rust一共提供了5个重要的标签trait,都被定义在标准库std::marker模块中,分别为:
· Sized trait,用来标识编译期可确定大小的类型。
· Unsize trait,目前该trait为实验特性,用于标识动态大小类型(DST)。
· Copy trait,用来标识可以按位复制其值的类型。
· Send trait,用来标识可以跨线程安全通信的类型。
· Sync trait,用来标识可以在线程间安全共享引用的类型。
#[stable(feature = "rust1", since = "1.0.0")] #[lang = "sized"] pub trait Sized {
// Empty. }
这里真正起“打标签”作用的是属性#[lang="sized"],该属性lang表示Sized trait供Rust语言本身使用,声明为"sized",称为语言项(Lang Item),这样编译器就知道Sized trait如何定义了。
默认为Sized,否则
struct Bar<T:?Sized>(T);
Copy trait 继承Clone
#[lang = "copy"] pub trait Copy: Clone {
// Empty. }
#[lang = "clone"] pub trait Clone: Sized {
fn clone(&self) -> Self; #[inline] #[stable(feature = "rust1", since = "1.0.0")] fn clone_from(&mut self, source: &Self) {
*self = source.clone() } }
Rust为很多基本数据类型实现了Copy trait,比如常用的数字类型、字符(Char)、布尔类型、单元值、不可变引用等
Rust提供了Send和Sync两个标签trait,它们是Rust无数据竞争并发的基石。
类型转换
在编程语言中,类型转换分为隐式类型转换(Implicit Type Conversion)和显式类型转换(Explicit Type Conversion)。隐式类型转换是由编译器或解释器来完成的,开发者并未参与,所以又称之为强制类型转换(Type Coercion)。显式类型转换是由开发者指定的,就是一般意义上的类型转换(Type Cast)。
Deref解引用
Rust中的隐式类型转换基本上只有自动解引用。自动解引用的目的主要是方便开发者使用智能指针。Rust 中提供的 Box<T>、Rc<T>和 String 等类型,实际上是一种智能指针。
引用符&,解引用*
解引用可以自己实现,只要实现了Deref Trait 即可实现类型转换。
Deref的定义,
pub trait Deref {
type Target: ?Sized; fn deref(&self) -> &Self::Target; }
DerefMut,返回的是可变应用。
解引用例子,字符串连接
fn main(){
let s1 = "12".to_string(); let s2 = "34".to_string(); let s3 = s1 + &s2; println!("{}",s3); }
s1
,s2
都是String
类型,&s2应该是&String
,期望的是&str
,也就是说应该会报错,可是以上代码正常执行,就是因为String
实现了解引用,
impl Deref for String {
type Target = str; fn deref(&self) -> &str{
unsafe{
str::from_utf8_unchecked(&self.vec)} } }
标准库中常用的其他类型都实现了Deref,比如Vec<T>、Box<T>、Rc<T>、Arc<T>等。实现Deref的目的只有一个,就是简化编程。
解引用实例
use std::rc::Rc; fn test_vec(s:&[i32]){
println!("{:?}",s); } fn test_rc(){
let x = Rc::new("hello"); println!("{}",x); } fn main(){
let v = vec![1,2,3]; test_vec(&v); test_rc(); }
手动解引用
当某类型和其解引用目标类型中包含了相同的方法时,编译器就不知道该用哪一个了。此时就需要手动解引用,
fn test_deref(){
let x = Rc::new("hello"); let y = x.clone();//&Rc<&str> let z = (*x).clone();//&str }
因为Rc和str都实现了clone,所以不能自动解引用。
match需要手动解引用
fn test_str(){
let s = "1234".to_string(); match &s[..]{
"1234" => println!("hello"), _ => {
} } }
有如下几种方式:
- match x.deref()
- match x.as_ref()
- match x.borrow()
- match &*x
- match &x[…]
as 操作
as 操作符最常用的场景就是转换 Rust 中的基本数据类型。需要注意的是,as 关键字不支持重载。
fn main(){
let a = 1u32; let b = a as u64; let c = 3i64; let d = c as u32; print!("a{},b{},c{},d{}",a,b,c,d); }
长类型转短类型截断。
fn main(){
let a = u32::MAX; let b = a as u16; println!("a:{},b:{}",a,b);//a:,b:65535 }
as 还可以消除语法歧义。
#[derive(Debug)] struct C; trait A {
fn test(&self); } trait B {
fn test(&self); } impl A for C {
fn test(&self){
println!("A:{:?}",self); } } impl B for C {
fn test(&self){
println!("B:{:?}",self); } } fn main(){
let c = C; A::test(&c); B::test(&c); <C as A>::test(&c); <C as B>::test(&c); }
类型和子类型相互转换
as转换还可以用于类型和子类型之间的转换。Rust中没有标准定义中的子类型,比如结构体继承之类,但是生命周期标记可看作子类型。比如&'static str类型是&'a str类型的子类型,因为二者的生命周期标记不同,'a 和'static 都是生命周期标记,其中'a 是泛型标记,是&str的通用形式,而'static则是特指静态生命周期的&str字符串。
fn main(){
let a:&'static str = "hello"; let b:& str = a as &str; let c:&'static str = b as &'static str; }
From和Into
From和Into是定义于std::convert模块中的两个trait。它们定义了from和into两个方法,这两个方法互为反操作。
定义
pub trait From<T>: Sized {
/// Performs the conversion. #[lang = "from"] fn from(_: T) -> Self; }
pub trait Into<T>: Sized {
/// Performs the conversion. #[must_use] fn into(self) -> T; }
关于Into有一条默认的规则:如果类型U实现了From<T>,则T类型实例调用into方法就可以转换为类型U
因为Rust实现了
impl<T,U> Into<U> for T where U:From<T>
当前trait系统的不足
· 孤儿规则的局限性。
· 代码复用的效率不高。
· 抽象表达能力有待改进。
版权声明:
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若内容造成侵权、违法违规、事实不符,请将相关资料发送至xkadmin@xkablog.com进行投诉反馈,一经查实,立即处理!
转载请注明出处,原文链接:https://www.xkablog.com/rustbc/1695.html