Home

Rust 2.8 组合数据类型

11 Jan 2015 by LelouchHe

原文链接

同其他语言一样,Rust有很多内置类型.你已经接触过整型和字符串类型了,接下来,我们讨论下更加复杂的数据类型.

元组(tuple)

第一种数据类型是元组.元组是固定大小的有序列表.比如:

let x = (1, "hello");

小括号和逗号构成了长度为2的元组.下面类似,但增加了类型标注:

let x: (i32, &str) = (1, "hello");

可以看到,元组的类型和元组类似,只不过每个位置上都是类型名称,而不是其值.元组的元素可以是不同的:上面的元组中分别是i32类型和&str类型.以前没有见过&str类型,这个我们后面介绍字符串时会提到.系统编程语言中,字符串是比较复杂的.现在的话,姑且将&str称为”字符串片”(string slice),以后会详述.

你可以通过解析let(destructuring let)来获取元组内部元素的值.比如:

let (x, y, z) = (1, 2, 3);
println!("x is {}", x);

还记得么,我以前提到过let的左边比单纯的绑定赋值的功能要强大很多?这就是明证.我们可以在let的左边写成模式(pattern),当它和右边匹配(match)时,就会同时进行绑定.这里例子里,let解析或拆分了元组,并进行了3次绑定.

这个模式非常强大,后面还会看到的.

我们还可以不解析元组,而是把它当作整体来处理.你可以把一个元组赋值给另一个,只要他们的元素的个数和类型匹配即可.

let mut x = (1, 2);
let y = (2, 3);

x = y;

你也可以用==进行相等判断.而且只有当二者类型完全一致时,才能编译成功.(译者: 即类型如果不一致,连编译都无法通过,更无从比较相等了)

let x = (1, 2, 3);
let y = (2, 2, 4);

if x == y {
    println!("yes");
} else {
    println!("no");
}

上面的例子会输出”no”,因为二者类型一致,但值不相同.

另一个用途是通过元组返回多个值.

fn next_two(x: i32) -> (i32, i32) {
    (x + 1, x + 2)
}

fn main() {
    let (x, y) = next_two(5);
    println!("x, y = {}, {}", x, y);
}

虽然函数只能返回单值,但元组本身是一个值,只不过元组内恰好有多个值罢了.上面例子也演示了如何解析返回的元组.

元组是非常简单的数据类型,有时并不能满足要求.下面就是更强大的类型,结构体(struct).

结构体(struct)

结构体和元组类似,是另一种记录类型(record type).结构体的不同是:结构体给每个元素有单独的名字,称为字段(field)或成员变量(member).比如:

struct Point {
    x: i32,
    y: i32,
}

fn main() {
    let origin = Point{ x: 0, y: 0};
    println!("The origin is at ({}, {})", origin.x, origin.y);
}

上面有很多内容,我们慢慢分析.我们用struct声明了一个结构体,后面跟着它的名字(“Point”).按照惯例,结构体的名字要以大写字母开头,并且是CamelCase形式: “PointInSpace”, 而不是”Point_In_Space”.

可以使用let创建一个结构体的实例,还可以使用key: value的形式对每个字段赋值.赋值顺序和字段声明顺序不需要一致.

最后,因为字段有名字,所以可以通过点操作符(“.”)来获取字段: origin.x

结构体的值默认情况下,同Rust其他绑定类似,是不可变的.使用mut表示可变:

struct Point {
    x: i32,
    y: i32,
}

fn main() {
    let mut point = Point { x: 0, y: 0 };
    point.x = 5;

    println!("The point is at ({}, {})", point.x, point.y);
}

这个会输出”The point is at (5, 0)”.

元组结构体(tuple struct)和新类型

Rust还有叫做元组结构体(tuple struct)的类型,类似元组和结构体的混合类型.元组结构体有名字,但字段没名字:

struct Color(i32, i32, i32);
struct Point(i32, i32, i32);

这两个类型不相同,就算他们的值一样:

let black = Color(0, 0, 0);
let origin = Point(0, 0, 0);

通常来讲,结构体比元组结构体要好得多.我们可以重写下Color:

struct Color {
    red: i32,
    blue: i32,
    green: i32,
}

struct Point {
    x: i32,
    y: i32,
    z: i32,
}

