Rust 语言学习笔记(二)

3

(一)在这里,下面是(二)。

引用与生命期

写了一天的 Rust 代码下来,发现根本没写几行,万事开头难啊。仔细想来,多半功夫花在学习最佳实践和调试编译错误上了。说到这编译错误我就气不打一出来,放着好生生的 Python 不用跑这来糟心~~呵呵,开玩笑,Rust 的语法——尤其是对于内存管理——可谓是相当精密,在调好了许多编译错误之后,往往会发出“原来是这样”的感叹。对于初学者的我来说,着实不应该期望着一上来就能顺风顺水地写出一次性编译通过的代码——当然我相信对语言的熟悉程度最终会导致这一结果。

废话不多说了,聊一下碰到的一个问题和学习到的东西。之前大部分编译错误都能磕磕绊绊修过去学明白,直到碰到了这个问题(片段):

pub fn parse_uri(uri: &str) -> Result<(&str, &str), int> {
    match uri.find_str("://") {
        Some(pos) => {
            let protocol = uri.slice_to(pos);
            let address = uri.slice_from(pos + 3);
            if protocol.len() == 0 || address.len() == 0 {
                Err(consts::EINVAL)
            } else {
                Ok((protocol, address))
            }
        },
        None => Err(consts::EINVAL),
    }
}

这一段其实很简单,就是 ZMQ 里面解析一个 endpoint 的代码。比如给一个 endpoint tcp://127.0.0.1:8890 作为参数,正确解析的话就应该返回两个字符串:tcp127.0.0.1:8890。不得不用 Python 写一下示意:

def parse_uri(uri):
    return uri.split('://', 1)

回到 Rust。我期望做到的就是在不拷贝字符串的情况下,通过 borrowed pointer aka reference 来实现字符串的解析切分,即借进去一个字符串的引用,返回来该字符串上不同位置的两个不同的引用。理想和现实的差距见下:

...rs:19:28: 19:31 error: cannot infer an appropriate lifetime for region in type/impl due to conflicting requirements
...rs:19             let protocol = uri.slice_to(pos);
                                             ^~~

貌似意思是说,在这个函数里面,Rust 编译器不能确保返回回去的引用到时候还能用。

这下麻烦了。

怎么搞呢?还是得看文档。书上说,这种情况下应该给生命期起个名字。

原来,Rust 对于每一个引用的生命期都是严格控制的,在编译期就保证了引用的阳寿不会长过它引用的对象,从而确保了“野指针”的不可发生性。在刚才的代码中,对于新创建的两个引用 protocoladdress,编译器没有拿到足够的信息以说明它们俩不会变成“野指针”,故而出于安全报了错误。修好这个错误异常简单:

- pub fn parse_uri(uri: &str) -> Result<(&str, &str), int> {
+ pub fn parse_uri<'r>(uri: &'r str) -> Result<(&'r str, &'r str), int> {

这个长的像泛型的东西就叫做命名生命期,就是一个单引号 ' 加随便一个名字。尖括号里的可以认为是声明,就是说当前这个函数将会涉及到一个叫做 r 的生命期;参数里面出现的算是定义,比如说这个例子里面,uri: &'r str 的意思就是说,uri 是一个字符串引用,生命期 r 就刚好跟 uri 的一样;最后返回值类型里面就是使用这个生命期的地方,意思就是说,这个引用和这个引用的声明期跟 r 一致。这样一来,我们就把编译器缺失的信息补上了。

值得注意的是,并不是所有返回的引用都能像这样套用生命期定义,只有从参数里的引用派生出来的引用才可以——比如说这样是不行的:

fn test_ref<'r>(input: &'r str) -> &'r int {
    let num = ~123;
    &*num
}

编译结果:

t.rs:3:5: 3:10 error: borrowed value does not live long enough
t.rs:3     &*num
           ^~~~~
t.rs:1:44: 4:2 note: reference must be valid for the lifetime &'r  as defined on the block at 1:43...
t.rs:1 fn test_ref<'r>(input: &'r str) -> &'r int {
t.rs:2     let num = ~123;
t.rs:3     &*num
t.rs:4 }
t.rs:1:44: 4:2 note: ...but borrowed value is only valid for the block at 1:43
t.rs:1 fn test_ref<'r>(input: &'r str) -> &'r int {
t.rs:2     let num = ~123;
t.rs:3     &*num
t.rs:4 }

关于参数传递的更多

上次说到不用 box 或是引用的参数传递是内存拷贝,这确实是这样,但如果数据量比较小,官方还是建议直接做内存拷贝,而不是使用引用或 box ——为了细微的性能提升而增加代码的复杂性是不值得的;除非必要,如果怕影响性能,可以先做个性能测试。

相反,对于返回值,类似的“值传递”却能在强大的编译器的帮助下“变成”零拷贝。改自官方例子:

fn foo(x: ~int) -> int {
    return *x;
}

fn main() {
    let x = ~5;
    let y = ~foo(x);
}

main() 先创建一个 owned box,放进去一个 5,然后把这个 box 扔进 foo() 里,foo() 把 box 打开扔掉,把内容 5 返回,最后在 main() 里再给 5 套一个盒子 y

