一切皆表达式,表达式皆有值,值皆有类型,所以一切皆类型。
包括:
编程语言中不同的类型本质上是内存占用空间和编码方式的不同,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
}
胖指针由来。
除了可确定大小类型和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是空,底类型是无。
以下返回底类型:
以下代码正常执行
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)是一种参数化多态。使用泛型可以编写更为抽象的代码,减少工作量。简单来说,泛型就是把一个泛化的类型作为参数,单个类型就可以抽象化为一簇类型。
即把类型作为参数。
泛型可以用在,函数,结构体,使用泛型必须声明
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是Rust的灵魂。Rust中所有的抽象,比如接口抽象、OOP范式抽象、函数式范式抽象等,均基于trait来完成。同时,trait也保证了这些抽象几乎都是运行时零开销的。
从类型系统的角度来说,trait是Rust对Ad-hoc多态的支持。从语义上来说,trait是在行为上对类型的约束,这种约束可以让trait有如下4种用法:
特点:
接口中定义方法,并支持默认实现。
接口中不能实现另一个接口,但接口间可以继承。
同一个接口可以被多个类型实现,但不能被同一个类型实现。
使用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
的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
}
优点是添加了新功能再不影响之前功能的情况下。
很多情况下,一个行为并不是为所有类型实现的。比如:
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的Self类型参数不能被限定为Sized。
· 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继承的
}
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 这种对行为约束的特性也非常适合作为类型的标签。
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)。
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"),
_ => {}
}
}
有如下几种方式:
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:4294967295,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是定义于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 Into for T where U:From
· 孤儿规则的局限性。
· 代码复用的效率不高。
· 抽象表达能力有待改进。