推荐编程语言:Rust 及特性介绍

Rust

Rust是一个由 Mozilla 主导开发的通用、编译型编程语言。它的设计准则为“安全,并发,实用”,支持函数式,并发式,过程式以及面向对象的编程风格。
—摘自 Wiki

Rust 的特点(卖点):

  • 零开销的抽象
  • 转移语义
  • 保证内存安全
  • 没有数据竞争的线程
  • trait 泛型
  • 模式匹配
  • 类型推断
  • 极小运行时
  • 高效的C绑定

—摘自官网描述

和类似编程语言的异同

  • Rust 编译器编译出来的程序无需运行时支持,这点跟 Go 是一样的。但是 Rust 程序并没有内嵌一个垃圾回收器,这点跟 Go 不一样。
  • Rust 程序虽然不是垃圾回收器管理内存,但也不是手动管理内存,所以跟 C++ 也不一样。Rust 的内存管理仍然是自动的,听起来很不可思议。不过这也带来了一点麻烦,那就是需要在代码层面进行一定前提的保证。

所以,Rust 是一种无需手动管理内存的系统编程语言。 比带 GC 的语言更实时、比手动管理内存的语言更(内存)安全。

Rust 周边

Rust 1.0 正式版在15年中旬推出,所以其实 Rust 是一门非常新的语言,年龄还很小,自然也比较小众。当然这个“小众”指的是将 Rust 应用于实际产品或者生产环境中的公司,并不是说 Rust 没人学没人接触。反而 Rust 被认为是一门有前景的广受好评的语言。
想知道那些公司在用 Rust ?看这里

在 Rust 的发展过程中,不得不提一个叫 Servo 的项目,它是一个浏览器 Web 引擎。以 Rust 作为开发语言,同样由 Mozilla 主导。
Rust 在 Servo 上的实际开发应用的同时也能充分体现 Rust 的缺陷以及改进,相当于一个和 Rust 共生的实验性项目。

为什么学 Rust

如果你需要一门高级的系统编程语言,但是并不喜欢 C++,那么 Rust 或许你可以尝试一下。
如果你只接触过一些比较传统的编程语言以及编程模型,例如 Java、C# 以及一些脚本语言,你当然适合学习 Rust ,它会给你带来的新的眼界上的开阔以及弥补对系统编程技术栈上的不足。

我个人在学习 Rust 之前其实对 Rust 已经认识得比较深了,只是缺乏一个动力学习它。推迟了半年多才在被延期很久的学习计划中重新调出来。我为什么学习 Rust 很简单,因为它是一门优秀的编程语言。
不过要明确的是,Rust 值得学习但目前并不能给你直接的带来“钱途”,因为国内很少使用 Rust 的公司(也就是这个技能未必会给你带来加分或者得到相关岗位)。所以学习 Rust 首先要明白是为了接触新事物,而不是接触一种赚钱的新途径。

介绍主要特性

其实也算不上“特性”,Rust 从语言角度其实并没有多少创新,大多数仍然是借鉴的其他优秀的语言设计实践。所以下面我介绍的其实是 Rust 语言的的几大主要特点。
当然在这之前你要安装 Rust 开发环境。当你能正确执行完成下述步骤的时候,Rust 环境就已经正确了。

第一个程序

创建 hello_world.rs:

fn main() {
              println!("Hello, World!");
          }
          

使用 rustc 编译源代码文件,编译目标到 out 目录:

rustc hello_world.rs -o out/bin
          

执行 bin 文件:

out/bin
          

输出:

Hello, World!
          

这是一个典型的在 main 函数标准输出字符串的传统 Hello World 例子。

  1. fn 即 function,在 Go 里边是 func,Rust 定义函数关键字比 Go 更简洁…
  2. println! 并不是一个函数,它是一个宏调用。在 Rust 里边一般 后缀!的都是宏调用,不是库中的函数

然后这个例子基本没有介绍价值了,毕竟它只是 Hello World。

此变量非彼变量

在几乎任何一门编程语言中,最基本的概念元素之一就是“变量”了。一般来讲,变量指的对数据引用的自定义别名,使用这个别名可以修改数据、使用数据以及重新赋值别名对应的数据对象等。
一般在动态语言中,变量不会强制要求类型不可变,而静态语言则会强制要求类型不变。所以在动态语言中,声明一个变量可以无需指定类型,并且可以随意修改成不同类型的值,例如 JS:

