rust类型转换

2023-06-10 17:49:30 浏览数 (2)

类型转换

Rust 是类型安全的语言,因此在 Rust 中做类型转换不是一件简单的事。

as转换

Rust 不提供原生类型之间的隐式类型转换(coercion),但可以使用 as 关键字进行显式类型转换(casting)。例如:

代码语言:javascript复制
fn main() {
    cast();
}

// as 进行的显示类型强制转换
fn cast() {
    let n: u8 = 123;
    let m: i32 = n as i32;      // 将u8强制转换为i32类型

    println!("u8({})转i32({})", n, m);

    let a = 12345;         // 整型字面值常量是i32类型
    let b: i8 = a as i8;        // 能容纳更大数值的类型i32转容纳范围较小的i8,存在数据溢出的风险。

    println!("i32({})转i8({})", a, b);

    let c = '我';          // char类型
    let d = c as u32;

    println!("char({})转u32({})", c, d);       

    let f = 100u8;      
    let h = f as char;      // 只有u8才能转char(相当于只支持ASCII码的值和字符转换)

    println!("u8({})转char({})", f, h);

    let f = 123.123;    
    let q = f as i32;
    println!("f64({})转i32({})", f, q);


    let mut num = [1, 2, 3];
    let mut y = num.as_mut_ptr();       // 可变的指针类型
    let mut p = y as usize;                // 把指针转为usize类型
    p  = 4;                                       // 指针步进一步(i32类型占4字节,因此加4即可)
    y = p as *mut i32;                            // 将 usize转为指针
    
    unsafe {                                        
        println!("{}", *y);                       // 在unsafe模块中操作指针
    }
}

转换不具有传递性 就算 e as U1 as U2 是合法的,也不能说明 e as U2 是合法的(e 不能直接转换成 U2)。as转换基本上只用于数值类型之间的转换。而且需要注意,当你从可以容纳范围更大的数据类型向可以容纳范围较小的数据类型转换的时候会发生溢出,因此你要人为保证数据转换是正确的。

into和from

From 和 Into 两个 trait 是内部相关联的,实际上这是它们实现的一部分。如果我们能够从类型 B 得到类型 A,那么很容易相信我们也能够把类型 B 转换为类型 A。

From

From trait 允许一种类型定义 “怎么根据另一种类型生成自己”,因此它提供了一种类型转换的简单机制。在标准库中有无数 From 的实现,规定原生类型及其他常见类型的转换功能。

比如,可以很容易地把 str 转换成 String:

代码语言:javascript复制
let s = String::from("qwert");
println!("s={s}");

也可以为我们自己的类型定义转换机制:

代码语言:javascript复制
#[derive(Debug)]
#[allow(unused)]
struct Number {
    value: i32,
}

impl From<i32> for Number {
    fn from(item: i32) -> Self {
        Number { value: item }
    }
}

let num = Number::from(30);
println!("My number is {:?}", num);

Into

Into trait 就是把 From trait 倒过来而已。也就是说,如果你为你的类型实现了 From,那么同时你也就免费获得了 Into。

使用 Into trait 通常要求指明要转换到的类型,因为编译器大多数时候不能推断它。不过考虑到我们免费获得了 Into,这点代价不值一提。

代码语言:javascript复制
// 需要指明转换到的类型是Number
let a: Number = 1.into();
println!("My number is {:?}", a);

TryInto和TryFrom

类似于 From 和 Into,TryFrom 和 TryInto 是类型转换的通用 trait。不同于 From/Into 的是,TryFrom 和 TryInto trait 用于易出错的转换,也正因如此,其返回值是 Result 型。

代码语言:javascript复制
pub fn catsing(){
    let b = 123;
    let a: u8 = b.try_into().unwrap();          // try_into
    println!("{a}");

    let b:i32 = 12345;                // 有一点非常奇怪,那就是必须显示声明b的类型,否则编译器无法推断e的类型,导致错误。
    let _a: u8 = match b.try_into() {           // try_into
        Ok(v) => v,
        Err(e) => {
            println!("{:?}", e.to_string());
            0
        }
    };
}

如果我们需要自己实现try_from和try_into方法,那么需要实现TryFrom trait即可。例如:

代码语言:javascript复制
#[derive(Debug, PartialEq)]
struct EvenNumber(i32);

impl TryFrom<i32> for EvenNumber {
    type Error = ();
    fn try_from(value: i32) -> Result<Self, Self::Error> {
        if value % 2 == 0 {
            Ok(EvenNumber(value))
        } else {
            Err(())
        }
    }
}

// TryFrom
assert_eq!(EvenNumber::try_from(8), Ok(EvenNumber(8)));
assert_eq!(EvenNumber::try_from(5), Err(()));

// TryInto
let result: Result<EvenNumber, ()> = 8i32.try_into();
assert_eq!(result, Ok(EvenNumber(8)));
let result: Result<EvenNumber, ()> = 5i32.try_into();
assert_eq!(result, Err(()));

ToString 和 FromStr

上面的这些转换适大多数时候不适合字符串。它更需要ToString

Display

要把任何类型转换成 String,只需要实现那个类型的 ToString trait。然而不要直接这么做,您应该实现fmt::Display trait,它会自动提供 ToString,并且还可以用来打印类型。

代码语言:javascript复制
pub fn format_string() {
    use std::fmt;

    struct Circle {
        radius: i32
    }

    impl fmt::Display for Circle {      // 为 Circle 实现 Display trait
        fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
            write!(f, "Circle of radius {}", self.radius)
        }
    }

    let circle = Circle { radius: 6 };
    println!("{}", circle.to_string());     // to_string是由Display trait实现的。
}

