2
6
2015
19

小谈 Rust

本文来自依云's Blog,转载请注明。

最近很火的 Rust 前不久发布了 alpha 版。正式版虽不说指日可待(还在各种大改中),但是也不是那么遥远了。而经过了这么久,再见 Rust,感觉完全不一样呢。

还记得第一次见 Rust,是在 Fantix 的博客上。现在只记得当时看到各种~和生命周期的东西,挺头疼的。而这次是看到 Rust for beginners 以及已经被合并到《The Rust Programming Language》这本书的官方 guide。感触很容易概括:「一门实用的类 Haskell 语言,是我很早就想要的东西呢。」于是才有了我的第一个 Rust 程序,以及后来的 各种语言实现的 swapview

当然后来事实证明 Rust 不仅仅有着与 Haskell 类似的代数数据类型,比如有表示空的 unit 类型、表示可选的 option 类型、用于返回结果或错误的 Result 类型。作为一名曾经苦学 Haskell 还折腾过 OCaml 的人,看到这些熟悉的类型,感到甚是亲切。这种类型系统最大的特点是类型安全、没有 null 指针/类型。

我接触到的绝大多数编程语言,都会有 null 指针,或者 null / none / nil 类型:

  • C、C++:「Segmentation fault」
  • Java:还记得经常在日志里露脸的「NullPointerException」吗
  • Python:一不小心就会出现的「AttributeError: 'NoneType' object has no attribute 'xxx'」
  • Lua:「attempt to index global 'xxx' (a nil value)」
  • 等等

都是一个不小心,没注意检查对象是不是 null 值就用,然后程序跑着跑着就出错了。