var a = 100; // 100 -> Number
          a = 'Hello World!'; // "Hello World!" -> String
          

但是在静态语言,例如 Java 中,则必须指定变量类型:

String a = "Hello World!";
          Int b = 100;
          a = b; // 编译错误
          

但是在某些静态语言中,在变量声明的时候也不需要指定变量类型,这是编译器“类型推倒”的功劳。实际变量仍然是满足固定类型和类型不可变。

介绍完了变量基础知识,要进入正题了。在 Rust 中,变量和传统的“变量”意义有差别,例如 Rust 的变量可能无法修改、可能无法被访问第二次等。在不了解 Rust 变量的“所有权”、“借用/引用”、“拷贝”等概念之前会觉得非常奇怪。

例如说这段代码:

fn main() {
              let x: i32 = 100;
              x = 1000;
          }
          

let 可以理解为 Rust 声明变量的关键字,这段代码就是声明了一个 i32 类型的 x 变量并赋了初始值,然后修改了这个变量值。
但是这段代码编译通不过。注意:1000 是 i32 类型的,所以不存在类型错误问题。
原因是在 Rust 中,let 声明的变量是“不可变”的,“可变”的需要用 let mut 声明,所以:

fn main() {
              let mut x: i32 = 100;
              x = 1000;
          }
          

这样就编译通过了… 那么这跟 Scala 中的 val 变量或者其他语言都用 const 常量不是一回事吗?当然不是!请继续往下看。

fn main() {
              let x: String = String::from("啦啦啦~");
              let y = x;
              println!("{}", x);
          }
          

这段代码是想把 String 类型的 x 变量赋值给 y,然后输出 x 的值。很遗憾,它也编译通不过。
原因是在 Rust 中,let 声明的变量实际是一种能“绑定”数据的别名,重点是“绑定”这个概念:

let x 相当于创建了一个名为 x 的所有者,这个 x 可以绑定到一个数据区域上。String::from 方法创建了一个 String 类型的数据 “啦啦啦~”,然后交给 x 绑定。所以此时 x 就是该字符串数据的所有者。
let y = x 表示创建了一个新的所有者 y,并且把 y 绑定到 x 的数据上。但是该数据已经有 x 这个所有者了,想绑定到新的所有者 y 上,x 只能交出所有权,那么此时的 “啦啦啦~”的所有者是 y,而 x 没有再绑定任何的数据,这个动作叫做“转移所有权”,此时 x 变量的状态是 moved。
而 println! 无法访问一个已经转移所有权的 x 变量,这就是编译通不过的原因。

我之所以说 let 声明的不可变“变量”和其他语言类似的概念是不同的,是因为在 Rust 中“不可变”约束的是数据的所有者“绑定不可变”以及数据“不能通过所有者修改”。所以不仅仅只是并非常量。在 Rust 中当然也提供常量 const 关键字,但是没有绑定的概念。

与上述概念相悖的代码:

fn main() {
              let x: i32 = 1000;
              let y = x;
              println!("{}", x);
          }
          

如果把 String 类型换成 i32类型,这段代码能顺利编译和运行。这是为什么呢?所有权不是转移了吗?为什么仍然能访问 x?
原因就是 i32 类型实现了 “Copy” 特性,实现这个特性的类型在被 let 关键字使用的时候,并不会移交所有权,而是创建一个自身的副本数据再将新副本绑定给新的变量。于是乎,y 绑定到了不同的数据上。如果你理解了,那么将 y 改为可变绑定,修改 y 的值,一定也能答出,并不会影响到 x 的内容,如下:

fn main() {
              let x: i32 = 1000;
              let mut y = x;
              y = 99;
              println!("x:{},y:{}", x, y); // x:1000,y:99
          }
          

所以,想让 String 类型的绑定变量能被 let 关键字使用并且不交出所有权,就得给 String 实现 Copy 特性,这个将留在下面章节讲述。
但是在这之前还有更适用的“引用/借用”来解决这个所有权机制带来的麻烦。

借与还

因为所有权机制,所以 let 使用时不应该直接取得所有权,而应该“借用”所有权。代码如下:

fn main() {
              let mut x = String::from("Hello!");
              {
                  let mut y = &mut x;
                  *y = String::from("World!");
              }
              println!("{}", x); // "World!"
          }
          