当然了,也可以实现ToString trait。例如:

代码语言:javascript复制
pub fn to_stirng() {
    struct Circle {
        radius: i32
    }

    impl ToString for Circle {
        fn to_string(&self) -> String {
            format!("Circle of radius {:?}", self.radius)
        }
    }

    let circle = Circle { radius: 6 };
    println!("{}", circle.to_string());
}

字符串转数字

只要对目标类型实现了 FromStr trait,就可以用 parse 把字符串转换成目标类型。 标准库中已经给无数种类型实现了 FromStr。如果要转换到用户定义类型,只要手动实现 FromStr 就行。 我们得提供要转换到的类型,这可以通过显示声明类型,或者用 “涡轮鱼” 语法(turbo fish,<>)实现。例如:

代码语言:javascript复制
pub fn string_to_number(){
    let num = "12345";
    let num = num.parse::<i32>().unwrap();      // turbo fish写法
    println!("{}", num);

    let num = "12345";
    let num: u64 = num.parse().unwrap();            // 显示声明类型写法
    println!("{}", num);
}

点操作符

方法调用的点操作符看起来简单,实际上非常不简单,它在调用时,会发生很多魔法般的类型转换,例如:自动引用、自动解引用,强制类型转换直到类型能匹配等。

假设有一个方法 foo,它有一个接收器(接收器就是 self、&self、&mut self 参数)。如果调用 value.foo(),编译器在调用 foo 之前,需要决定到底使用哪个 Self 类型来调用。现在假设 value 拥有类型 T。再进一步,我们使用完全限定语法来进行准确的函数调用:

  1. 首先,编译器检查它是否可以直接调用 T::foo(value),称之为值方法调用
  2. 如果上一步调用无法完成(例如方法类型错误或者特征没有针对 Self 进行实现,上文提到过特征不能进行强制转换),那么编译器会尝试增加自动引用,例如会尝试以下调用: <&T>::foo(value)<&mut T>::foo(value),称之为引用方法调用
  3. 若上面两个方法依然不工作,编译器会试着解引用 T ,然后再进行尝试。这里使用了 Deref 特征 —— 若 T: Deref<Target = U> (T 可以被解引用为 U),那么编译器会使用 U 类型进行尝试,称之为解引用方法调用
  4. 若 T 不能被解引用,且 T 是一个定长类型(在编译器类型长度是已知的),那么编译器也会尝试将 T 从定长类型转为不定长类型,例如将 [i32; 2] 转为 [i32]
  5. 若还是不行,那么调用失败

因此点操作符的背后是按照 值方法调用->引用方法调用->解引用方法调用->其它 的顺序来进行调用的。下面是一个例子:

代码语言:javascript复制
fn do_stuff<T: Clone>(value: &T) {
    let cloned = value.clone();
}

上面例子中 cloned 的类型是什么?首先编译器检查能不能进行值方法调用, value 的类型是 &T,同时 clone 方法的签名也是 &T : fn clone(&T) -> T,因此可以进行值方法调用,再加上编译器知道了 T 实现了 Clone,因此 cloned 的类型是 T。

如果 T: Clone 的特征约束被移除呢?

代码语言:javascript复制
fn do_stuff<T>(value: &T) {
    let cloned = value.clone();
}

首先,从直觉上来说,该方法会报错,因为 T 没有实现 Clone 特征,但是真实情况是什么呢?

我们先来推导一番。 首先通过值方法调用就不再可行,因为 T 没有实现 Clone 特征,也就无法调用 T 的 clone 方法。接着编译器尝试引用方法调用,此时 T 变成 &T,在这种情况下, clone 方法的签名如下: fn clone(&&T) -> &T,接着我们现在对 value 进行了引用。 编译器发现 &T 实现了 Clone 类型(所有的引用类型都可以被复制,因为其实就是复制一份地址),因此可以推出 cloned 也是 &T 类型。

最终,我们复制出一份引用指针,这很合理,因为值类型 T 没有实现 Clone,只能去复制一个指针了。

下面是一个更复杂的例子:

代码语言:javascript复制
#[derive(Clone)]
struct Container<T>(Arc<T>);

fn clone_containers<T>(foo: &Container<i32>, bar: &Container<T>) {
    let foo_cloned = foo.clone();
    let bar_cloned = bar.clone();
}

上面代码中,Container<i32> 实现了 Clone 特征,因此编译器可以直接进行值方法调用,此时相当于直接调用 foo.clone,其中 clone 的函数签名是 fn clone(&T) -> T,由此可以看出 foo_cloned 的类型是 Container<i32>

然而,bar_cloned 的类型却是 &Container<T>。这是因为derive 宏最终生成的代码大概如下所示:

代码语言:javascript复制
impl<T> Clone for Container<T> where T: Clone {
    fn clone(&self) -> Self {
        Self(Arc::clone(&self.0))
    }
}

从上面代码可以看出,派生 Clone 能实现的根本是 T 实现了Clone特征:where T: Clone, 因此 Container<T> 就没有实现 Clone 特征。

编译器接着会去尝试引用方法调用,此时 &Container<T> 引用实现了 Clone,最终可以得出 bar_cloned 的类型是 &Container<T>

当然,也可以为 Container<T> 手动实现 Clone 特征:

代码语言:javascript复制
impl<T> Clone for Container<T> {
    fn clone(&self) -> Self {
        Self(Arc::clone(&self.0))
    }
}

此时,编译器首次尝试值方法调用即可通过,因此 bar_cloned 的类型变成 Container<T>

参考资料

  1. 通过例子学 Rust
  2. rust语言圣经

0 人点赞