元组 Tuple

元组 tuple, 比如 (1, 'a', 3.14, true), 用于存放定长的不同数据类型.

元组与数组 (array) 的比较:

  • 可以组合各种不同类型的数据, 而数组只能存放相同类型的数据
  • 与数组一样, 编译期即可确定其元素个数
  • 与数组一样, 使用下标访问元素, 而不是像 struct 那样使用元素名来访问
  • 常使用它做为函数返回值, 这样函数就可以返回多个不同类型的值

默认可以使用下标访问元组中的元素:

#![allow(unused)]
fn main() {
let tuple = (1, 'a', 3.14, true);
assert_eq!(tuple.0, 1);
assert_eq!(tuple.1, 'a');
assert!(tuple.3);
}

但下标的语法可读性并不好, 可以使用模式匹配的方式. 比如 fn split_once(&self, delimiter: Pattern) -> Option<(&str, &str)>; 用来分隔字符串:

#![allow(unused)]
fn main() {
let entry = "key=42";
if let Some((key, value_str)) = entry.split_once('=') {
  assert_eq!(key, "key");
  let value: i32 = value_str.parse().unwrap();
  assert_eq!(value, 42);
}
}

Rust 允许在使用逗号的地方, 在最后一个元素尾部多加一个逗号以方使书写. 比如:

  • fn foo(a: i32, b:i32,);
  • (48, 42,)
  • enum Colors { Red, Green, Blue, }

空的元组 Unit type

空的元组不包含任何元素, 写成 (), 也称为 unit type, 有点类似于C中的 void.

比如, 一个函数如果没有任何返回值, 那它返回的就是 unit type, 它的返回值类型可以省去.

fn main() {
  println!("hello, world");
}

就相当于:

fn main() -> () {
  println!("hello, world");
}

tuple 的内存布局

先看下面的示例代码:

use std::mem::size_of_val;

#[allow(clippy::approx_constant)]
fn main() {
    let tuple: (i32, f64, bool, char) = (1, 3.14, true, 'a');
    assert_eq!(size_of_val(&tuple), 24);
    assert_eq!(size_of_val(&tuple.0), 4);
    assert_eq!(size_of_val(&tuple.1), 8);
    assert_eq!(size_of_val(&tuple.2), 1);
    assert_eq!(size_of_val(&tuple.3), 4);
}

整个元组的内存布局情况如下图所示, 一共占用了24个字节:

tuple values

先查询第一个元素, tuple.0 是一个整数, 占4个字节:

tuple value at 0

接下来查询第二个元素, tuple.1 是一个双精度浮点值, 占用8个字节:

tuple value at 1

可以看到 tuple.0tuple.1 在内存中的顺序与在代码声明里的顺序是相反的, 这个是 rustc 编译器 自行处理的, 这是与C/C++编译器不一样的地方, rustc 不保证元素在内存中的实际顺序与其声明顺序一致.

再来看第三个元素, tuple.2, 是一个布尔值, 它本身只占1个字节:

tuple value at 2

最后一个元素, tuple.3, 是一个字符, 占用4个字节:

tuple value at 3

上面例子中, 元组里每个元素占用的字节数加起来是 4 + 8 + 1 + 4 = 17 17个字节; 但整个元组为什么 占用了24个字节呢? 这个与内存对齐 (memory alignment) 有关系:

  • 尽管 tuple.2bool类型, 但它占了4个字节, 其中的3个字节用作填充 (padding)
  • tuple.3char 类型, 它本身只需要4个字节, 但它后面又多出了4个字节, 后面的4个字节也被用作填充

tuple mem layout