page contents

那些C语言缺失的,我在Rust里找到了

Librsvg似乎已经到了这样的一个地步:直接将C语言开发的部分改用Rust要比继续使用C语言来得更加容易。更何况,它越来越多的代码已经使用了Rust。近来,我在C语言和Rust之间来回切换。在我看来,C语言似乎变得更像老古董。
C语言挽歌

我大概在24年前就爱上了C语言。当时,我通过一本西班牙语版的“The C Programming Language”(第二版,作者是Brian Kernighan和Dennis Ritchie,所以有时候也用K&R来称呼这本书)来学习C语言。在这之前,我用过Turbo Pascal,它也有指针,也需要手动管理内存,而C语言在当时是新生事物,但十分强大。

K&R因其独特的文风和简洁明了的代码风格而闻名。它甚至还教你如何自己实现简单的malloc()和free()函数,这实在太有意思了。而且,这门语言本身的一些特性也可以通过自身来实现。

在接下来的几年,我一直使用C语言。它是一门轻巧的编程语言,使用差不多2万行代码实现了Unix内核。

GIMP和GTK+让我学会了如何使用C语言来实现面向对象编程,GNOME让我学会了如何使用C语言维护大型的软件项目。一个2万行代码的项目,一个人花上几周就可以完全读懂。

但现在的代码库规模已经不可同日而语,我们的软件对编程语言的标准库有了更高的期望。

C语言的一些好的体验

第一次通过阅读POV-Ray源代码学会如何在C语言中实现面向对象编程。

通过阅读GTK+源代码了解C语言代码的清晰、干净和可维护性。

通过阅读SIOD和Guile的源代码,知道如何使用C语言实现Scheme解析器。

使用C语言写出GNOME Eye的初始版本,并对MicroTile渲染进行调优。

C语言的一些不好的体验

在Evolution团队时,很多东西老是崩溃。那个时候还没有Valgrind,为了得到Purify这个软件,需要购买一台Solaris机器。

调试gnome-vfs线程死锁问题。

调试Mesa,却无果。

接手Nautilus-share的初始版本,却发现代码里面居然没有使用free()。

想要重构代码,却不知道该如何管理好内存。

想要打包代码,却发现到处是全局变量,而且没有静态函数。

但不管怎样,还是来说说那些Rust里有但C语言里没有的东西吧。

自动资源管理

我读过的第一篇关于Rust的文章是“Rust means never having to close a socket”(blog.skylight.io/rust-means-…)。Rust从C++那里借鉴了一些想法,如RAII(Resource Acquisition Is Initialization,资源获取即初始化)和智能指针,并加入了值的单一所有权原则,还提供了自动化的决策性资源管理机制。

  • 自动化:不需要手动调用free()。内存使用完后会自动释放,文件使用完后会自动关闭,互斥锁在作用域之外会自动释放。如果要封装外部资源,基本上只要实现Drop这个trait就可以了。封装过的资源就像是编程语言的一部分,因为你不需要去管理它的生命周期。
  • 决策性:资源被创建(内存分配、初始化、打开文件等),然后在作用域之外被销毁。根本不存在垃圾收集这回事:代码执行完就都结束了。程序数据的生命周期看起来就像是函数调用树。

如果在写代码时老是忘记调用这些方法(free/close/destroy),或者发现以前写的代码已经忘记调用,甚至错误地调用,那么以后我再也不想使用这些方法了。

泛型

Vec<T>真的就是元素T的vector,而不只是对象指针的数组。在经过编译之后,它只能用来存放类型T的对象。

在C语言里需要些很多代码才能实现类似的功能,所以我不想再这么干了。

trait不只是interface

Rust并不是一门类似Java那样的面向对象编程语言,它有trait,看起来就像是Java里的interface——可以用来实现动态绑定。如果一个对象实现了Drawable,那么就可以肯定该对象带有draw()方法。

不过不管怎样,trait的威力可不止这些。

关联类型

trait里可以包含关联类型,以Rust的Iterator这个trait为例:

__Fri Mar 09 2018 10:31:40 GMT+0800 (CST)____Fri Mar 09 2018 10:31:40 GMT+0800 (CST)__pub trait Iterator {
  type Item;
  fn next(&mut self) -> Option<Self::Item>;
}__Fri Mar 09 2018 10:31:40 GMT+0800 (CST)____Fri Mar 09 2018 10:31:40 GMT+0800 (CST)__复制代码