x 是一个可变的 String 类型绑定,并且有初始值。注意:这里之前一直未介绍 Rust 是一门有类型推倒能力的语言,所以 x 不显式声明类型也可以被等号右边的 String::from 方法的返回值推倒类型出来。 & 操作符表示取引用,y 绑定到 x 的可变引用上,可以称作“y 借用了 x 的所有权”。并且由于是 &mut(可变引用),所以 y 可以修改 x 的数据内容。此时 y 被称作”Hello!“数据对象的“借用者”。 * 表示取引用对象,这里的两个符号跟指针的操作符意义很相似。修改了 y 引用对象的内容,然后输出 x ,会发现 x 也被修改了。这就是“引用/借用”。
注意:大括号代码块中表示一个自己作用域,平行级别的大括号代码块作用域互相独立。而里层大括号作用域可以访问外层作用域,不过从外层作用域的角度看是隔离的,即外层作用域无法访问里层作用域对象,并且作用域的最有一行代码执行完毕后所有该作用域中的对象会被回收。

这里有一个小疑惑,就是把大括号删除后,编译不通过,如下:

fn main() {
              let mut x = String::from("Hello!");
              let mut y = &mut x;
              *y = String::from("World!");
              println!("{}", x);
          }
          

那么,问题来了,为什么 y 借用 x 必须放到独立的作用域中呢?
原因就在于借用有一些必须遵循的“规则”:

  1. 同一时刻,最多只有一个可变借用(&mut T),或者2。
  2. 同一时刻,可有0个或多个不可变借用(&T)但不能有任何可变借用。
  3. 借用在离开作用域后释放。
  4. 在可变借用释放前不可访问源变量。

显然去掉大括号的代码违反了规则1,所以无法通过编译。而加上大括号由于规则3的作用,所以没有违反规则1,这就是加大括号的原因。
由于作用域结束,y 被销毁,借来的引用就相当于被还给了了原绑定变量。既然已还,所以规则4也不成立了,于是乎访问源变量 x 无压力。
也就是说,我们在用源变量之前必须让借用者先归还引用。我们可以总结出:所有权转移不会自动归还所有权,而借用会在借用期间禁止原所有者进行所有权操作,并且让借用者获得临时的读写权限,并且只有在归还以后才允许所有者恢复所有权操作(例如转移或者继续借给其它变量)。所以再想继续使用源变量的情况下,不能直接转移所有权。

如果想在共享作用域内存在多个对 x 的可变引用怎么办?首先,存在多个直接对 x 的可变引用是行不通的,但是可以间接的引用 x ,如下:

fn main() {
              let mut x = String::from("Hello!");
              {
                  let mut y = &mut x;
                  *y = String::from("World!");
          
                  let mut z = &mut y;
                  **z = String::from("Hello World!");
              }
              println!("{}", x); // "Hello World!"
          }
          

y 是 x 的直接可变引用,z 是 y 的直接可变引用,则 z 间接的引用了 x。由于第一次解引仍然是一个引用(y),所以需要解引两次(**)。

生命周期

生命周期表示一个对象在被创建和销毁时经过的区间,例如上述大括号代码块中 y 和 z 的生命周期在离开作用域时(即 { 以后)结束。
不过在一些情形下(函数、struct 等),生命周期没有这么直观:

fn foo(str: &String) -> &String {
              str
          }
          
          fn main() {
              let a = String::from("Hello");
              let b = foo(&a);
              println!("{}", b);
          }
          

上面的代码的意思是定义了一个 foo 函数,接收一个 String 类型的借用。函数返回 str 时,由于 str 是 a 的借用,而返回值又是 str 的借用,所以 a 的生命周期 >= str >= 返回值。
函数定义时,若只有一个输入参数和一个输出参数,且输出参数依赖输入参数。那么默认输出参数的生命周期等于输入参数。即代码如下:

fn foo<'a>(str: &'a String) -> &'a String {
              str
          }
          

‘a 是显式表示生命周期的标识符,foo 的尖括号定义所有需要的标识符然后让输入和输出标记。因为满足上面的条件,所以此时输入输出的生命周期标识都是 ‘a 。注意:这里的 a 可以是如何字母,b、c、d 等都可以。不过当可以隐式推倒生命周期时就不要显式标记了。

