< 返回版块

2019-03-21 11:59    责任编辑:Mike

本文转载自:http://wayslog.com/2017/03/28/Rust-Type-System/


title: Rust Type System date: 2017-03-28 10:45:21 tags:

  • rust
  • error
  • Rust-In-Server-Side

Meta

本系列文是 log 我本人的一些关于 Rust 的总结,发上来将这些作为骗赞的资本。想起来就更新,想不起来也不要催更,log 很懒的。

Trait and Generic

初次接触 Rust 的同学可能对其 trait 的概念表示不能理解,其实这个概念在其他 fp 语言里早已被应用的很广泛了,Rust 作为一门 System Programming Language 引入这个概念,自然是想要让它自己的 type system 拥有更好的表达能力。trait 被国内的翻译者翻译成了 特征、特性, 但是我觉得都不能表达其原有意思,干脆在这这里保留原名字。它规定了一个类型能做的一系列 动作,可以看做约束,也可以认为是另一种形式上的 标记。具体怎么使用呢,我们可以来看一下下面。

增补标准库

Rust 提供了一个功能不算多的标准库(就这还天天被吐槽标准库太大了),于是很自然的,我们需要的某些功能在标准库里就可能找不到了。但是,有的时候我们又希望对标准库做一些增补。比如,我想对所有所有实现了 std::io::Read 的类型增补一个 read_u8 功能,想要从一个 Read 里直接读出一个 u8 类型(对应 C++ 里面的 char, Go 里面的 byte 类型)。

对于其他 OOP 语言,比如 Java 这种,显式继承 interface 的语言,除非我们能拿到 JVM 的源码,修改,并自己编译一版标准库,不然是不行的。但在 Rust 里,我们可以这么做,先上代码:

use std::io::{Read, Error, ErrorKind, Result};

trait ReadExt: Read {
    fn read_u8(&mut self) -> Result<u8> {
        // 示例代码,效率问题不在考虑之内
        let mut bytes = [0u8; 1];
        let size = try!(self.read(&mut bytes[..]));
        if size != 1 {
            return Err(Error::new(ErrorKind::UnexpectedEof, "read not one bytes"));
        }
        Ok(bytes[0])
    }
}

这里有些点我们需要解释下:

trait inherit

Rust 没有提供 struct 的继承机制,但是,提供了一个被官方称为 trait inherit 的机制,具体表现就是上面代码里的 trait ReadExt: Read {...} 。这里,ReadExt 其实表示了一个类型范围,这个范围会比 Read 更广泛。在 Rust 的其他地方,我们可以看到,除非是类型声明,不然 A: B 符号总表示一个类型范围: A 的(type Kind)范围里包含 B。此条同样适用于生命周期限制,因为生命周期也是 Rust 的一种 Kind 。好了,话题扯远了,回归主题。

我们看到,这里我们规定了一个类型,所有实现 Read 的类型都可以实现一个 ReadExt 来实现 读取u8 这么一个操作。但是,仅仅这样就能用了么?答案当然是不能。 我们附上上测试代码

use std::io::{Read, Error, ErrorKind, Result};
use std::io::Cursor;

trait ReadExt: Read {
    fn read_u8(&mut self) -> Result<u8> {...}
}

fn main() {
    let mut cursor = Cursor::new(vec![0u8, 1, 2]);
    let value = cursor.read_u8().unwrap();
    println!("read u8: {}", value);
}

尝试运行一下,我们得到了这么个错误:

error: no method named `read_u8` found for type `std::io::Cursor<std::vec::Vec<u8>>` in the current scope
  --> src/main.rs:21:24
   |
21 |     let value = cursor.read_u8().unwrap();
   |                        ^^^^^^^
   |
   = help: items from traits can only be used if the trait is implemented and in scope; the following trait defines an item `read_u8`, perhaps you need to implement it:
   = help: candidate #1: `ReadExt`

瓦塔?编译信息说的明白:你必须对你使用的类型(这里是Cursor<Vec>) 实现 ReadExt。 所以,我们需要加上这么一行:

trait ReadExt: Read {...}

impl<T: Read> ReadExt for T {}

fn main() {...}

这一行: impl<T: Read> ReadExt for T {} 的意思再明白不过了,就是对所有实现了 trait Read 的类型 T 去实现 ReadExt。再次运行,成功。而且,这里我们的 ReadExt 其实是没有任何 需要用户提供的函数 的。

Error Handle

异常,在我们目前常见的语言中,几乎都有内建的机制。 无论是 C++/Java 的 try/catch, Python 的 try/expect,其实其本质就是一个终止执行并有机会 failover 的过程。与异常对应而生的,还有一个异常安全的问题。这一点,我们不展开。

异常,其实是一个非常经典而且直观的错误解决方式。

而 Rust 采用了更加函数式而且统一的 Result/Option 来进行错误处理。这里, Result 和 Option 不再是需要 runtime 参与的特殊类型,而是牢牢的嵌入到了其类型系统里。

继续以上面的例子来看,我们将完整代码附上:

use std::io::{Read, Error, ErrorKind, Result};
use std::io::Cursor;