问:这个过程中,5 一共被拷贝了几次?

两次?foo() 里拆包一次,返回之后套盒子又一次?

只有一次!原来,在 foo() 返回之前,y 的盒子空间就已经准备好了,foo() 做的其实只是把 5 从一个盒子拷贝到另一个盒子。据说这都是编译器干的好事。

所以呢,官方文档建议大家,在返回数据结构的时候,大胆返回整个值就好了,不用考虑返回前先放到盒子里(用 ~),让调用的地方灵活地来选择是应该放盒子还是怎么样。

可爱的 result

想必大家都知道,Rust 没有提供空指针,也没有提供异常处理。那那那,这代码还怎么写啊,人家明天还要上班呢!

其实前面已经有一个例子了,就是 parse_uri——它会把一个 ZMQ 的地址拆成两部分,否则报错。这里我们看看怎么调用它:(摘自刚写的 zmq.rs 的 bind

    fn bind(&self, addr: &str) -> Result<(), int> {
        parse_uri(addr).and_then(|(protocol, address)| {
            match protocol {
                "tcp" => Ok(()),
                _ => Err(consts::EINVAL),
            }
        })
    }

哈哈,其实很简单明了嘛。and_then 这里起到了承上启下的作用,意思是“先解析 URI,然后返回(执行)下面的;除非解析 URI 失败了,跳过下面直接返回错误”,大大地体现了函数式编程的特点有没有。这个 and_then 其实就是平常有异常的语言中的顺序执行嘛,不去抓异常,依次执行,只要出错就跳出,没错就执行到底。

简单总结一下哈。

  • Result 枚举有两种值,OkErr,适合于可能出错的函数来返回。

  • Option 枚举也有两种值,SomeNone,适合于意义上需要返回“虚无”的函数。

  • Condition 好像给删掉了?

它们都有一堆很方便的小函数可以用,请参考上述文档链接。

你可能感兴趣的

19 条评论
依云 · 2014年03月12日

这模式匹配和枚举太像 Haskell 啦!Result 不就是 Haskell 里的 Either 嘛,Option 不就是 Haskell 里的 Maybe、OCaml 里的 option 嘛~~

+1 回复

依云 · 2014年03月12日

说话我还是没明白为什么编译器没推断出正确的生命期来。
不知道 Rust 会比 Haskell 快多少呢 =w=

回复

fantix 作者 · 2014年03月12日

不懂 Haskell 和 OCaml 的飘过~~

回复

fantix 作者 · 2014年03月12日

这个事儿其实我也百思不得其解。难道 conflicting requirements 指的是这两个引用(参数和返回值)在互相指定对方的生命期(瞎猜ing)?另外~这儿貌似有个 benchmark 哈:http://togototo.wordpress.com/2013/07/23/benchmarking-level-generation-go-rust-haskell-and-d/

回复

依云 · 2014年03月12日

看起来效果不错呢~代码行数多了不少就是了……

回复

fantix 作者 · 2014年03月12日

. 呵呵

回复

依云 · 2014年03月12日

话说 rust-lang.org 怎么访问不了了?

回复

fantix 作者 · 2014年03月12日

我这里貌似可以的:

(env)fantix@fantix-air:~$ telnet rust-lang.org 80
Trying 72.8.141.90...
Connected to rust-lang.org.
Escape character is '^]'.
GET / HTTP/1.1
Host: rust-lang.org

HTTP/1.1 301 Moved Permanently
Date: Wed, 12 Mar 2014 06:45:39 GMT
Server: Apache/2.2.16 (Debian)
X-Powered-By: PHP/5.3.3-7+squeeze16
Location: http://www.rust-lang.org
Connection: close
Vary: Accept-Encoding
Content-Length: 0
Content-Type: text/html

Connection closed by foreign host.

回复

依云 · 2014年03月12日

哦,不能用文档里写的 https 协议访问……

回复

依云 · 2014年03月12日

汗,官网上那个例子编译通过不了……

回复

依云 · 2014年03月12日

果然 Rust 的 Influenced by 里各种函数式语言都有~~

回复

fantix 作者 · 2014年03月12日

呵呵,各种踪迹~~哪个编不过啦?

回复

leunggamciu · 2014年03月12日

do 貌似在0.9之后就被废弃了,现在使用spawn(proc() {......})

回复

依云 · 2014年03月12日

哦~但是为什么 0.9 的教程里还有呢,还是说我点错了……

回复

依云 · 2014年12月23日

我遇到了变量生命周期不够长的问题呜呜呜……

回复

fantix 作者 · 2014年12月23日

哈,祝好运!具体什么问题分享一下?

回复

依云 · 2014年12月23日

我想把一个 &str 返回出去。后来换 String 了。不过写出来的程序效率不怎么样啊:http://p.vim-cn.com/cbK2/rust

回复

依云 · 2014年12月23日

啊啊,我的错,忘记开启优化了!!

回复

fantix 作者 · 2014年12月24日

酱事儿啊

回复

载入中...