也就是说,在实现Iterator时,必须同时指定一个Item类型。在调用next()方法时,如果还有更多元素,会得到一个Some(用户定义的元素类型)。如果元素迭代完毕,会返回None。

关联类型可以引用其他trait。

例如,在Rust里,for循环可以用于遍历任何一个实现了IntoIterator的对象。

__Fri Mar 09 2018 10:31:40 GMT+0800 (CST)____Fri Mar 09 2018 10:31:40 GMT+0800 (CST)__pub trait IntoIterator {
  /// 被遍历元素的类型
  type Item;
  type IntoIter: Iterator<Item=Self::Item>;
  fn into_iter(self) -> Self::IntoIter;
}__Fri Mar 09 2018 10:31:40 GMT+0800 (CST)____Fri Mar 09 2018 10:31:40 GMT+0800 (CST)__复制代码

在实现这个trait时,必须同时提供Item类型和IntoIter类型,IntoIter必须实现Iterator,用于维护迭代器状态。

通过这种方式就可以建立起类型网络,类型之间相互引用。

字符串切割

我之前发表了一篇有关C语言缺少字符串切割特性的文章(people.gnome.org/~federico/b…),解释了C语言的这个痛点。

依赖管理

以前实现依赖管理需要:

  • 手动调用或通过自动化工具宏来调用pkg-config。
  • 指定头文件和库文件路径。
  • 基本上需要人为确保安装了正确版本的库文件。

而在Rust里,只需要编写一个Cargo.toml文件,然后在文件里指明依赖库的版本。这些依赖库会被自动下载下来,或者从某个指定的地方获取。

测试

C语言的单元测试非常困难,原因如下:

  • 内部函数通常都是静态的。也就是说,它们无法被外部文件调用。测试程序需要使用#include指令把源文件包含进来,或者使用#ifdefs在测试过程中移除这些静态函数。
  • 需要编写Makefile文件将测试程序链接到其中的部分依赖库或部分代码。
  • 需要使用测试框架,并把测试用例注册到框架上,还要学会如何使用这些框架。

而在Rust里,可以在任何地方写这样的代码:

__Fri Mar 09 2018 10:31:40 GMT+0800 (CST)____Fri Mar 09 2018 10:31:40 GMT+0800 (CST)__#[test]
fn test_that_foo_works() {
  assert!(foo() == expected_result);
}__Fri Mar 09 2018 10:31:40 GMT+0800 (CST)____Fri Mar 09 2018 10:31:40 GMT+0800 (CST)__复制代码

然后运行cargo test运行单元测试。这些代码只会被链接到测试文件中,不需要手动编译任何东西,不需要编写Makefile文件或抽取内部函数用于测试。

对我来说,这个功能简直就是杀手锏。

包含测试的文档

在Rust中,可以将使用Markdown语法编写的注释生成文档。注释里的测试代码会被作为测试用例执行。也就是说,你可以在解释如何使用一个函数的同时对它进行单元测试:

__Fri Mar 09 2018 10:31:40 GMT+0800 (CST)____Fri Mar 09 2018 10:31:40 GMT+0800 (CST)__/// Multiples the specified number by two
///
/// ```
/// assert_eq!(multiply_by_two(5), 10);
/// ```
fn multiply_by_two(x: i32) -> i32 {
  x * 2
}__Fri Mar 09 2018 10:31:40 GMT+0800 (CST)____Fri Mar 09 2018 10:31:40 GMT+0800 (CST)__复制代码

注释中的示例代码被作为测试用例执行,以确保文档与实际代码保持同步。

卫生宏(Hygienic Macro)

Rust的卫生宏避免了C语言宏可能存在的问题,比如宏中的一些东西会掩盖掉代码里的标识符。Rust并不要求宏中所有的符号都必须使用括号,比如max(5 + 3, 4)。

没有自动转型

在C语言里,很多bug都是因为在无意中将int转成short或char而导致,而在Rust里就不会出现这种情况,因为它要求显示转型。

不会出现整型溢出

这个就不用再多作解释了。