什么情况下需要显式标记生命周期呢?当函数返回值依赖多个输入参数时:

fn foo(a: &String, b: &String) -> &String {
              if (true) { a } else { b }
          }
          
          fn main() {
              let a = String::from("Hello");
              let b = String::from("World");
              let c = foo(&a, &b);
              println!("{}", c);
          }
          

上面的函数有两个借用参数,并且返回值可能会是如何一个输入参数。虽然这里是 if(true) 能保证一定会返回 a ,但是这里是运行时决定的,编译时并不考虑,所以无法隐式推倒生命周期。所以需要这样:

fn foo<'a>(a: &'a String, b: &'a String) -> &'a String {
              if (true) { a } else { b }
          }
          
          fn main() {
              let a = String::from("Hello");
              let b = String::from("World");
              let c = foo(&a, &b);
              println!("{}", c);
          }
          

因为满足 a 生命周期 >= 返回值 && b 生命周期 >= 返回值,所以能通过编译。
如果在某些情况下,需要指定不同的生命周期呢?假设如下:

fn foo<'a, 'b>(a: &'a String, b: &'b String) -> &'a String {
              if (true) { a } else { b }
          }
          

还记得上面的规定吗?返回值可能依赖于 a 或者 b,然而返回的是 ‘a 的生命周期,返回值生命周期是 ‘a,所以生命周期 ‘b >= ‘a。所以还需要显式定义生命周期大小关系:

fn foo<'a, 'b: 'a>(a: &'a String, b: &'b String) -> &'a String {
              if (true) { a } else { b }
          }
          

‘b: ‘a 表示生命周期 ‘a 是 ‘b 的子集,通过编译。这些概念在 struct 和 enum 上都是差不多的,这就是 Rust 中的生命周期。

Unsafe

在按照要求编写能通过编译的 Rust 代码的情况下,程序一定是内存安全的。上述的所有权、借用/引用、生命周期等可以看作对这种编译器层面保证内存安全的“保证”。
但是在某些情况下,这些保证会程序变得过于复杂或者性能代价太大,例如实现某些复杂的数据结构(存在很多指针的互相引用)。还有就是有编译器无法保障的情况,例如调用外部的“不安全”接口。

所以,Rust 提供了 unsafe 关键字。注意:unsafe 并不是“一定不安全”,而是“无法保证绝对安全”。
unsafe 代码块可以让开发人员编写一些“非安全”代码,例如解引裸指针:

fn main() {
              let x = 5;
              let raw = &x as *const i32;
              let point_at = unsafe { *raw };
              println!("row point_at:{}", point_at);
          }
          