这样的话,我们就可以使用字段名称,而不仅仅是位置了.好的名称是很重要的,使用结构体,我们就能利用这个名称.

但有一种情况,元组结构体是有用的,即只有一个元素的元组结构体.我们称之为”新类型”(newtype),因为这让你可以创造一个名字不同的新类型:

struct Inches(i32);

let length = Inches(10);

let Inches(integer_length) = length;
println!("length is {} inches",integer_length);

你可以通过解析let获取到其中的值.

枚举(enum)

最后,Rust有枚举(enum)类型.枚举类型非常有用,标准库里使用的非常广泛.下面就是标准库提供的一个枚举类型:

enum Ordering {
    Less,
    Equal,
    Greater,
}

一个Ordering类型的变量,只能是Less, Equal或者Greater.

因为这是标准库提供的,我们可以用use来引入.后面我们会详述,此时只要知道,use是引入名字的即可.

下面就是一个例子:

use std::cmp::Ordering;

fn cmp(a: i32, b: i32) -> Ordering {
    if a < b { Ordering::Less }
    else if a > b { Ordering::Greater }
    else { Ordering::Equal }
}

fn main() {
    let x = 5;
    let y = 10;

    let ordering = cmp(x, y);

    if ordering == Ordering::Less {
        println!("less");
    } else if ordering == Ordering::Greater {
        println!("greater");
    } else {
        println!("equal");
    }
}

这里有一个符号我们没加过: 双冒号(“::”).这个是用来表示命名空间(namespace)的.在这里,Orderingcmp模块里,而cmpstd模块里.后面我们会介绍模块(module).现在,知道可以使用use引入标准库即可.

我们再看下真实的代码.cmp比较两个整型大小,并返回Ordering类型的值.根据二者的大小,分别返回Ordering::Less, Ordering::GreaterOrdering::Equal.注意,每个枚举类型的值,都在这个枚举类型的命名空间里: 是Ordering::Greater,而不是Greater.

ordering变量的类型是Ordering,所以它包含上述3个值之一.我们可以使用一堆if/else来判断.但是重复的if/else很复杂.Rust有一个功能可以解除这个重复,同时避免遗漏某些情况.但在那之前,我们在看下另一种枚举: 单一值(one with value).

下面这个枚举有2个变量,但只有一个值:

enum OptionalInt {
    Value(i32),
    Missing,
}

这个枚举表示的是可选的整型.如果是Missing,我们就没有值了,否则,如果是Value,我们就有整型值.这个枚举是专门针对整型的,我们能让它适用于所有类型,但还有些内容后面才会学到,所以暂且不表.

你也可以在枚举中有任意数量的其他值:

enum OptionalColor {
    Color(i32, i32, i32),
    Missing,
}

或者像下面这个:

enum StringResult {
    StringOK(String),
    ErrorReason(String),
}

StringResult可以是StringResult::StringOK,其值是正常的计算结果字符串,或者是StringResult::ErrorReason,其值为错误原因的字符串.这些枚举都很有用,在标准库中有广泛用途.

下面是一个简单的例子:

enum StringResult {
    StringOK(String),
    ErrorReason(String),
}

fn respond(greeting: &str) -> StringResult {
    if greeting == "Hello" {
        StringResult::StringOK("Good morning!".to_string())
    } else {
        StringResult::ErrorReason("I didn't understannd you!".to_string())
    }
}

打的字太多了,使用use简化下:

use StringResult::StringOK;
use StringResult::ErrorReason;

enum StringResult {
    StringOK(String),
    ErrorReason(String),
}

fn respond(greeting: &str) -> StringResult {
    if greeting == "Hello" {
        StringOK("Good morning!".to_string())
    } else {
        ErrorReason("I didn't understannd you!".to_string())
    }
}

use声明必须在最开始,所以这里看上去有些诡异,我们在类型定义之前就需要声明use了.不管怎样,我们就可以直接使用StringOK,而不是全称StringResult::StringOK.引入名称非常方便,但有时会有冲突,所以需要谨慎.良好的风格是不进行引入.

可以看到,带值的枚举类型是非常有用的,而且如果引入泛型(generic)的话,会更有用.在那之前,我们将会学习使用模式匹配,通过模式匹配来处理枚举类型,会更加优雅,而且不用重复的if/else