在安全模式下,Rust里几乎不存在未定义的行为

在Rust的“安全”模式下编写的代码(unsafe{}代码块之外的代码)如果出现了未定义行为,可以直接把它当成是一个bug来处理。比如,将一个负整数右移,这样做是完全可以的。

模式匹配

在对一个枚举类型进行switch操作时,如果没有处理所有的值,gcc编译器就会给出警告。

Rust提供了模式匹配,可以在match表达式里处理枚举类型,并从单个函数返回多个值。

__Fri Mar 09 2018 10:31:40 GMT+0800 (CST)____Fri Mar 09 2018 10:31:40 GMT+0800 (CST)__impl f64 {
  pub fn sin_cos(self) -> (f64, f64);
}

let angle: f64 = 42.0;
let (sin_angle, cos_angle) = angle.sin_cos();__Fri Mar 09 2018 10:31:40 GMT+0800 (CST)____Fri Mar 09 2018 10:31:40 GMT+0800 (CST)__复制代码

match表达式也可以用在字符串上。是的,字符串。

__Fri Mar 09 2018 10:31:40 GMT+0800 (CST)____Fri Mar 09 2018 10:31:40 GMT+0800 (CST)__let color = "green";

match color {
  "red"  => println!("it's red"),
  "green" => println!("it's green"),
  _    => println!("it's something else"),
}__Fri Mar 09 2018 10:31:40 GMT+0800 (CST)____Fri Mar 09 2018 10:31:40 GMT+0800 (CST)__复制代码

你是不是很难猜出下面这个函数是干什么用的?

__Fri Mar 09 2018 10:31:40 GMT+0800 (CST)____Fri Mar 09 2018 10:31:40 GMT+0800 (CST)__my_func(true, false, false)__Fri Mar 09 2018 10:31:40 GMT+0800 (CST)____Fri Mar 09 2018 10:31:40 GMT+0800 (CST)__复制代码

但如果在函数的参数上使用模式匹配,那么事情就会变得不一样:

__Fri Mar 09 2018 10:31:40 GMT+0800 (CST)____Fri Mar 09 2018 10:31:40 GMT+0800 (CST)__pub struct Fubarize(pub bool);
pub struct Frobnify(pub bool);
pub struct Bazificate(pub bool);

fn my_func(Fubarize(fub): Fubarize, 
      Frobnify(frob): Frobnify, 
      Bazificate(baz): Bazificate) {
  if fub {
    ...;
  }

  if frob && baz {
    ...;
  }
}

...

my_func(Fubarize(true), Frobnify(false), Bazificate(true));__Fri Mar 09 2018 10:31:40 GMT+0800 (CST)____Fri Mar 09 2018 10:31:40 GMT+0800 (CST)__复制代码

标准的错误处理

在Rust里,不再只是简单地返回一个布尔值表示出错与否,也不再简单粗暴地忽略错误,也不再通过非本地跳转来处理异常。

#[derive(Debug)]

在创建新类型时(比如创建一个包含大量字段的struct),可以使用#[derive(Debug)],Rust会自动打印该类型的内容用于调试,不需要再手动编写函数去获取类型的信息。

闭包

不再需要使用函数指针了。

结论

在多线程环境里,Rust的并发控制机制可以防止出现数据竟态条件。我想,对于那些经常写多线程并发代码的人来说,这会是个好消息。

C语言是一门古老的语言,用它来编写单处理器的Unix内核或许是个不错的选择,但对于现今的软件来说,它算不上好语言。

Rust有一定的学习曲线,但我觉得完全值得一学。它之所以不好学,是因为它要求开发者对自己所写的代码必须有充分的了解。我想,Rust是一门这样的语言:它可以让你变成更好的开发者,而且它会成为你解决问题的利器。

  • 发表于 2020-12-18 10:47
  • 阅读 ( 801 )
  • 分类:C/C++开发

0 条评论

请先 登录 后评论
Pack
Pack

1135 篇文章

作家榜 »

  1. 轩辕小不懂 2403 文章
  2. 小柒 1478 文章
  3. Pack 1135 文章
  4. Nen 576 文章
  5. 王昭君 209 文章
  6. 文双 71 文章
  7. 小威 64 文章
  8. Cara 36 文章