as 关键字是类型转换(跟 C# 一样),*const T 表示裸指针。这段代码是将一个引用转换为裸指针然后解引指针将内容赋值给新的变量绑定,其中解引指针(*raw)是不安全操作,在默认情况下是不能使用的,必须放到 unsafe 代码块中。

类似的,还有“读写一个可变的静态变量static mut”:

static mut N: i32 = 5;
          unsafe {
              N += 1;
              println!("N: {}", N);
          }
          

调用一个不安全函数:

unsafe fn foo() {
              //实现
          }
          fn main() {
              unsafe {
                  foo();
              }
          }
          

等等。

函数式

Rust 也对函数式的某些特性进行了支持,例如 闭包、高阶函数。所以在 Rust 中,函数也是第一值级类型。而由于 Rust 又有 struct 和 trait,所以 Rust 也支持对面向对象。即 Rust 确实算一门多范式语言。

在 Rust 中使用闭包: 单行闭包:

let plus_one = |x: i32| x + 1;
          assert_eq!(2, plus_one(1));
          

多行闭包:

let plus_two = |x| {
              let mut result: i32 = x;
          
              result += 1;
              result += 1;
          
              result
          };
          
          assert_eq!(4, plus_two(2));
          

| 管道符号中间是参数,{} 中间是代码块。单行时可以省略大括号。使用环境变量:

fn main() {
              let multiples = 99;
              let totle = |x: i32| x * multiples;
              let result = totle(101);
              println!("{}", result); // 9999
          }
          

此处的闭包使用了外部作用域的 multiples,准确的说是“借用”了 multiples。由于是借用,所以如果出现闭包内可变借用的情况,如下:

fn main() {
              let mut num = 5;
              let plus_num = |x: i32| x + num;
          
              let y = &mut num;
          }
          

闭包 plus_num 中借用了 num 的可变引用,再加上下面的可变借用 y ,即在一个共享作用域内出现了对同一个对象(x)的多个可变借用,所以编译会失败。
所以需要这样:

fn main() {
              let mut num = 5;
              {
                  let plus_num = |x: i32| x + num;
                  println!("{}", plus_num(11));
              }
              let y = &mut num;
              println!("{}", y);
          }
          

或者在这种情形下:

fn main() {
              let mut init = 0;
              let mut counter = || {
                  init += 1;
                  init
              };
              counter();
              counter();
              let result = counter();
              println!("{}", result);
              println!("{}", init);
          }
          

由于闭包存在对 init 的可变借用,所以闭包未回收之前是无法访问源所有者对象(init)的,编译会失败。注:如果闭包需要改变外部环境,那么闭包要用 let mut 声明。
所以,需要这样:

fn main() {
              let mut init = 0;
              {
                  let mut counter = || {
                      init += 1;
                      init
                  };
                  counter();
                  counter();
                  let result = counter();
                  println!("{}", result); // 3
              }
              println!("{}", init); // 3
          }
          

不过,如果在闭包中不借用而是直接获得外部变量的所有权呢?上面也讲过了,如果直接获取 i32 类型的所有权的话会拷贝一个副本对象用于绑定,原变量绑定不变、对象数据也不会变。在闭包中想直接获取所有权而不是借用,需要用 move 关键字,使用 move 便可以无需独立作用域:

fn main() {
              let mut init = 0;
              let mut counter = move || {
                  init += 1;
                  init
              };
              counter();
              counter();
              let result = counter();
              println!("{}", result); // 3
              println!("{}", init); // 0
          }
          

高阶函数例子:

fn subtract(x: i32) -> i32 {
              x - 100
          }
          fn add(x: i32) -> i32 {
              x + 100
          }
          fn calculate(x: i32, func: fn(i32) -> i32) -> i32 {
              func(x)
          }
          fn main() {
              println!("{}", calculate(100, add)); // 200
              println!("{}", calculate(100, subtract)); // 0
          }
          

上面 calculate 是将函数作为参数的高阶函数,参数类型是 “fn(类型) -> 返回值”。当然还有以函数作为返回值的情况:

fn calculate(x: i32) -> fn(i32) -> i32 {
              fn subtract(x: i32) -> i32 {
                  x - 1
              }
              fn add(x: i32) -> i32 {
                  x + 1
              }
              if x % 2 == 0 { add } else { subtract }
          }
          fn main() {
              println!("{}", calculate(99)(2)); // 1
              println!("{}", calculate(100)(0)); // 1
          }
          

返回值为”fn(类型) -> 返回值”,不需要括号(当然我个人觉得加上括号或许更好,避免多个 -> 干扰)。上面的高阶函数 calculate 根据输入值的奇偶而返回不用的 function,进而进一步调用。注意:返回的函数是不能使用外部函数的变量的,如果需要那么应该返回闭包:

fn calculate(x: i32) -> Box<Fn() -> i32> {
              let subtract = move || x - 1;
              let add = move || x + 1;
              if x % 2 == 0 {
                  Box::new(add)
              } else {
                  Box::new(subtract)
              }
          }
          fn main() {
              println!("{}", calculate(101)()); // 100
              println!("{}", calculate(98)()); // 99
          }
          

最后

本文大致上介绍了 Rust 主要的几个特点,当然并不全面。本来还想介绍 Trait 和 错误处理 以及 FFI(外部函数接口)方面的,但是精力有限先暂时放下了。
这篇文章的代码主要是按照我以前接触 Rust 记录的一个个小 Demo,Rust 也算是我学习的编程语言周期稍长的一类,更长的例如 Scala 是因为内容确实太多。而 Rust 把握在一个度,并且以后的更新不会再随意加新的特性,甚至可能砍特性。

想系统的开始学习 Rust,当然得完完整整的看文档。如果阅读英文马马虎虎导致过于吃力,这里还有中文文档。当然我的建议是从中文文档入手,英文文档深入!

不管怎样,学习 Rust 是很有价值的一件事。我也非常期待 Rust 的成功和 Mozilla 的顺利发展。