而 Haskell 和 Rust 都能有效地避免这一点,至少是你可以预先察觉,因为它返回的是不一样的类型。比如在 Rust 中,想要把字符串解析成整数,你写let a: i32 = "123".parse()不成。因为不是所有字符串都能解析成整数的,所以parse方法会返回一个Result<i32,ParseIntError>类型(早期版本是返回Option<i32>)。你需要显式地处理错误——或者忽略,如果你希望在出错的时候程序崩溃的话。不管选哪条路,写的时候都是明确知道这个地方可能出错的。不像我写 Python 时那样,直接想当然地写int(xxx),很少会想到想当然以为是个整数表示的xxx其实可能是别的什么东西(比如None)。我总不可能在每一次按.键(取属性)、(键(函数调用开始)、[键(取下标)时都先想想「相关对象会不会是奇怪的东西、是的话要怎么处理」吧?当然这样的错误处理会比较麻烦。如果一个项目不值得这样麻烦的错误处理的话,那就换个更适合的语言去做就是了。

Rust 另一个小特点是,if这类条件判断后边只能是布尔值,和 Haskell 一样,而和 Python、Lisp、Lua 等都不同,就更别说没有真正的布尔类型的 C 了。这样更严谨,挺好的,意义明确。像把0当成假值这种事 Lua 就不干,把空容器当假这事 Python 喜欢但是别的语言又不一样。早先版本的datetime模块甚至认为午夜是假的、其它时间才是真的……

Rust 还有个显著的特点是,关键字都特别短,但是不至于短到不认识,比如pub, fn, mut, ref, impl等。有些人不喜欢,我倒是觉得挺好的。非要写一长串字符浪费空间嘛,虽然现在的显示器不是终端机那样一行只能显示80字符,但我要分割成多列呢。笔记本显示器可以显示两列代码对照着看,外接显示器要显示两行三列还不计偶尔会用到的侧栏呢。

Rust 还继承了 Python 式的显式名称导入。只要不用星号,一个名字是从哪里来的,当前文件里搜一下就找到了。不像 Ruby 那样子,String 莫名其妙多了个方法不知道是干什么的?拿 Google 搜索整个互联网吧……

Rust 资源管理很有特点,我还没在其它语言里看到这种。Rust 程序里,编译器知道每一个对象的生命周期,所以可以在编译期就插入相应的释放资源的代码,不需要 gc 过一段时间停下所有工作来检查一遍。也不像引用计数那样得维护计数,引起很多不必要的内存写请求。毕竟 Rust 的目标是像 C++ 那样高效的系统级编程语言嘛。当然引用计数如果需要还是可以有的。最初 Rust 的另一目标——像 Erlang 那样的并发性,因为绿色用户级线程被官方移出之后就大打折扣了。不过因为类型检查和生命周期推断,线程安全的特性还是保留了下来。

Rust 有各式各样的 trait,类似于 Haskell 里的类型类。要指定资源释放时调用的函数的话,直接实现Drop trait 就可以了。比如我的:

struct AtDir {
  oldpwd: Path,
}

impl AtDir {
  fn new(dir: &str) -> io::IoResult<AtDir> {
    let oldpwd = try!(std::os::getcwd());
    try!(std::os::change_dir(&Path::new(dir)));
    Ok(AtDir { oldpwd: oldpwd })
  }
}

impl std::ops::Drop for AtDir {
  fn drop(&mut self) {
    std::os::change_dir(&self.oldpwd).unwrap();
  }
}

使用的时候直接在需要的作用域时生成一个变量就好,就像下边这样子。Rust 保证在其生命周期结束时调用drop方法。而且是按其所有者变量定义的顺序的逆序调用的。不像 Python,PEP 442折腾了之后,反而是把我一个模块的__del__方法在解释器关闭时的调用顺序弄错了。虽然 Rust 没有 Python 那样的with语句,但是拿Drop可以做到一样的效果,而且能保证调用的时机与预期的一致。

let _cwd = match AtDir::new(directory_name) {
  Ok(atdir) => atdir,
  Err(err) => return Err(err.desc.to_string()),
};

Rust 编译器及标准库目前大部分(92.0%)使用 Rust 编写。而在此之前,Rust 竟然是使用 OCaml 编写的。这从侧面解释为什么目前 79.4% 使用 Go 编写 Go 语言用起来那么像 C(因为它的开发者用的是 C,设计目标好像也是更好的 C),而 Rust 虽然有很多借鉴自 C++ 的东西,导致其语法有些像 C,但写起来完全没有 C 和 Go 那样原始的感觉。这也是我更喜欢 Rust 的原因之一。

目前,除了还在改来改去,让我的程序过几天就各种报错编译不了之外,作为一名初学者,我能发现的另一个缺点就是编译极其费时了,特别是普通优化和链接时优化全开的时候,我一运行时间不到 0.2 秒的小程序,竟然需要半分钟才能编译好……

对了,之前给 Arch Linux 打包的 thestinger 不再打包 Rust 了,所以我开始在 archlinuxcn 源里维护64位的 rust-git、cargo-git(因为 Rust 更新的原因,至今还没打包成功……)以及 vim-rust-git。这些包是自动更新的,因此不出问题的话,有更新就会在一天内更新。

PS: 写的时候有点赶,希望没有写得太乱 ( >﹏<。)

Category: 编程 | Tags: Rust | Read Count: 14055
coolwanglu 说:
Feb 06, 2015 05:56:48 PM

我粗浅的认为go是适合系统架构,rust是适合动态的环境,似乎还是创造者为了简化自己的大project做的一个工具。以后估计语言会特化到不同领域吧。

Avatar_small
依云 说:
Feb 06, 2015 09:27:13 PM

Go 主打的高性能服务开发。Rust 看起来是想取代 C++,做客户端应用程序啊游戏引擎什么的。

菜鸟浮出水 说:
Feb 07, 2015 11:38:29 AM

我感觉我就快被依云忽悠的去折腾rust了 0 - 0

MaskRay 说:
Feb 07, 2015 11:49:50 PM

占位,我是看到reddit前一段时间铺天盖地的Rust文去学的……lifetime很值

Fwolf 说:
Feb 08, 2015 04:15:42 PM

虽然用不到,但觉得 Rust 的特性非常吸引我,有时间会深入体验的。

Star Brilliant 说:
Feb 08, 2015 05:02:01 PM

其实只要打包 AUR 里的 rust-nightly-bin 就好了(自带 cargo)。
现在依然建议只用 nightly 版,等 1.0 正式版出来再说。

因为最大的问题是现在的 Rust 编译器只能用前一天的编译器才能成功编译,
所以不如下载官方的 binary。

其实这样不如只用 AUR,反正都是下载这么多数据。

Avatar_small
依云 说:
Feb 08, 2015 06:19:46 PM

没用过官方编译好的版本。那个为了更广泛的兼容性,和直接在 Arch 上编译出来的应该有些不一样。我打包的这个也是每夜更新的哦。

至于同样是下载这么多数据,它们还真不一样。别忘了我们生活在一个神奇的国度,官方使用亚马逊的 CDN 分发的,所以有不少人报告下载不了或者很慢。

至于编译成功率,你多虑啦。记得 Rust 的构建脚本会去下载一个可以用的快照版本的。反正我那个包配置好之后就一直编译成功呢。

wooya 说:
Feb 08, 2015 09:10:43 PM

rust编译时间主要花在llvm优化这一块。
而使得llvm优化太慢的原因是rustc给llvm的中间代码太冗余了。
对比同样llvm的clang,明显clang更快

Fwolf 说:
Feb 09, 2015 09:20:16 AM

编译时间可以放在以后优化,目前应该先把特性稳定下来。

Jex 说:
Feb 10, 2015 04:57:12 PM

Rust的GC机制是编译期计算出对象活跃期,在deadline后面插入free?(而不是像C++那样过了作用域就释放)。我不确定它是不是只是Escape analysis那种,但Haskell GHC应该是有类似的。还有Haskell JHC编译器也是No GC,但用的是Region Inference。

不过这些方法遇到对象引用share时应该仍然需要配合Runtime GC或者手动free,比如一个global LinkedList,是无法静态分析出一个Node的lifetime的。

这些技术其实早该应用了,本来编译器优化时就已经计算出变量的lifetime了,再多分析下应该也不麻烦吧。可惜现在解释型语言太流行,个个都把JIT当成万金油。我看UglifyJS好像都没实现dead var reuse,Google closure compiler就实现了。
C/C++没用这种GC方式可能一是语言自身定义,变量本来就是作用域范围内有效;二是如果不是ref pointer,那么就是stack region,函数返回时自动free,如果是ref,没有办法静态分析,private方法或许可以。除了C/C++外,好像主流的都是解释型用不上这东西了

Jex 说:
Feb 10, 2015 05:18:37 PM

看了Rust Memory Management的资料,感觉它和C++还是一样的啊,unique_ptr,shared_ptr,&ref。我误以为是无需注释完全由编译器判断其lifetime呢

asdfsx 说:
Feb 22, 2015 02:23:05 PM

最近纠结于golang rust nim,编程语言如雨后春笋啊

Mucid 说:
Feb 23, 2015 07:51:42 PM

“Rust 程序里,编译器知道每一个对象的生命周期,所以可以在编译期就插入相应的释放资源的代码”
貌似objectC+llvm就是这个特性?

Star Brilliant 说:
Feb 23, 2015 08:29:15 PM

ObjectC 貌似不会知道指针所指的目标在什么时候失效吧?

Endle 说:
Feb 24, 2015 10:20:39 AM

大大的布道文章写得真好,可惜短期内没有时间学了
既然 Rust 这么像 C++,那不知道什么时候能和 QT 等工具整合?

Endle 说:
Feb 24, 2015 10:23:34 AM

果然有人有类似的想法
http://endl.ch/content/cxx2rust-pains-wrapping-c-rust-example-qt5
上就有示例

Star Brilliant 说:
Feb 24, 2015 11:18:54 AM

Rust 现在已经在着手研究你说的这种 deadline 后面插 free 的做法了。
但是有些细节还没有敲定(比如 RAII 相关)所以还没有实现。

Avatar_small
依云 说:
Feb 24, 2015 12:50:38 PM

我不会 C++ 也不会 ObjC 啦……

Avatar_small
Qians 说:
May 17, 2015 03:14:49 PM

为什么rust不要runtime了啊?


登录 *


loading captcha image...
(输入验证码)
or Ctrl+Enter

| Theme: Aeros 2.0 by TheBuckmaker.com