trait ReadExt: Read {
    fn read_u8(&mut self) -> Result<u8> {
        let mut bytes = [0u8; 1];
        let size = try!(self.read(&mut bytes[..]));
        if size != 1 {
            return Err(Error::new(ErrorKind::UnexpectedEof, "read not one bytes"));
        }
        Ok(bytes[0])
    }
}

impl<T: Read> ReadExt for T {}

fn main() {
    let mut cursor = Cursor::new(vec![0u8, 1, 2]);
    let value = cursor.read_u8().unwrap();
    println!("read u8: {}", value);
}

对于一个 io 操作,我们认为其在操作的时候很有可能会失败,于是 std::io 默认提供了 Result 类型。翻开标准库,我们可以看到:

// in std::io source code
use std::result;

pub type Result<T> = result::Result<T, std::io::Error>;

好吧,我们知道了 std::io 下的 Result 类型其实是 std::result 类型下的别名,只不过限定死了错误类型是 std::io::Error。

对于一个 Result, 我们可以进行 unwrap, 这个操作其实和如下操作等效:

match some_func() {
    Ok(val) => val,
    Err(err) => {
        panic!("reason {:?}", err);
    }
}

即:取出正确的值,一旦错误,则panic当前线程。

这其实是 Rust 的所有错误处理中最不值得推荐的一种处理,但是在程序原型构建期,或者 demo 里还是比较有用的。

相对的, 其实我们还可以进行 match 操作,这个关键字在 Rust 以及其他函数式语言里被称为 模式匹配:

let value = match cursor.read_u8() {
    Ok(val) => val,
    Err(err) => {
        println!("read faild: {:?}", err);
        0
    }
};

关于模式匹配的概念,这里不再赘述,有想要深入了解的可以参见这里 模式匹配

但是,就算有模式匹配,其实对于错误处理来说,也是略繁琐,我不想在每个地方都进行一次模式匹配的,很多时候,其实模式匹配就是想拿出值来,但是遇到错误则将错误返回,比如下面:

let size = match self.read(&mut bytes[..]) {
    Ok(val) => val,
    Err(err) => {
        return Err(err);
    }
};

为此,Rust 提供了一个简写的写法:try! 宏。这个宏提供了如上的功能,于是我们在代码里就看到了 try! 的用法:

let size = try!(self.read(&mut bytes[..]));

相对来说是简洁了一点点,但是,Rust 的 Result 的很多操作是可以串联起来的比如:

let value = try!(try!(try!(call0()).call1()).call2());

try!(try(try!(....))) .....

这么写起来就太麻烦了,于是 Rust 提供了一个名叫 Carrier 的语法糖,在语法上用 ? 代替,于是我们就可以这么写:

let value = call0()?.call1()?.call2()?;

其作用是和 try! 是一样的。

Uniform Error Type (personal best practice)

这里我们要说的,是 Rust 的两个 trait : FromInto。 顾名思义,这是一对相反操作,表示 一种类型可以无任何风险的转换成另一种类型变量(这里其实还有一个限制:前后变量都是 ownership 的,但是这个在这里不是重点)。重要的是, try! 和 ? 里,默认都调用了 Into 操作。前面说,From 和 Into 其实是反操作,那么, Rust 库里自然有互相实现他们的反操作的地方:

/// (注:摘自标准库注释)
/// From<T> for U implies Into<U>for T

预备知识介绍完毕,我们可以开始了。首先,准备一个错误类型和自己的 Result 类型:

#[derive(Debug)]
pub enum MyError {
}

pub type MyResult<T> = result::Result<T, MyError>;

然后,我们让这个类型能承包 std::io::Error:

#[derive(Debug)]
pub enum MyError {
    IoError(Error),
}

impl From<io::Error> for MyError {
    fn from(oe: io::Error) -> MyError {
        MyError::IoError(oe)
    }
}

PS: 我喜欢实现 From,当然你也可以去实现 Into ,一样的。

这样,我们实际上得到了一个 std::io::Error 的超集。我们最终,整理到的代码就是:

use std::io::{self, Read, Error, ErrorKind};
use std::io::Cursor;
use std::result;
use std::convert::From;

#[derive(Debug)]
pub enum MyError {
    IoError(Error),
}

impl From<io::Error> for MyError {
    fn from(oe: io::Error) -> MyError {
        MyError::IoError(oe)
    }
}

pub type MyResult<T> = result::Result<T, MyError>;

trait ReadExt: Read {
    fn read_u8(&mut self) -> MyResult<u8> {
        // 示例代码,效率问题不在考虑之内
        let mut bytes = [0u8; 1];
        let size = self.read(&mut bytes[..])?;
        if size != 1 {
            return Err(Error::new(ErrorKind::UnexpectedEof, "read not one bytes"))?;
        }
        Ok(bytes[0])
    }
}

impl<T: Read> ReadExt for T {}

fn main() {
    let mut cursor = Cursor::new(vec![0u8, 1, 2]);
    let value = match cursor.read_u8() {
        Ok(val) => val,
        Err(err) => {
            println!("read faild: {:?}", err);
            0
        }
    };
    println!("read u8: {}", value);
}

基本上对原代码破坏很小就实现了错误类型的统一。

以上。